carray 3.0.3 → 3.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +264 -0
  3. data/README.md +3 -2
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_descriptor.h +27 -24
  6. data/ext/ca_axis_dispatch.c +1 -6
  7. data/ext/ca_axis_group.c +27 -27
  8. data/ext/ca_binop_dispatch.c +1 -29
  9. data/ext/ca_binop_dispatch.h +0 -1
  10. data/ext/ca_categorical_iterator.c +205 -134
  11. data/ext/ca_compare.c +7 -18
  12. data/ext/ca_group_iter.c +16 -17
  13. data/ext/ca_kernel_iterator.c +1234 -1994
  14. data/ext/ca_kernel_iterator.h +391 -437
  15. data/ext/ca_monop_dispatch.c +82 -59
  16. data/ext/ca_monop_dispatch.h +1 -0
  17. data/ext/ca_obj_array.c +96 -39
  18. data/ext/ca_obj_bincmp.c +24 -21
  19. data/ext/ca_obj_binop.c +15 -26
  20. data/ext/ca_obj_bitarray.c +3 -2
  21. data/ext/ca_obj_bitfield.c +5 -6
  22. data/ext/ca_obj_block.c +18 -9
  23. data/ext/ca_obj_byte_swap.c +5 -6
  24. data/ext/ca_obj_const_string.c +173 -19
  25. data/ext/ca_obj_face.c +238 -57
  26. data/ext/ca_obj_face.h +19 -0
  27. data/ext/ca_obj_fake.c +15 -29
  28. data/ext/ca_obj_farray.c +6 -3
  29. data/ext/ca_obj_field.c +17 -5
  30. data/ext/ca_obj_fixlen_string.c +2 -3
  31. data/ext/ca_obj_grid.c +406 -132
  32. data/ext/ca_obj_meld.c +172 -34
  33. data/ext/ca_obj_moncmp.c +23 -8
  34. data/ext/ca_obj_monop.c +67 -57
  35. data/ext/ca_obj_object.c +157 -51
  36. data/ext/ca_obj_record.c +12 -7
  37. data/ext/ca_obj_reduce.c +13 -8
  38. data/ext/ca_obj_refer.c +25 -27
  39. data/ext/ca_obj_remap.c +30 -9
  40. data/ext/ca_obj_repeat.c +9 -10
  41. data/ext/ca_obj_roll.c +6 -5
  42. data/ext/ca_obj_select.c +14 -9
  43. data/ext/ca_obj_select_axis.c +15 -47
  44. data/ext/ca_obj_shift.c +11 -1
  45. data/ext/ca_obj_stack.c +179 -34
  46. data/ext/ca_obj_stride.c +162 -115
  47. data/ext/ca_obj_string.c +71 -4
  48. data/ext/ca_obj_tile.c +6 -5
  49. data/ext/ca_obj_time.c +22 -6
  50. data/ext/ca_obj_timedelta.c +22 -6
  51. data/ext/ca_obj_transpose.c +5 -3
  52. data/ext/ca_obj_triop.c +23 -32
  53. data/ext/ca_obj_window.c +190 -26
  54. data/ext/ca_sweep_engine.c +2 -2
  55. data/ext/caframe_csv_reader.c +80 -1
  56. data/ext/carray.h +181 -75
  57. data/ext/carray_access.c +184 -96
  58. data/ext/carray_address_basis.c +343 -87
  59. data/ext/carray_attribute.c +89 -6
  60. data/ext/carray_bincount.c +10 -8
  61. data/ext/carray_broadcast.c +13 -8
  62. data/ext/carray_build_flags.h +3 -0
  63. data/ext/carray_call_cfunc.c +108 -0
  64. data/ext/carray_cast.c +142 -24
  65. data/ext/carray_cast_func.rb +46 -6
  66. data/ext/carray_class.c +4 -3
  67. data/ext/carray_config.h +28 -0
  68. data/ext/carray_conversion.c +16 -7
  69. data/ext/carray_copy.c +51 -14
  70. data/ext/carray_core.c +147 -55
  71. data/ext/carray_count.c +50 -20
  72. data/ext/carray_element.c +76 -33
  73. data/ext/carray_factorize.c +118 -64
  74. data/ext/carray_generate.c +191 -50
  75. data/ext/carray_histogram.c +62 -17
  76. data/ext/carray_hold.c +12 -15
  77. data/ext/carray_index_classifier.c +388 -176
  78. data/ext/carray_internal.h +336 -29
  79. data/ext/carray_kernels_bincmp.c +4453 -0
  80. data/ext/carray_kernels_binop.c +11368 -0
  81. data/ext/carray_kernels_init.c +1734 -0
  82. data/ext/carray_kernels_moncmp.c +2104 -0
  83. data/ext/carray_kernels_monop.c +18369 -0
  84. data/ext/carray_kernels_reduce_aggregate.c +23818 -0
  85. data/ext/carray_kernels_reduce_boolean.c +406 -0
  86. data/ext/carray_kernels_reduce_cumulative.c +10086 -0
  87. data/ext/carray_kernels_reduce_extreme.c +17634 -0
  88. data/ext/carray_kernels_reduce_variance.c +3849 -0
  89. data/ext/carray_kernels_scan.c +4302 -0
  90. data/ext/carray_kernels_search.c +33829 -0
  91. data/ext/carray_kernels_sort.c +10906 -0
  92. data/ext/carray_kernels_triop.c +1864 -0
  93. data/ext/carray_key_windows.c +216 -0
  94. data/ext/carray_lazy.c +170 -111
  95. data/ext/carray_mask.c +292 -72
  96. data/ext/carray_median_percentile.c +82 -28
  97. data/ext/carray_memory_view.c +669 -561
  98. data/ext/carray_operator.c +124 -47
  99. data/ext/carray_order.c +384 -76
  100. data/ext/carray_partition.c +28 -27
  101. data/ext/carray_random.c +67 -13
  102. data/ext/carray_round.c +273 -0
  103. data/ext/carray_scatter.c +63 -38
  104. data/ext/carray_segment.c +3 -3
  105. data/ext/carray_slab.c +91 -28
  106. data/ext/carray_sort.c +35 -35
  107. data/ext/carray_struct.c +19 -18
  108. data/ext/carray_test.c +26 -5
  109. data/ext/carray_undef.c +4 -0
  110. data/ext/carray_utils.c +246 -45
  111. data/ext/mk_call_cfunc.rb +19 -2
  112. data/ext/mkkernel.rb +198 -240
  113. data/ext/ruby_carray.c +10 -4
  114. data/ext/version.h +4 -4
  115. data/lib/carray/attribute.rb +126 -22
  116. data/lib/carray/autoload_carray.rb +8 -31
  117. data/lib/carray/autoload_method_extension.rb +14 -5
  118. data/lib/carray/axis_group.rb +255 -106
  119. data/lib/carray/basics.rb +78 -10
  120. data/lib/carray/bincount_nd.rb +12 -2
  121. data/lib/carray/block_iterator.rb +126 -23
  122. data/lib/carray/boolean_reduce.rb +17 -17
  123. data/lib/carray/categorical.rb +165 -43
  124. data/lib/carray/categorical_iterator.rb +130 -27
  125. data/lib/carray/complex.rb +5 -5
  126. data/lib/carray/conditional.rb +67 -18
  127. data/lib/carray/const_string.rb +14 -5
  128. data/lib/carray/construct.rb +15 -11
  129. data/lib/carray/data_type_extension.rb +1 -0
  130. data/lib/carray/distance_limit.rb +77 -0
  131. data/lib/carray/fixlen_string.rb +7 -1
  132. data/lib/carray/frame/column_verbs.rb +70 -0
  133. data/lib/carray/frame/convert.rb +69 -0
  134. data/lib/carray/frame/csv_parser.rb +111 -17
  135. data/lib/carray/frame/describe.rb +3 -2
  136. data/lib/carray/frame/frame.rb +246 -21
  137. data/lib/carray/frame/group.rb +510 -95
  138. data/lib/carray/frame/io.rb +115 -28
  139. data/lib/carray/frame/join.rb +312 -51
  140. data/lib/carray/frame/records.rb +83 -7
  141. data/lib/carray/frame/reshape.rb +42 -35
  142. data/lib/carray/frame/sort.rb +7 -19
  143. data/lib/carray/frame/verbs.rb +188 -48
  144. data/lib/carray/frame/windows.rb +117 -0
  145. data/lib/carray/frame.rb +2 -0
  146. data/lib/carray/fuse_source.rb +11 -4
  147. data/lib/carray/fusion.rb +40 -16
  148. data/lib/carray/histogram.rb +6 -1
  149. data/lib/carray/inspect.rb +31 -8
  150. data/lib/carray/iterator.rb +85 -7
  151. data/lib/carray/key_windows.rb +620 -0
  152. data/lib/carray/lazy.rb +182 -183
  153. data/lib/carray/mask_gap_fill.rb +63 -57
  154. data/lib/carray/meld_reduce.rb +12 -14
  155. data/lib/carray/methods/bin.rb +26 -18
  156. data/lib/carray/methods/bincount.rb +16 -0
  157. data/lib/carray/methods/bit_string.rb +1 -1
  158. data/lib/carray/methods/choose.rb +16 -3
  159. data/lib/carray/methods/composition.rb +3 -1
  160. data/lib/carray/methods/gather_nd.rb +14 -5
  161. data/lib/carray/methods/insert_block.rb +2 -2
  162. data/lib/carray/methods/is_between.rb +59 -0
  163. data/lib/carray/methods/is_in.rb +53 -36
  164. data/lib/carray/methods/join.rb +1 -6
  165. data/lib/carray/methods/lag.rb +49 -0
  166. data/lib/carray/methods/locate_addr.rb +47 -51
  167. data/lib/carray/methods/mode.rb +1 -1
  168. data/lib/carray/methods/nunique.rb +11 -6
  169. data/lib/carray/methods/resize.rb +3 -3
  170. data/lib/carray/methods/value_counts.rb +4 -11
  171. data/lib/carray/record_order.rb +3 -3
  172. data/lib/carray/rng.rb +3 -0
  173. data/lib/carray/runtime.rb +53 -8
  174. data/lib/carray/segment_iterator.rb +313 -82
  175. data/lib/carray/serialize.rb +24 -10
  176. data/lib/carray/slab_iterator.rb +50 -0
  177. data/lib/carray/string.rb +105 -5
  178. data/lib/carray/string_operation_extension.rb +8 -1
  179. data/lib/carray/struct.rb +9 -21
  180. data/lib/carray/struct_builder.rb +19 -8
  181. data/lib/carray/time.rb +726 -104
  182. data/lib/carray/window_iterator.rb +122 -16
  183. data/lib/carray.rb +7 -0
  184. data/yard-stubs/ca_obj_bitfield.rb +1 -1
  185. data/yard-stubs/ca_obj_field.rb +4 -0
  186. data/yard-stubs/ca_obj_grid.rb +21 -10
  187. data/yard-stubs/ca_obj_refer.rb +2 -1
  188. data/yard-stubs/ca_obj_roll.rb +2 -2
  189. data/yard-stubs/ca_obj_shift.rb +5 -1
  190. data/yard-stubs/ca_obj_tile.rb +1 -1
  191. data/yard-stubs/ca_obj_window.rb +8 -3
  192. data/yard-stubs/carray_access.rb +6 -1
  193. data/yard-stubs/carray_attribute.rb +0 -5
  194. data/yard-stubs/carray_cast.rb +10 -0
  195. data/yard-stubs/carray_conversion.rb +2 -1
  196. data/yard-stubs/carray_copy.rb +4 -2
  197. data/yard-stubs/carray_count.rb +4 -4
  198. data/yard-stubs/carray_element.rb +10 -4
  199. data/yard-stubs/carray_generate.rb +21 -1
  200. data/yard-stubs/carray_mask.rb +26 -2
  201. data/yard-stubs/carray_math.rb +27 -1
  202. data/yard-stubs/carray_median_percentile.rb +5 -2
  203. data/yard-stubs/carray_memory_view.rb +40 -23
  204. data/yard-stubs/carray_order.rb +29 -18
  205. data/yard-stubs/carray_random.rb +13 -4
  206. data/yard-stubs/carray_scatter.rb +14 -9
  207. data/yard-stubs/carray_slab.rb +9 -2
  208. data/yard-stubs/carray_sort.rb +6 -5
  209. data/yard-stubs/carray_test.rb +5 -3
  210. metadata +25 -3
  211. data/lib/carray/arrow_tensor.rb +0 -401
  212. data/lib/carray/table.rb +0 -108
@@ -37,10 +37,12 @@ class CAFrame
37
37
  # Integer data_types that select rows positionally when used as a df[] key.
38
38
  INTEGER_TYPES = [:int8, :int16, :int32, :int64,
39
39
  :uint8, :uint16, :uint32, :uint64].freeze
40
+ Ractor.make_shareable(INTEGER_TYPES)
40
41
  private_constant :INTEGER_TYPES
41
42
 
42
43
  # @!visibility private
43
44
  DEFAULT_AXIS_NAME = "row"
45
+ Ractor.make_shareable(DEFAULT_AXIS_NAME)
44
46
 
45
47
  # Build a frame from a Hash of +name => column+. Columns may be CArrays or
46
48
  # anything that answers +to_ca+ (Ruby Array, lazy view). All columns must
@@ -51,22 +53,34 @@ class CAFrame
51
53
  # y)+). +:axis_name+ / +:index+ are control options; any remaining
52
54
  # (string-keyed) options are treated as columns.
53
55
  #
54
- # Column names are normalized to Strings, so a Symbol key in an explicit
55
- # column hash is stringified. The Symbol rejection below applies only to the
56
- # keyword channel, which doubles as the +:axis_name+ / +:index+ control-option
57
- # channel: a stray Symbol there is a mistyped control option, not a column.
56
+ # Column names are Strings, in an explicit column hash as in inline pairs.
57
+ # A stray Symbol among the keywords is a mistyped control option
58
+ # (+:axis_name+ / +:index+), not a column, and is named as such.
59
+ #
60
+ # +types:+ casts the built frame as +from_records+ does: it takes anything
61
+ # +cast+ takes as a map (a name => type map, :infer, or a map with
62
+ # +default:+), with +on_error:+ as for +cast+. A cast column is a new array,
63
+ # so a CArray given for it is no longer shared with the frame.
58
64
  def initialize(columns = {}, **opts)
59
65
  axis_name = opts.delete(:axis_name)
60
66
  index = opts.delete(:index)
61
- stray = opts.keys.reject { |k| k.is_a?(String) }
62
- unless stray.empty?
63
- raise ArgumentError,
64
- "column keys must be Strings (Symbols are reserved); got #{stray.inspect}"
67
+ attrs = opts.delete(:attrs)
68
+ types = opts.delete(:types)
69
+ on_error = opts.key?(:on_error) ? opts.delete(:on_error) : :mask
70
+ opts.each_key do |k|
71
+ CAFrame.__send__(:column_key, k, "CAFrame.new",
72
+ hint: "axis_name:, index:, attrs:, types: and on_error: are the options")
73
+ end
74
+ @attrs = nil
75
+ unless attrs.nil?
76
+ raise TypeError, "CAFrame.new: attrs: takes a Hash (got #{attrs.class})" unless attrs.is_a?(Hash)
77
+ set_attrs(attrs)
65
78
  end
66
79
  columns = columns.merge(opts) unless opts.empty?
67
80
 
68
81
  @columns = {}
69
- @axis_name = axis_name || DEFAULT_AXIS_NAME
82
+ @axis_name = axis_name.nil? ? DEFAULT_AXIS_NAME :
83
+ CAFrame.__send__(:column_key, axis_name, "CAFrame.new", "axis_name:")
70
84
  @index = nil
71
85
  # The row axis name this frame had before set_index promoted a column over
72
86
  # it, so reset_index can put it back. A frame built with an index never
@@ -77,7 +91,8 @@ class CAFrame
77
91
  n = nil
78
92
  n_from = nil
79
93
  columns.each do |name, col|
80
- key = name.to_s
94
+ key = column_key(name, "CAFrame.new")
95
+ raise ArgumentError, "CAFrame.new: a column name can not be empty" if key.empty?
81
96
  ca = coerce_column(col)
82
97
  len = ca.shape[0]
83
98
  if n.nil?
@@ -115,6 +130,8 @@ class CAFrame
115
130
  raise ArgumentError,
116
131
  "axis_name #{@axis_name.inspect} collides with a column of the same name"
117
132
  end
133
+
134
+ cast(types, on_error: on_error, option_name: :types) unless types.nil?
118
135
  end
119
136
 
120
137
  # --- df[...] : key type decides the axis (memo §13.2) ------------------
@@ -251,7 +268,11 @@ class CAFrame
251
268
  # first so a refused edit leaves the frame as it was: the edits change the
252
269
  # column table before any instance variable.
253
270
  private def refuse_if_frozen
254
- raise FrozenError.new("can't modify frozen CAFrame: #{inspect}", receiver: self) if frozen?
271
+ return unless frozen?
272
+ # The size, not the table: inspect prints every row.
273
+ size = "#{@nrow} row#{@nrow == 1 ? "" : "s"}, #{ncol} column#{ncol == 1 ? "" : "s"}"
274
+ raise FrozenError.new("can't modify frozen CAFrame (#{size})",
275
+ receiver: self)
255
276
  end
256
277
 
257
278
  # Make this frame read-only and return it. It is +freeze+ extended to the
@@ -325,6 +346,7 @@ class CAFrame
325
346
  # leaves the old one alone (the same rule +cast+ follows, for the physical
326
347
  # reason that a type change cannot reuse the buffer).
327
348
  private def rebind_column(key, column)
349
+ raise ArgumentError, "df[name] =: a column name can not be empty" if key.empty?
328
350
  if @index && key == @axis_name
329
351
  raise ArgumentError,
330
352
  "#{key.inspect} names the index, not a column; " \
@@ -369,8 +391,7 @@ class CAFrame
369
391
  def select(*names)
370
392
  raise ArgumentError, "select requires at least one column name" if names.empty?
371
393
  cols = {}
372
- names.each do |name|
373
- key = name.to_s
394
+ column_keys(names, "select").each do |key|
374
395
  raise KeyError, "no column #{key.inspect}" unless @columns.key?(key)
375
396
  cols[key] = @columns[key]
376
397
  end
@@ -399,7 +420,7 @@ class CAFrame
399
420
  mask = yield(self)
400
421
  unless mask.is_a?(CArray) && mask.data_type == :boolean
401
422
  raise ArgumentError,
402
- "filter block must return a boolean CArray (got #{mask.class})"
423
+ "filter block must return a boolean CArray (got #{mask.is_a?(CArray) ? "a #{CArray::Inspector.type_label(mask)} CArray" : mask.class})"
403
424
  end
404
425
  unless mask.shape == [@nrow]
405
426
  raise ArgumentError,
@@ -408,12 +429,69 @@ class CAFrame
408
429
  select_rows(mask, keep_masked: keep_masked)
409
430
  end
410
431
 
432
+ # Judge every row against named rules, and return the verdicts as a frame:
433
+ #
434
+ # report = df.verify(
435
+ # "age is not negative" => ->(f) { f["age"] >= 0 },
436
+ # "end after start" => ->(f) { f["end"] > f["start"] },
437
+ # )
438
+ # df.filter { |f| report["end after start"].not } # the rows that break it
439
+ #
440
+ # A rule is called with the frame, as a +filter+ block is, and returns a
441
+ # boolean CArray with one entry per row on its first axis; true means the
442
+ # rule holds. A rule over an N-D column may answer per cell, in the
443
+ # column's own shape, so the cells that break it can be masked directly.
444
+ # A cell the rule could not judge -- it read a masked value -- stays
445
+ # UNDEF rather than becoming false.
446
+ #
447
+ # The result has one column per rule, named after it, and this frame's
448
+ # index and row axis name.
449
+ #
450
+ # @param rules [Hash{String => #call}] the rules by name.
451
+ # @return [CAFrame]
452
+ # @raise [ArgumentError] when there are no rules, a name is empty, or a
453
+ # rule does not return a boolean CArray of this frame's length.
454
+ # @raise [TypeError] when a name is not a String.
455
+ def verify(rules)
456
+ unless rules.is_a?(Hash)
457
+ raise ArgumentError, "verify: the rules are a Hash of name => rule (got #{rules.class})"
458
+ end
459
+ raise ArgumentError, "verify: there are no rules" if rules.empty?
460
+ verdicts = {}
461
+ rules.each do |name, rule|
462
+ key = column_name_arg(name, "verify")
463
+ if @index && key == @axis_name
464
+ raise ArgumentError,
465
+ "verify: rule #{key.inspect} has the name of the row axis, " \
466
+ "which the index has in the result; name the rule otherwise"
467
+ end
468
+ unless rule.respond_to?(:call)
469
+ raise ArgumentError, "verify: rule #{key.inspect} is not callable (got #{rule.class})"
470
+ end
471
+ verdict = rule.call(self)
472
+ unless verdict.is_a?(CArray) && verdict.data_type == :boolean
473
+ raise ArgumentError,
474
+ "verify: rule #{key.inspect} must return a boolean CArray (got " \
475
+ "#{verdict.is_a?(CArray) ? "a #{verdict.data_type} CArray" : verdict.class})"
476
+ end
477
+ unless verdict.shape[0] == @nrow
478
+ raise ArgumentError,
479
+ "verify: rule #{key.inspect} returned shape #{verdict.shape.inspect}, " \
480
+ "expected #{@nrow} rows on the first axis"
481
+ end
482
+ verdicts[key] = verdict
483
+ end
484
+ # The verdicts are a table about the rows, not a part of this one, so the
485
+ # frame's attributes are not carried.
486
+ CAFrame.new(verdicts, axis_name: @axis_name, index: @index)
487
+ end
488
+
411
489
  # Move a column to the index and name the row axis after it (memo §3).
412
490
  # An index-role change: the data is unchanged, so this mutates self and
413
491
  # returns it (memo §3.8). Any existing index is replaced.
414
492
  def set_index(name)
415
493
  refuse_if_frozen
416
- key = name.to_s
494
+ key = column_key(name, "set_index")
417
495
  raise KeyError, "no column #{key.inspect}" unless @columns.key?(key)
418
496
  idx = @columns[key]
419
497
  unless idx.ndim == 1
@@ -517,7 +595,8 @@ class CAFrame
517
595
  def copy
518
596
  cols = {}
519
597
  @columns.each { |k, v| cols[k] = v.copy }
520
- CAFrame.new(cols, axis_name: @axis_name, index: @index && @index.copy)
598
+ CAFrame.new(cols, axis_name: @axis_name, index: @index && @index.copy,
599
+ attrs: @attrs)
521
600
  end
522
601
 
523
602
  # +dup+ / +clone+ share the column data -- that is the CArray dup contract
@@ -528,6 +607,84 @@ class CAFrame
528
607
  private def initialize_copy(other)
529
608
  super
530
609
  @columns = @columns.dup
610
+ @attrs = @attrs.dup if @attrs
611
+ end
612
+
613
+ # Marshal writes the instance variables as they are. Loading stores the
614
+ # attribute values again as set_attr does, as frozen copies: Marshal
615
+ # brings back a value thawed.
616
+ private def marshal_dump
617
+ instance_variables.to_h { |name| [name, instance_variable_get(name)] }
618
+ end
619
+
620
+ private def marshal_load(ivars)
621
+ ivars.each { |name, value| instance_variable_set(name, value) }
622
+ if @attrs
623
+ @attrs = @attrs.transform_values { |v| CArray.__send__(:attr_coerce_value, v) }
624
+ end
625
+ end
626
+
627
+ # --- attributes of the frame ------------------------------------------
628
+ #
629
+ # Key/value metadata about the table as a whole (a title, a source), apart
630
+ # from each column's own attributes. Keys and values follow the rules of
631
+ # CArray#set_attr: a Symbol or String key, stored as a String, and a
632
+ # JSON-compatible value, stored as a frozen copy.
633
+ #
634
+ # A frame made from this one takes its attributes when it holds the same
635
+ # table: a selection of rows (df[rows], filter, head, tail, sort_by,
636
+ # join(how: :semi / :anti)), a change of columns (select, append, drop,
637
+ # rename, ...), copy and dup. A frame computed from it -- a summary, a
638
+ # join, a reshape -- does not; ask the frame it came from. Each frame has
639
+ # its own Hash: setting an attribute on a frame made from this one leaves
640
+ # this one unchanged.
641
+
642
+ # The value of the attribute +key+, or nil when the frame has none.
643
+ def attr(key)
644
+ @attrs ? @attrs[CArray.__send__(:attr_normalize_key, key)] : nil
645
+ end
646
+
647
+ # Set the attribute +key+ to +value+ and return the stored value.
648
+ def set_attr(key, value)
649
+ refuse_if_frozen
650
+ CArray.__send__(:attr_validate_value, value)
651
+ (@attrs ||= {})[CArray.__send__(:attr_normalize_key, key)] =
652
+ CArray.__send__(:attr_coerce_value, value)
653
+ end
654
+
655
+ # Set every key of +hash+, as set_attr sets one, and return self. Every
656
+ # value is checked before any is written.
657
+ def set_attrs(hash)
658
+ refuse_if_frozen
659
+ raise TypeError, "set_attrs takes a Hash (got #{hash.class})" unless hash.is_a?(Hash)
660
+ entries = hash.map do |key, value|
661
+ CArray.__send__(:attr_validate_value, value)
662
+ [CArray.__send__(:attr_normalize_key, key), CArray.__send__(:attr_coerce_value, value)]
663
+ end
664
+ unless entries.empty?
665
+ h = (@attrs ||= {})
666
+ entries.each { |k, v| h[k] = v }
667
+ end
668
+ self
669
+ end
670
+
671
+ # Remove the attribute +key+ and return its value, or nil.
672
+ def delete_attr(key)
673
+ k = CArray.__send__(:attr_normalize_key, key)
674
+ return nil unless @attrs&.key?(k)
675
+ refuse_if_frozen
676
+ @attrs.delete(k)
677
+ end
678
+
679
+ # A frozen Hash of the frame's attributes.
680
+ def attrs
681
+ (@attrs ? @attrs.dup : {}).freeze
682
+ end
683
+
684
+ # Whether the frame has any attribute, or the attribute +key+.
685
+ def has_attr?(key = nil)
686
+ return !(@attrs.nil? || @attrs.empty?) if key.nil?
687
+ !!@attrs&.key?(CArray.__send__(:attr_normalize_key, key))
531
688
  end
532
689
 
533
690
  # --- metadata readers (memo §13.2) ------------------------------------
@@ -559,18 +716,28 @@ class CAFrame
559
716
  # Index column (CArray) or nil.
560
717
  attr_reader :index
561
718
 
562
- # name => data_type, freshly derived.
719
+ # name => data_type, freshly derived; a Face column gives its class
720
+ # (CATime, CAString, ...), since a Face's data_type is the surface its
721
+ # kernels dispatch on, not what the column holds.
563
722
  def data_types
564
723
  h = {}
565
- @columns.each { |k, v| h[k] = v.data_type }
724
+ @columns.each { |k, v| h[k] = v.face? ? v.class : v.data_type } # type-label: data_types returns the class itself, a value
566
725
  h
567
726
  end
568
727
 
728
+ # A frame of the same table with a different set of columns: it takes this
729
+ # frame's attributes.
569
730
  private def rebuild(cols)
570
- CAFrame.new(cols, axis_name: @axis_name, index: @index)
731
+ CAFrame.new(cols, axis_name: @axis_name, index: @index, attrs: @attrs)
571
732
  end
572
733
 
734
+ # A column given to the frame: a CArray as it is; a Ruby Array typed by its
735
+ # values as from_records types a record's (build_record_column); anything
736
+ # else through its to_ca.
573
737
  private def coerce_column(col)
738
+ if col.is_a?(Array)
739
+ return CAFrame.__send__(:build_record_column, col, col.size)
740
+ end
574
741
  if col.is_a?(CAFrame)
575
742
  raise ArgumentError,
576
743
  "a CAFrame cannot be a column (it answers to_ca as a 2-D matrix); " \
@@ -680,8 +847,10 @@ class CAFrame
680
847
  # column beforehand — silent
681
848
  # promotion in a splice would
682
849
  # hide schema drift.
850
+ splice_keep_attrs(new_cols[name], col)
683
851
  end
684
852
  new_index = splice_index(other, lo, hi)
853
+ splice_keep_attrs(new_index, @index) if new_index
685
854
  @columns = new_cols
686
855
  @index = new_index
687
856
  # Read the new count off something the frame now holds, rather than
@@ -697,6 +866,13 @@ class CAFrame
697
866
  self
698
867
  end
699
868
 
869
+ # A splice edits this frame's table, so a spliced column keeps the
870
+ # attributes this frame's column had, whatever the rows put in carry.
871
+ private def splice_keep_attrs(result, own)
872
+ (result.attrs.keys - own.attrs.keys).each { |k| result.delete_attr(k) }
873
+ result.set_attrs(own.attrs)
874
+ end
875
+
700
876
  # The index for a splice: head + other's index + tail. When this frame has
701
877
  # an index, the spliced-in frame must have one too (its rows need labels).
702
878
  private def splice_index(other, lo, hi)
@@ -774,7 +950,7 @@ class CAFrame
774
950
  cols[name] = col[selector, *([nil] * (col.ndim - 1))]
775
951
  end
776
952
  new_index = @index ? @index[selector] : nil
777
- CAFrame.new(cols, axis_name: @axis_name, index: new_index)
953
+ CAFrame.new(cols, axis_name: @axis_name, index: new_index, attrs: @attrs)
778
954
  end
779
955
 
780
956
  private def select_rows_keep_masked(selector)
@@ -802,8 +978,57 @@ class CAFrame
802
978
  # aliased index would write through to the parent from a frame whose columns
803
979
  # do not.
804
980
  new_index = @index ? @index[keep].copy : nil
805
- CAFrame.new(cols, axis_name: @axis_name, index: new_index)
981
+ CAFrame.new(cols, axis_name: @axis_name, index: new_index, attrs: @attrs)
982
+ end
983
+
984
+ # A column name given as an argument: a String, as the frame's column
985
+ # keys are. Symbols are the frame's options and types, never names.
986
+ private def column_key(name, verb)
987
+ CAFrame.__send__(:column_key, name, verb)
988
+ end
989
+
990
+ # A name a verb is given for a column -- or, with +what+, for the row
991
+ # axis -- is a String: a Symbol is a frame's option or type, never a
992
+ # name. Anything else is refused with TypeError "<verb>: <what> is a
993
+ # String (got :x)", +hint+ appended. A +verb+ that is an option, as
994
+ # "types:", already ends in its colon.
995
+ def self.column_key(name, verb, what = "a column name", hint: nil)
996
+ return name if name.is_a?(String)
997
+ message = "#{verb.end_with?(":") ? verb : "#{verb}:"} #{what} is a String (got #{name.inspect})"
998
+ message += "; #{hint}" if hint
999
+ raise TypeError, message
1000
+ end
1001
+ private_class_method :column_key
1002
+
1003
+ # A list of column names as a verb takes them: each a String, and none
1004
+ # twice.
1005
+ private def column_keys(names, verb)
1006
+ CAFrame.__send__(:refuse_repeated_names, names.map { |n| column_key(n, verb) }, verb)
1007
+ end
1008
+
1009
+ # Column names a verb makes or takes, none of them twice. A frame holds
1010
+ # one column per name, so a repeated name among new columns would keep
1011
+ # only the last of them, and among the columns a verb takes would be
1012
+ # taken once or twice depending on the verb. Refused with ArgumentError
1013
+ # "<verb>: <what> <name> more than once", +hint+ appended. df[...] alone
1014
+ # takes a name twice, as Hash#values_at does.
1015
+ def self.refuse_repeated_names(names, verb, what = "names", hint: nil)
1016
+ dup = names.tally.find { |_, count| count > 1 }
1017
+ return names unless dup
1018
+ message = "#{verb}: #{what} #{dup.first.inspect} more than once"
1019
+ message += "; #{hint}" if hint
1020
+ raise ArgumentError, message
1021
+ end
1022
+ private_class_method :refuse_repeated_names
1023
+
1024
+ # The labels an index is made of from a key -- a group's first values,
1025
+ # the resample bins, the sorted pivot keys -- as the key column shows
1026
+ # them: +labels+, already of the key's data type, Face and unit, given
1027
+ # the key's attributes. Returns +labels+.
1028
+ def self.key_labels(key, labels)
1029
+ key.__send__(:carry_attrs, labels)
806
1030
  end
1031
+ private_class_method :key_labels
807
1032
 
808
1033
  private def positional_range?(range)
809
1034
  (range.begin.nil? || range.begin.is_a?(Integer)) &&