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
@@ -7,12 +7,14 @@
7
7
  # The frame layer just distributes that address to every column.
8
8
 
9
9
  require "carray/methods/is_in" # promote_value_set, for align's Array reference
10
+ require "carray/methods/locate_addr" # nearest_refuse_bytes, for join_asof
10
11
 
11
12
  class CAFrame
12
13
  # Default disambiguation for non-key columns present on both sides (§12-C):
13
14
  # left gets "_left", right gets "_right". +_left+/+_right+ read clearer than
14
15
  # pandas' +_x+/+_y+ for the target audience.
15
16
  DEFAULT_JOIN_SUFFIXES = ["_left", "_right"].freeze
17
+ Ractor.make_shareable(DEFAULT_JOIN_SUFFIXES)
16
18
  private_constant :DEFAULT_JOIN_SUFFIXES
17
19
 
18
20
  # Join +other+ on a shared key column, as SQL joins: each pair of a left
@@ -36,16 +38,38 @@ class CAFrame
36
38
  # both sides (+suffixes: ["_left", "_right"]+); pass a 2-element array to pick
37
39
  # meaningful names up front (+["_obs", "_fcst"]+), or +suffixes: false+ to
38
40
  # raise instead. Rename afterward with +rename+ if needed (§12-C).
41
+ #
42
+ # +on:+ may name several columns, as an Array: rows then match when every
43
+ # key column matches, and a row with any key cell masked matches nothing.
44
+ #
45
+ # obs.join(fcst, on: ["station", "date"])
46
+ #
47
+ # Two modes filter this frame's rows by +other+ instead of pairing them:
48
+ #
49
+ # how: :semi — the rows whose key some row of +other+ has.
50
+ # how: :anti — the rows whose key no row of +other+ has.
51
+ #
52
+ # Each row appears at most once, in this frame's order, and no column of
53
+ # +other+ is added, so no name collides and +suffixes:+ is refused. A row
54
+ # with a masked key matches nothing: :semi drops it and :anti keeps it.
55
+ # The result shares this frame's columns and index, as +filter+'s does.
56
+ #
57
+ # obs.join(stations, on: "station", how: :anti) # stations not listed
39
58
  def join(other, on:, how: :left, suffixes: DEFAULT_JOIN_SUFFIXES)
40
- on = on.to_s
41
- lkey = join_key(self[on], on, "join")
42
- rkey = join_key(other[on], on, "join")
43
- plan = join_name_plan(other, on, suffixes)
44
-
45
- unless %i[left inner outer right].include?(how)
59
+ unless %i[left inner outer right semi anti].include?(how)
46
60
  raise ArgumentError, "unknown join mode #{how.inspect}"
47
61
  end
48
- join_pairs(other, on, lkey, rkey, how, plan)
62
+ if (how == :semi || how == :anti) && !suffixes.equal?(DEFAULT_JOIN_SUFFIXES)
63
+ raise ArgumentError, "join: how: #{how.inspect} adds no column of the other frame, so suffixes: does not apply"
64
+ end
65
+ names = join_key_names(on, "join", "on:")
66
+ lkeys = names.map { |name| key_column(name, "join") }
67
+ rkeys = names.map { |name| other.__send__(:key_column, name, "join") }
68
+ join_check_key_widths(names, lkeys, rkeys, "join")
69
+ return join_filter(lkeys, rkeys, how) if how == :semi || how == :anti
70
+
71
+ plan = join_name_plan(other, names, suffixes)
72
+ join_pairs(other, names, lkeys, rkeys, how, plan)
49
73
  end
50
74
 
51
75
  # As-of (nearest-key) left join for irregular series (memo §11.7). Each
@@ -57,21 +81,43 @@ class CAFrame
57
81
  # is a number for a numeric key and a duration for a time key
58
82
  # ("10 minutes", or a CATimedelta value). Same wiring as the left join,
59
83
  # only the address primitive differs.
60
- def join_asof(other, on:, direction: :floor, tolerance: nil, suffixes: DEFAULT_JOIN_SUFFIXES)
61
- on = on.to_s
62
- plan = join_name_plan(other, on, suffixes)
63
- lkey = join_key(self[on], on, "join_asof")
64
- rkey = join_key(other[on], on, "join_asof")
65
- addr = lkey.locate_nearest_addr(rkey, direction: direction, tolerance: tolerance)
66
- join_by_addr(other, on, addr, plan)
84
+ #
85
+ # +by:+ names columns that must match exactly, one name or an Array: each
86
+ # left row is matched to the nearest row among the rows of +other+ with
87
+ # the same +by+ values. A row with a masked +by+ cell matches nothing.
88
+ #
89
+ # obs.join_asof(radar, on: "time", by: "station", tolerance: "10 minutes")
90
+ def join_asof(other, on:, by: nil, direction: :floor, tolerance: nil, suffixes: DEFAULT_JOIN_SUFFIXES)
91
+ on = column_key(on, "join_asof")
92
+ exact = by.nil? ? [] : join_key_names(by, "join_asof", "by:")
93
+ if exact.include?(on)
94
+ raise ArgumentError, "join_asof: #{on.inspect} is the on: key; by: names the others"
95
+ end
96
+ plan = join_name_plan(other, [on, *exact], suffixes)
97
+ lkey = key_column(on, "join_asof")
98
+ rkey = other.__send__(:key_column, on, "join_asof")
99
+ [lkey, rkey].each do |key|
100
+ key.__send__(:nearest_refuse_bytes, key, "join_asof: the on: key #{on.inspect}",
101
+ "join on it exactly with join")
102
+ end
103
+ addr = if exact.empty?
104
+ lkey.locate_nearest_addr(rkey, direction: direction, tolerance: tolerance)
105
+ else
106
+ lexact = exact.map { |name| key_column(name, "join_asof") }
107
+ rexact = exact.map { |name| other.__send__(:key_column, name, "join_asof") }
108
+ join_check_key_widths(exact, lexact, rexact, "join_asof")
109
+ _, lcode, rcode, k = join_codes(lexact, rexact)
110
+ join_asof_by(lkey, rkey, lcode, rcode, k, direction, tolerance)
111
+ end
112
+ join_by_addr(other, [on, *exact], addr, plan)
67
113
  end
68
114
 
69
115
  # Conform every column to an externally supplied reference key set (the
70
116
  # asymmetric sibling of +join+; pandas +reindex+). Given a +reference+ array
71
117
  # of key values, each column is gathered onto it by exact key match
72
- # (+locate_addr+): the aligned key column (or index) becomes the reference
73
- # itself, and a reference key absent from the source comes back UNDEF in
74
- # every other column.
118
+ # (+locate_addr+): the aligned key column (or index) holds a copy of the
119
+ # reference, with the key's attributes, and a reference key absent from
120
+ # the source comes back UNDEF in every other column.
75
121
  #
76
122
  # CAFrame measures no interval and generates nothing -- the caller owns the
77
123
  # reference. This is the primitive for reindexing to a caller-built axis
@@ -85,9 +131,9 @@ class CAFrame
85
131
  # Array is read as values of the key's kind, as +is_in+ reads one: numbers
86
132
  # against a numeric key, Time / Date / String instants against a time key.
87
133
  def align(key, reference)
88
- key = key.to_s
134
+ key = column_key(key, "align")
89
135
  on_index = !@columns.key?(key) && @axis_name == key && @index
90
- keys = on_index ? @index : join_key(self[key], key, "align")
136
+ keys = key_column(key, "align")
91
137
  # An Array reference is read as values of the key's kind, as is_in reads
92
138
  # one: 1 and 2.5 against a float key are numbers, and Time or String
93
139
  # values against a time key are instants.
@@ -97,10 +143,15 @@ class CAFrame
97
143
  "align: the reference has shape #{ref.shape.inspect}; give one key value per row of the result"
98
144
  end
99
145
  addr = ref.locate_addr(keys)
146
+ # The result's keys are the reference's values, taken as they are now
147
+ # and shown with the key's attributes; the caller's array is left as
148
+ # it is.
149
+ ref = CAFrame.__send__(:key_labels, keys, reference.is_a?(CArray) ? ref.copy : ref)
100
150
 
101
151
  cols = {}
102
152
  column_names.each { |name| cols[name] = project_rows(self[name], addr) }
103
153
  if on_index
154
+ # key-labels: ref was given the key's attributes above
104
155
  CAFrame.new(cols, axis_name: key, index: ref)
105
156
  else
106
157
  cols[key] = ref
@@ -157,6 +208,9 @@ class CAFrame
157
208
  # Detect columns that appear in 2+ frames (collisions across the K inputs).
158
209
  name_count = Hash.new(0)
159
210
  frames.each { |f| f.column_names.each { |name| name_count[name] += 1 } }
211
+ # The first frame's index keeps its name as the result's row axis, so a
212
+ # column of that name collides with it as with a column.
213
+ name_count[first.axis_name] += 1 if first.index
160
214
  collisions = name_count.select { |_, n| n > 1 }.keys
161
215
 
162
216
  if collisions.any?
@@ -192,17 +246,159 @@ class CAFrame
192
246
  new(cols, axis_name: first.axis_name, index: first.index)
193
247
  end
194
248
 
195
- # The key column of a join, checked to give each row one value. An N-D key
196
- # would be matched cell by cell rather than row by row.
197
- private def join_key(col, on, verb)
198
- return col if col.ndim == 1
199
- raise ArgumentError,
200
- "#{verb}: the key #{on.inspect} has shape #{col.shape.inspect}, not one value per row; " \
201
- "#{verb} on one of its components (e.g. df[name][nil, 0])"
249
+ # A fixlen key holds a cell-width blob, so a left key and a right key of
250
+ # different widths share no value. Refuse it here, naming the column,
251
+ # rather than from inside the set operation the join runs.
252
+ private def join_check_key_widths(names, lkeys, rkeys, verb)
253
+ names.zip(lkeys, rkeys) do |name, l, r|
254
+ next unless l.data_type == CA_FIXLEN && r.data_type == CA_FIXLEN
255
+ next if l.face? || r.face? || l.bytes == r.bytes
256
+ raise CArray::DataTypeError,
257
+ "#{verb}: the key #{name.inspect} is fixlen #{l.bytes} bytes wide on the left " \
258
+ "and #{r.bytes} on the right; a fixlen value is a cell-width blob, so the widths must match"
259
+ end
260
+ end
261
+
262
+ # The key column names of a join: one name, or an Array of names.
263
+ private def join_key_names(on, verb, option)
264
+ names = on.is_a?(Array) ? on : [on]
265
+ raise ArgumentError, "#{verb}: #{option} names no key column" if names.empty?
266
+ CAFrame.__send__(:refuse_repeated_names, names.map { |name| column_key(name, verb) },
267
+ verb, "#{option} names")
268
+ end
269
+
270
+ # Codes for the rows of both sides of a join on +lkeys+ / +rkeys+ (one
271
+ # array per key column): equal codes for equal keys, in 0...k, and none
272
+ # for a row with a masked key cell. Each key column gets a code in one
273
+ # set of distinct values of both sides, compared as the lookups compare
274
+ # them (type, unit and width reconciled). With several key columns a
275
+ # row's key is the tuple of its codes, coded again one column at a time
276
+ # so the number stays below the row count. Returns [parts, lcode, rcode,
277
+ # k], parts holding each column's [distinct values, left codes, right
278
+ # codes].
279
+ private def join_codes(lkeys, rkeys)
280
+ parts = lkeys.zip(rkeys).map do |lkey, rkey|
281
+ common = CArray.align_addr(lkey, rkey, join: :outer)[0]
282
+ [common, lkey.locate_addr(common), rkey.locate_addr(common)]
283
+ end
284
+ _, lcode, rcode = parts[0]
285
+ k = parts[0][0].elements
286
+ parts.drop(1).each do |common, lc, rc|
287
+ # keys: the two frames are coded on the values they share, which
288
+ # CACategorical.from_keys (one side's own values) cannot give
289
+ lt = lcode * common.elements + lc
290
+ rt = rcode * common.elements + rc
291
+ tuples = CArray.align_addr(lt, rt, join: :outer)[0]
292
+ lcode, rcode, k = lt.locate_addr(tuples), rt.locate_addr(tuples), tuples.elements
293
+ end
294
+ [parts, lcode, rcode, k]
295
+ end
296
+
297
+ # The rows of a :semi or :anti join: whether each row's key code occurs
298
+ # among the other side's codes. A row without a code (a masked key cell)
299
+ # has no match.
300
+ private def join_filter(lkeys, rkeys, how)
301
+ _, lcode, rcode, k = join_codes(lkeys, rkeys)
302
+ matched = if k == 0
303
+ CArray.boolean(nrow).fill(false)
304
+ else
305
+ rcode.bincount(length: k).project(lcode).strip_mask(0).gt(0)
306
+ end
307
+ select_rows(how == :semi ? matched : matched.not)
202
308
  end
203
309
 
204
- private def join_name_plan(other, on, suffixes)
205
- collisions = (column_names & other.column_names) - [on]
310
+ # The other side's row for each left row of an as-of join with exact
311
+ # keys: the nearest by +lkey+ / +rkey+ among the right rows with the
312
+ # same code, as locate_nearest_addr finds it within one group.
313
+ #
314
+ # All groups are searched at once. The right rows are sorted by (code,
315
+ # key, row); sorting the left keys in among them by (code, key) counts,
316
+ # for each left row, the right rows of its group below it, and the
317
+ # nearest is one of the two right rows on either side of that count.
318
+ private def join_asof_by(lkey, rkey, lcode, rcode, k, direction, tolerance)
319
+ # direction: and tolerance: are checked here, on no rows, whether or
320
+ # not any group is in both frames.
321
+ lkey[0...0].locate_nearest_addr(rkey[0...0], direction: direction, tolerance: tolerance)
322
+ limit = tolerance && CArray::DistanceLimit.read(tolerance, lkey, "tolerance:")
323
+ addr = CArray.int64(lkey.elements).fill(UNDEF)
324
+
325
+ # The right rows that can be found: a code, and a key that is present
326
+ # and not NaN. Each group's rows in key order, a tie in row order.
327
+ rvalid = asof_searchable(rkey, rcode)
328
+ rrows = CArray.int64(rkey.elements).seq[rvalid].copy
329
+ return addr if rrows.elements == 0
330
+ rrows = rrows.project(CArray.sort_addr(rcode.project(rrows), rkey.project(rrows), rrows))
331
+ lvalid = asof_searchable(lkey, lcode)
332
+ lrows = CArray.int64(lkey.elements).seq[lvalid].copy
333
+ return addr if lrows.elements == 0
334
+
335
+ rk, qk = lkey.__send__(:nearest_keys, rkey.project(rrows), lkey.project(lrows))
336
+ rc = rcode.project(rrows).int64
337
+ qc = lcode.project(lrows).int64
338
+ n = rrows.elements
339
+ gcount = rc.bincount(length: k).int64
340
+ gstart = gcount.cumsum.int64 - gcount
341
+
342
+ # How many right rows of its group each left row has below it (at or
343
+ # below when +inclusive+): the left keys sorted in among the right
344
+ # ones, a tie putting the right one first when it counts.
345
+ below = lambda do |inclusive|
346
+ tag = CArray.int8(n + qk.elements)
347
+ tag[0...n] = inclusive ? 0 : 1
348
+ tag[n..-1] = inclusive ? 1 : 0
349
+ perm = CArray.sort_addr(CArray.concatenate([rc, qc]), CArray.concatenate([rk, qk]), tag)
350
+ seen = perm.lt(n).cumsum.int64
351
+ query = perm.ge(n)
352
+ count = CArray.int64(qk.elements)
353
+ count[perm[query] - n] = seen[query]
354
+ count - gstart.project(qc)
355
+ end
356
+
357
+ size = gcount.project(qc)
358
+ local =
359
+ case direction
360
+ when :floor then below.(true) - 1
361
+ when :ceil then below.(false)
362
+ when :round
363
+ lt = below.(false)
364
+ start = gstart.project(qc)
365
+ has_lo = lt.gt(0)
366
+ has_hi = lt.lt(size)
367
+ lo = rk.project((start + lt - 1).tap { |a| a[has_lo.not] = UNDEF })
368
+ hi = rk.project((start + lt).tap { |a| a[has_hi.not] = UNDEF })
369
+ # an exact match takes its first reference; otherwise the nearer
370
+ # neighbour, the upper one on a tie
371
+ upper = (hi - qk).le(qk - lo).strip_mask(false)
372
+ upper[has_lo.not] = true
373
+ upper[has_hi.not] = false
374
+ upper[hi.eq(qk).strip_mask(false)] = true
375
+ lt - 1 + upper.int64
376
+ end
377
+ local[local.lt(0) | local.ge(size)] = UNDEF
378
+ found = rrows.project(gstart.project(qc) + local)
379
+ if limit
380
+ dist = (rkey.project(found) - lkey.project(lrows)).abs
381
+ found[dist > limit] = UNDEF
382
+ end
383
+ addr[lrows] = found
384
+ addr
385
+ end
386
+
387
+ # The rows of a key that an as-of search can place: a group code, and a
388
+ # key cell that is neither masked nor NaN.
389
+ private def asof_searchable(key, code)
390
+ ok = code.is_not_masked.strip_mask(false)
391
+ ok &= key.is_not_masked.strip_mask(false) if key.has_mask?
392
+ ok &= key.is_nan.strip_mask(false).not if key.float?
393
+ ok
394
+ end
395
+
396
+ # The left index keeps its name as the result's row axis, so a right
397
+ # column of that name collides with it as with a left column: it takes
398
+ # the right suffix, and there is no left column to take the left one.
399
+ private def join_name_plan(other, names, suffixes)
400
+ taken = @index ? column_names | [@axis_name] : column_names
401
+ collisions = (taken & other.column_names) - names
206
402
  return { collisions: [], lsuf: nil, rsuf: nil } if collisions.empty?
207
403
 
208
404
  if suffixes == false
@@ -221,6 +417,7 @@ class CAFrame
221
417
  plan[:collisions].include?(name) ? "#{name}#{suffix}" : name
222
418
  end
223
419
 
420
+ # names-check: the output is checked as it is built, a column at a time
224
421
  private def put_join_column(cols, name, col)
225
422
  if cols.key?(name)
226
423
  raise ArgumentError,
@@ -230,11 +427,11 @@ class CAFrame
230
427
  cols[name] = col
231
428
  end
232
429
 
233
- private def join_by_addr(other, on, addr, plan)
430
+ private def join_by_addr(other, names, addr, plan)
234
431
  cols = {}
235
432
  column_names.each { |name| put_join_column(cols, join_name(name, plan[:lsuf], plan), self[name]) }
236
433
  other.column_names.each do |name|
237
- next if name == on
434
+ next if names.include?(name)
238
435
  put_join_column(cols, join_name(name, plan[:rsuf], plan), project_rows(other[name], addr))
239
436
  end
240
437
  CAFrame.new(cols, axis_name: @axis_name, index: @index)
@@ -242,19 +439,15 @@ class CAFrame
242
439
 
243
440
  # The rows of a key join: li / ri are the left / right row of each result
244
441
  # row, UNDEF on the side a row has no match on.
245
- private def join_pairs(other, on, lkey, rkey, how, plan)
246
- # Both keys get a code in one set of distinct values, compared as the
247
- # lookups compare them (type, unit and width reconciled); a masked key
248
- # gets none and matches nothing.
249
- common = CArray.align_addr(lkey, rkey, join: :outer)[0]
250
- lcode = lkey.locate_addr(common)
251
- rcode = rkey.locate_addr(common)
442
+ private def join_pairs(other, names, lkeys, rkeys, how, plan)
443
+ parts, lcode, rcode, k = join_codes(lkeys, rkeys)
444
+ extra = nil
252
445
  if how == :right
253
- ri, li = join_expand(rcode, lcode, common.elements, true)
446
+ ri, li = join_expand(rcode, lcode, k, true)
254
447
  else
255
- li, ri = join_expand(lcode, rcode, common.elements, how != :inner)
448
+ li, ri = join_expand(lcode, rcode, k, how != :inner)
256
449
  if how == :outer
257
- lcount = lcode.bincount(length: common.elements)
450
+ lcount = lcode.bincount(length: k)
258
451
  alone = rcode.is_masked | lcount.project(rcode).strip_mask(0).eq(0)
259
452
  alone = alone.strip_mask(true)
260
453
  extra = CArray.int64(rcode.elements).seq[alone]
@@ -262,33 +455,35 @@ class CAFrame
262
455
  undef_rows = CArray.int64(extra.elements).fill(UNDEF)
263
456
  li = CArray.concatenate([li, undef_rows])
264
457
  ri = CArray.concatenate([ri, extra])
265
- code = CArray.concatenate([lcode.project(li[0...(li.elements - extra.elements)]),
266
- rcode.project(extra)])
458
+ else
459
+ extra = nil
267
460
  end
268
461
  end
269
462
  end
270
463
  every_left = how == :left || how == :inner
271
464
  identity = every_left && li.elements == nrow &&
272
465
  (nrow == 0 || li.eq(CArray.int64(nrow).seq).all)
466
+ rows = { li: li, ri: ri, extra: extra, how: how, every_left: every_left, identity: identity }
273
467
  cols = {}
274
468
  column_names.each do |name|
275
- col = if identity then self[name]
469
+ j = names.index(name)
470
+ col = if j then joined_key(j, parts, lkeys, rkeys, rows)
471
+ elsif identity then self[name]
276
472
  elsif every_left then self[name][li, *([nil] * (self[name].ndim - 1))]
277
- elsif name == on then nil
278
473
  else project_rows(self[name], li)
279
474
  end
280
- if name == on && col.nil?
281
- col = how == :right ? project_rows(rkey, ri) :
282
- common.project(code || lcode.project(li))
283
- end
284
- name = join_name(name, plan[:lsuf], plan) unless name == on
475
+ name = join_name(name, plan[:lsuf], plan) unless j
285
476
  put_join_column(cols, name, col)
286
477
  end
287
478
  other.column_names.each do |name|
288
- next if name == on
479
+ next if names.include?(name)
289
480
  put_join_column(cols, join_name(name, plan[:rsuf], plan), project_rows(other[name], ri))
290
481
  end
482
+ # A key that is the left index is the result's index, as a key column
483
+ # is a column of it.
484
+ ij = @index && !@columns.key?(@axis_name) ? names.index(@axis_name) : nil
291
485
  new_index = if @index.nil? then nil
486
+ elsif ij then joined_key(ij, parts, lkeys, rkeys, rows)
292
487
  elsif identity then @index
293
488
  elsif every_left then @index[li]
294
489
  else project_rows(@index, li)
@@ -296,14 +491,58 @@ class CAFrame
296
491
  CAFrame.new(cols, axis_name: @axis_name, index: new_index)
297
492
  end
298
493
 
494
+ # The key +j+ of each row of a key join: the left key on the rows from the
495
+ # left, and on an unmatched right row of a right or outer join the right
496
+ # key, so a key is kept once and is never UNDEF where a row has one.
497
+ private def joined_key(j, parts, lkeys, rkeys, rows)
498
+ li, ri = rows[:li], rows[:ri]
499
+ return lkeys[j] if rows[:identity]
500
+ return lkeys[j][li] if rows[:every_left]
501
+ return project_rows(rkeys[j], ri) if rows[:how] == :right
502
+ common, lc, rc = parts[j]
503
+ extra = rows[:extra]
504
+ code = if extra
505
+ nleft = li.elements - extra.elements
506
+ CArray.concatenate([lc.project(li[0...nleft]), rc.project(extra)])
507
+ else
508
+ lc.project(li)
509
+ end
510
+ # The key takes the attributes both sides show alike, as a
511
+ # concatenation of the two would.
512
+ shared = CArray.__send__(:attrs_shared_by, [lkeys[j], rkeys[j]])
513
+ key = joined_key_kind(common.project(code), lkeys[j], rkeys[j])
514
+ key.tap { |c| c.set_attrs(shared) unless shared.empty? }
515
+ end
516
+
517
+ # The kind of a key column holding values of both sides, the same
518
+ # whichever side is the receiver: a Face both sides share stays (a
519
+ # categorical with the labels of both, the left's first), and keys of
520
+ # two different Faces, or of a Face and a plain array, are objects.
521
+ private def joined_key_kind(col, lkey, rkey)
522
+ lface = lkey.class if lkey.face?
523
+ rface = rkey.class if rkey.face?
524
+ return col if lface.nil? && rface.nil?
525
+ if lface == rface
526
+ return col unless lface <= CACategorical
527
+ values = CArray.object(col.elements)
528
+ values[] = col.to_a
529
+ return values.categorize(labels: lkey.labels | rkey.labels)
530
+ end
531
+ return col if !col.face? && col.object?
532
+ CArray.object(col.elements).tap { |o| o[] = col.to_a }
533
+ end
534
+
299
535
  # Pairs a side's rows (codes +acode+) with the other side's (+bcode+), in
300
536
  # a-row order and, for one a-row, in b-row order. An a-row with no match
301
537
  # comes once with no b-row when +keep_alone+. Returns [ai, bi].
302
538
  private def join_expand(acode, bcode, k, keep_alone)
303
539
  na, nb = acode.elements, bcode.elements
540
+ bcount = bcode.bincount(length: k).int64
541
+ if k == 0 || bcount.max <= 1
542
+ return join_expand_unique(acode, bcode, k, bcount, keep_alone)
543
+ end
304
544
  # b's rows by code, each code's rows in b-row order.
305
545
  border = CArray.sort_addr(bcode, CArray.int64(nb).seq)
306
- bcount = bcode.bincount(length: k).int64
307
546
  bstart = bcount.cumsum.int64 - bcount
308
547
  matches = bcount.project(acode).strip_mask(0)
309
548
  reps = keep_alone ? matches.pmax(1) : matches
@@ -325,6 +564,28 @@ class CAFrame
325
564
  [ai, bi]
326
565
  end
327
566
 
567
+ # join_expand when each code is on at most one b-row: an a-row pairs
568
+ # with that row or with none, so no a-row repeats. The b-row of each
569
+ # code is looked up once, without the repeat-and-scatter of the
570
+ # general case; the pairs come in the same order.
571
+ private def join_expand_unique(acode, bcode, k, bcount, keep_alone)
572
+ na, nb = acode.elements, bcode.elements
573
+ brow = CArray.int64(k).fill(UNDEF)
574
+ if nb > 0
575
+ present = bcode.is_not_masked
576
+ brow[bcode[present]] = CArray.int64(nb).seq![present]
577
+ end
578
+ if keep_alone
579
+ return [CArray.int64(0), CArray.int64(0)] if na == 0
580
+ [CArray.int64(na).seq!, brow.project(acode)]
581
+ else
582
+ hit = bcount.project(acode).strip_mask(0).gt(0)
583
+ ai = CArray.int64(na).seq![hit].copy
584
+ return [CArray.int64(0), CArray.int64(0)] if ai.elements == 0
585
+ [ai, brow.project(acode.project(ai))]
586
+ end
587
+ end
588
+
328
589
  private def project_rows(col, addr)
329
590
  return col.project(addr) if col.ndim == 1
330
591
  trailing = col.shape[1..]
@@ -16,7 +16,8 @@
16
16
 
17
17
  class CAFrame
18
18
  # Build a frame from an Array of row Hashes. Column set is the union of keys
19
- # in first-appearance order; keys are stringified. +types:+ takes
19
+ # in first-appearance order; keys are stringified, and two keys that
20
+ # stringify to one name (+:temp+ and +"temp"+) raise. +types:+ takes
20
21
  # anything +cast+ takes as a map (a name => type map, :infer, or a map with
21
22
  # +default:+) and casts the built frame with it, with +on_error:+ as for
22
23
  # +cast+.
@@ -27,6 +28,11 @@ class CAFrame
27
28
  return new if records.empty?
28
29
 
29
30
  keys = record_key_union(records)
31
+ if keys.any? { |k| k.to_s.empty? }
32
+ raise ArgumentError, "from_records: a key names a column, and a column name can not be empty"
33
+ end
34
+ refuse_repeated_names(keys.map(&:to_s), "from_records", "the keys name the column",
35
+ hint: "a String key and a Symbol key spell it both ways; use one spelling")
30
36
  n = records.size
31
37
  cols = {}
32
38
  keys.each do |key|
@@ -56,15 +62,27 @@ class CAFrame
56
62
  end
57
63
  private_class_method :record_key_union
58
64
 
59
- # Arrange one column's values (Ruby objects, nil for missing) into a CArray.
60
- # All-Array cells of equal length -> N-D native column; numeric scalars ->
61
- # native scalar column (nil -> UNDEF); anything else -> object column.
65
+ # Arrange one column's values (Ruby objects, nil or UNDEF for missing) into
66
+ # a CArray: the one rule a frame types Ruby values by, wherever it takes
67
+ # them (CAFrame.new and df[name]= given an Array, from_records, the values
68
+ # a group's block or Proc gives). All-Array cells of equal length -> N-D
69
+ # native column; numeric scalars -> native scalar column (missing ->
70
+ # UNDEF); Strings -> a text column (CAString), as from_csv reads text;
71
+ # time and duration elements -> a CATime / CATimedelta column in the finest
72
+ # resolution among them; anything else -> object column.
62
73
  def self.build_record_column(values, n)
74
+ values = values.map { |v| UNDEF.equal?(v) ? nil : v }
63
75
  present = values.reject(&:nil?)
64
76
  return mask_missing(CArray.object(n) { values }) if present.empty?
65
77
 
66
78
  if present.all? { |v| v.is_a?(Array) || v.is_a?(CArray) }
67
79
  build_nd_column(values, present, n)
80
+ elsif present.all? { |v| v.is_a?(String) }
81
+ CArray.string(values)
82
+ elsif present.all? { |v| v.is_a?(CATime::Element) }
83
+ CArray.time(values)
84
+ elsif present.all? { |v| v.is_a?(CATimedelta::Element) }
85
+ build_duration_column(values, present, n)
68
86
  else
69
87
  type = numeric_leaf_type(present)
70
88
  col = CArray.object(n) { values }
@@ -73,6 +91,27 @@ class CAFrame
73
91
  end
74
92
  private_class_method :build_record_column
75
93
 
94
+ # A CATimedelta column of duration elements, in the finest of their
95
+ # resolutions. Durations that have no common resolution (a month beside a
96
+ # second) stay an object column of the elements.
97
+ def self.build_duration_column(values, present, n)
98
+ unit = begin
99
+ present.map(&:unit).reduce { |a, b| CATimeUnitAlgebra.finer(a, b) }
100
+ rescue ArgumentError
101
+ return mask_missing(CArray.object(n) { values })
102
+ end
103
+ ticks = CArray.int64(n)
104
+ values.each_with_index do |v, i|
105
+ ticks[i] =
106
+ if v.nil? then UNDEF
107
+ elsif v.unit == unit then v.value
108
+ else CA_INT64([v.value]).timedelta(unit: v.unit).to_unit(unit).ticks[0]
109
+ end
110
+ end
111
+ ticks.timedelta(unit: unit)
112
+ end
113
+ private_class_method :build_duration_column
114
+
76
115
  # A missing cell is UNDEF, not a Ruby nil sitting in a cell. to_records
77
116
  # writes a masked cell as nil, so nil on the way back in is the only
78
117
  # spelling a missing cell has; to_type does this conversion for a numeric
@@ -98,14 +137,35 @@ class CAFrame
98
137
  end
99
138
 
100
139
  blank = shape.reverse.inject(nil) { |cell, d| Array.new(d) { cell } }
101
- nested = values.map { |v| v.nil? ? blank : (v.is_a?(CArray) ? v.to_a : v) }
140
+ # A missing leaf is nil or UNDEF (a masked cell of a CArray cell reads
141
+ # as UNDEF), the same two spellings a scalar cell has.
142
+ nested = values.map { |v| v.nil? ? blank : undef_to_nil(v.is_a?(CArray) ? v.to_a : v) }
102
143
  leaves = nested.flatten.compact
103
- type = leaves.empty? ? nil : numeric_leaf_type(leaves)
144
+ # Cells given as numeric CArrays keep their data type, the one the
145
+ # cells have in common, when that type holds every cell's values;
146
+ # otherwise (int64 beside uint64, a large int64 beside float32), and
147
+ # for Ruby values, the column is typed by its leaves.
148
+ common = if present.all? { |v| v.is_a?(CArray) && !v.face? && v.numeric? }
149
+ CArray.result_type(*present)
150
+ end
151
+ type = if common && present.all? { |v| holds_values?(v, common) }
152
+ common
153
+ elsif leaves.empty?
154
+ nil
155
+ else
156
+ numeric_leaf_type(leaves)
157
+ end
104
158
  table = CArray.object(n, *shape) { nested }
105
159
  type ? table.to_type(type) : mask_missing(table)
106
160
  end
107
161
  private_class_method :build_nd_column
108
162
 
163
+ def self.undef_to_nil(v)
164
+ return v.map { |e| undef_to_nil(e) } if v.is_a?(Array)
165
+ UNDEF.equal?(v) ? nil : v
166
+ end
167
+ private_class_method :undef_to_nil
168
+
109
169
  # The shape of a rectangular nested Array, or nil when it is not
110
170
  # rectangular. A leaf (anything but an Array) has shape [].
111
171
  def self.nested_shape(v)
@@ -120,8 +180,24 @@ class CAFrame
120
180
  # :int64 if every value is an Integer, :float64 for Integers and Floats,
121
181
  # :cmplx128 when Complex values join them, otherwise nil (keep object).
122
182
  # Other Numerics (Rational, BigDecimal) stay object: a float column would
123
- # round them, and a Complex one has room for them only as floats too.
183
+ # round them, and a Complex one has room for them only as floats too. So
184
+ # does an Integer an int64 cannot hold: it is kept as it is, not refused.
185
+ INT64_RANGE = (-2**63..2**63 - 1)
186
+ private_constant :INT64_RANGE
187
+
188
+ # Whether converting the numeric CArray +cell+ to +data_type+ keeps every
189
+ # value (a NaN stays a NaN).
190
+ def self.holds_values?(cell, data_type)
191
+ return true if cell.data_type == data_type
192
+ # coerce: the type CArray.result_type chose for the cells, tried on each
193
+ cell.to_type(data_type).to_a.flatten.zip(cell.to_a.flatten).all? do |a, b|
194
+ a == b || (a.respond_to?(:nan?) && b.respond_to?(:nan?) && a.nan? && b.nan?)
195
+ end
196
+ end
197
+ private_class_method :holds_values?
198
+
124
199
  def self.numeric_leaf_type(values)
200
+ return nil if values.any? { |v| v.is_a?(Integer) && !INT64_RANGE.cover?(v) }
125
201
  if values.all? { |v| v.is_a?(Integer) }
126
202
  :int64
127
203
  elsif values.all? { |v| v.is_a?(Integer) || v.is_a?(Float) }