carray 3.0.1 → 3.0.3

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 (229) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +824 -0
  3. data/README.md +5 -6
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_dispatch.c +258 -4
  6. data/ext/ca_axis_group.c +368 -184
  7. data/ext/ca_binop_dispatch.c +54 -32
  8. data/ext/ca_binop_dispatch.h +1 -0
  9. data/ext/ca_categorical_iterator.c +90 -41
  10. data/ext/ca_compare.c +8 -2
  11. data/ext/ca_for_buffer.h +21 -68
  12. data/ext/ca_group_iter.c +27 -11
  13. data/ext/ca_kernel_iterator.c +960 -245
  14. data/ext/ca_kernel_iterator.h +352 -181
  15. data/ext/ca_monop_dispatch.c +4 -4
  16. data/ext/ca_monop_dispatch.h +4 -0
  17. data/ext/ca_obj_array.c +194 -42
  18. data/ext/ca_obj_bincmp.c +152 -121
  19. data/ext/ca_obj_binop.c +107 -210
  20. data/ext/ca_obj_bitarray.c +37 -24
  21. data/ext/ca_obj_bitfield.c +95 -62
  22. data/ext/ca_obj_block.c +5 -4
  23. data/ext/ca_obj_byte_swap.c +40 -40
  24. data/ext/ca_obj_const_string.c +180 -50
  25. data/ext/ca_obj_face.c +428 -62
  26. data/ext/ca_obj_face.h +107 -47
  27. data/ext/ca_obj_fake.c +21 -50
  28. data/ext/ca_obj_farray.c +1 -0
  29. data/ext/ca_obj_field.c +16 -3
  30. data/ext/ca_obj_fixlen_string.c +39 -26
  31. data/ext/ca_obj_grid.c +181 -124
  32. data/ext/ca_obj_meld.c +235 -55
  33. data/ext/ca_obj_moncmp.c +61 -61
  34. data/ext/ca_obj_monop.c +125 -107
  35. data/ext/ca_obj_object.c +551 -233
  36. data/ext/ca_obj_record.c +51 -16
  37. data/ext/ca_obj_reduce.c +5 -8
  38. data/ext/ca_obj_refer.c +117 -33
  39. data/ext/ca_obj_remap.c +57 -17
  40. data/ext/ca_obj_repeat.c +21 -5
  41. data/ext/ca_obj_roll.c +115 -11
  42. data/ext/ca_obj_select.c +88 -75
  43. data/ext/ca_obj_select_axis.c +58 -60
  44. data/ext/ca_obj_shift.c +22 -102
  45. data/ext/ca_obj_stack.c +223 -123
  46. data/ext/ca_obj_stride.c +267 -35
  47. data/ext/ca_obj_string.c +25 -22
  48. data/ext/ca_obj_tile.c +32 -16
  49. data/ext/ca_obj_time.c +23 -25
  50. data/ext/ca_obj_timedelta.c +21 -23
  51. data/ext/ca_obj_transpose.c +12 -3
  52. data/ext/ca_obj_triop.c +110 -117
  53. data/ext/ca_obj_window.c +157 -29
  54. data/ext/ca_op_ipower.c +25 -13
  55. data/ext/ca_op_powi.h +57 -30
  56. data/ext/ca_pow5_table.h +656 -0
  57. data/ext/ca_rng_normal.h +42 -0
  58. data/ext/ca_rng_xoshiro256pp.h +105 -0
  59. data/ext/ca_sort_kernels.h +70 -0
  60. data/ext/ca_sweep_engine.c +385 -198
  61. data/ext/ca_sweep_engine.h +31 -23
  62. data/ext/ca_triop_dispatch.c +4 -1
  63. data/ext/ca_triop_dispatch.h +5 -2
  64. data/ext/caframe_csv_reader.c +463 -0
  65. data/ext/carray.h +90 -28
  66. data/ext/carray_access.c +487 -231
  67. data/ext/carray_address_basis.c +731 -0
  68. data/ext/carray_attribute.c +45 -3
  69. data/ext/carray_bincount.c +32 -2
  70. data/ext/carray_broadcast.c +49 -17
  71. data/ext/carray_call_cfunc.c +667 -511
  72. data/ext/carray_cast.c +1556 -247
  73. data/ext/carray_cast_func.rb +34 -21
  74. data/ext/carray_conversion.c +197 -73
  75. data/ext/carray_copy.c +84 -38
  76. data/ext/carray_core.c +591 -119
  77. data/ext/carray_count.c +148 -96
  78. data/ext/carray_element.c +18 -27
  79. data/ext/carray_factorize.c +604 -291
  80. data/ext/carray_generate.c +76 -40
  81. data/ext/carray_histogram.c +152 -94
  82. data/ext/carray_hold.c +2 -1
  83. data/ext/carray_index_classifier.c +63 -96
  84. data/ext/carray_internal.h +400 -1
  85. data/ext/carray_lazy.c +834 -79
  86. data/ext/carray_loop.c +88 -66
  87. data/ext/carray_mask.c +705 -324
  88. data/ext/carray_math_kernel.h +1 -0
  89. data/ext/carray_median_percentile.c +263 -63
  90. data/ext/carray_memory_view.c +254 -248
  91. data/ext/carray_operator.c +1093 -363
  92. data/ext/carray_order.c +133 -69
  93. data/ext/carray_partition.c +102 -56
  94. data/ext/carray_random.c +582 -204
  95. data/ext/carray_scatter.c +308 -65
  96. data/ext/carray_segment.c +257 -0
  97. data/ext/carray_slab.c +183 -70
  98. data/ext/carray_sort.c +235 -240
  99. data/ext/carray_sort_kernel.c +8 -1
  100. data/ext/carray_struct.c +99 -21
  101. data/ext/carray_test.c +167 -64
  102. data/ext/carray_time_text.c +1182 -0
  103. data/ext/carray_utils.c +363 -98
  104. data/ext/extconf.rb +27 -2
  105. data/ext/mk_call_cfunc.rb +103 -118
  106. data/ext/mk_pow5_table.rb +35 -0
  107. data/ext/mkkernel.rb +1349 -851
  108. data/ext/ruby_carray.c +17 -2
  109. data/ext/version.h +4 -4
  110. data/ext/version.rb +4 -2
  111. data/lib/carray/attribute.rb +39 -0
  112. data/lib/carray/autoload_carray.rb +24 -3
  113. data/lib/carray/autoload_method_extension.rb +18 -0
  114. data/lib/carray/axis_group.rb +245 -20
  115. data/lib/carray/basics.rb +95 -25
  116. data/lib/carray/bincount_nd.rb +6 -0
  117. data/lib/carray/block_iterator.rb +200 -54
  118. data/lib/carray/boolean_reduce.rb +21 -29
  119. data/lib/carray/categorical.rb +244 -59
  120. data/lib/carray/categorical_iterator.rb +391 -757
  121. data/lib/carray/complex.rb +36 -29
  122. data/lib/carray/conditional.rb +94 -7
  123. data/lib/carray/const_string.rb +177 -27
  124. data/lib/carray/construct.rb +40 -0
  125. data/lib/carray/data_type_extension.rb +3 -0
  126. data/lib/carray/data_type_limits.rb +91 -0
  127. data/lib/carray/fixlen_string.rb +1 -1
  128. data/lib/carray/frame/concat.rb +157 -6
  129. data/lib/carray/frame/convert.rb +13 -7
  130. data/lib/carray/frame/csv_parser.rb +590 -76
  131. data/lib/carray/frame/describe.rb +79 -0
  132. data/lib/carray/frame/frame.rb +205 -35
  133. data/lib/carray/frame/group.rb +199 -40
  134. data/lib/carray/frame/io.rb +452 -60
  135. data/lib/carray/frame/join.rb +134 -46
  136. data/lib/carray/frame/records.rb +54 -19
  137. data/lib/carray/frame/reshape.rb +448 -0
  138. data/lib/carray/frame/sort.rb +26 -2
  139. data/lib/carray/frame/verbs.rb +726 -56
  140. data/lib/carray/frame.rb +2 -0
  141. data/lib/carray/fuse_source.rb +111 -2
  142. data/lib/carray/fusion.rb +229 -38
  143. data/lib/carray/inspect.rb +71 -20
  144. data/lib/carray/iterator.rb +157 -2
  145. data/lib/carray/lazy.rb +54 -67
  146. data/lib/carray/mask_gap_fill.rb +66 -50
  147. data/lib/carray/math.rb +32 -19
  148. data/lib/carray/meld_reduce.rb +40 -7
  149. data/lib/carray/methods/align_addr.rb +8 -4
  150. data/lib/carray/methods/bincount.rb +2 -2
  151. data/lib/carray/methods/bit_string.rb +14 -3
  152. data/lib/carray/methods/composition.rb +16 -10
  153. data/lib/carray/methods/discovery_along.rb +74 -0
  154. data/lib/carray/methods/factorize.rb +50 -0
  155. data/lib/carray/methods/gather_nd.rb +13 -2
  156. data/lib/carray/methods/insert_block.rb +9 -14
  157. data/lib/carray/methods/is_in.rb +130 -6
  158. data/lib/carray/methods/join.rb +1 -1
  159. data/lib/carray/methods/locate_addr.rb +235 -21
  160. data/lib/carray/methods/mask_duplicates.rb +35 -1
  161. data/lib/carray/methods/mode.rb +18 -1
  162. data/lib/carray/methods/nunique.rb +22 -1
  163. data/lib/carray/methods/repeat.rb +91 -0
  164. data/lib/carray/methods/resize.rb +21 -23
  165. data/lib/carray/methods/snap.rb +34 -12
  166. data/lib/carray/methods/unique.rb +52 -3
  167. data/lib/carray/record_order.rb +208 -0
  168. data/lib/carray/rng.rb +86 -0
  169. data/lib/carray/runtime.rb +14 -11
  170. data/lib/carray/segment_iterator.rb +990 -0
  171. data/lib/carray/serialize.rb +197 -27
  172. data/lib/carray/slab_iterator.rb +109 -40
  173. data/lib/carray/stack.rb +10 -4
  174. data/lib/carray/string_operation_extension.rb +9 -4
  175. data/lib/carray/struct.rb +142 -51
  176. data/lib/carray/struct_builder.rb +110 -14
  177. data/lib/carray/table.rb +2 -2
  178. data/lib/carray/time.rb +535 -105
  179. data/lib/carray/window_iterator.rb +259 -82
  180. data/lib/carray.rb +4 -2
  181. data/yard-stubs/ca_obj_array.rb +4 -4
  182. data/yard-stubs/ca_obj_bitarray.rb +7 -0
  183. data/yard-stubs/ca_obj_bitfield.rb +23 -12
  184. data/yard-stubs/ca_obj_block.rb +2 -7
  185. data/yard-stubs/ca_obj_fake.rb +1 -1
  186. data/yard-stubs/ca_obj_field.rb +1 -1
  187. data/yard-stubs/ca_obj_refer.rb +3 -3
  188. data/yard-stubs/ca_obj_roll.rb +0 -1
  189. data/yard-stubs/ca_obj_stride.rb +10 -7
  190. data/yard-stubs/ca_obj_transpose.rb +1 -1
  191. data/yard-stubs/ca_obj_window.rb +11 -4
  192. data/yard-stubs/carray_access.rb +6 -5
  193. data/yard-stubs/carray_broadcast.rb +2 -2
  194. data/yard-stubs/carray_cast.rb +78 -43
  195. data/yard-stubs/carray_conversion.rb +13 -1
  196. data/yard-stubs/carray_copy.rb +8 -3
  197. data/yard-stubs/carray_core.rb +3 -83
  198. data/yard-stubs/carray_count.rb +16 -2
  199. data/yard-stubs/carray_lazy.rb +210 -1
  200. data/yard-stubs/carray_loop.rb +4 -1
  201. data/yard-stubs/carray_mask.rb +34 -10
  202. data/yard-stubs/carray_math.rb +1499 -4
  203. data/yard-stubs/carray_median_percentile.rb +26 -8
  204. data/yard-stubs/carray_order.rb +43 -82
  205. data/yard-stubs/carray_scatter.rb +10 -3
  206. data/yard-stubs/carray_segment.rb +72 -0
  207. data/yard-stubs/carray_slab.rb +11 -8
  208. data/yard-stubs/carray_sort.rb +50 -8
  209. data/yard-stubs/carray_test.rb +10 -0
  210. data/yard-stubs/carray_utils.rb +0 -26
  211. metadata +19 -19
  212. data/ext/ca_for_each_element.h +0 -239
  213. data/ext/carray_build_flags.h +0 -3
  214. data/ext/carray_config.h +0 -28
  215. data/ext/carray_kernels_bincmp.c +0 -4446
  216. data/ext/carray_kernels_binop.c +0 -11001
  217. data/ext/carray_kernels_init.c +0 -1131
  218. data/ext/carray_kernels_map.c +0 -3467
  219. data/ext/carray_kernels_moncmp.c +0 -2097
  220. data/ext/carray_kernels_monop.c +0 -18313
  221. data/ext/carray_kernels_reduce_aggregate.c +0 -25837
  222. data/ext/carray_kernels_reduce_boolean.c +0 -330
  223. data/ext/carray_kernels_reduce_cumulative.c +0 -14593
  224. data/ext/carray_kernels_reduce_extreme.c +0 -16948
  225. data/ext/carray_kernels_reduce_variance.c +0 -3910
  226. data/ext/carray_kernels_scan.c +0 -3693
  227. data/ext/carray_kernels_search.c +0 -32138
  228. data/ext/carray_kernels_sort.c +0 -10626
  229. data/ext/carray_kernels_triop.c +0 -1392
@@ -6,6 +6,8 @@
6
6
  # +project+ (length-preserving, miss -> UNDEF, Face-lift, read-only safe).
7
7
  # The frame layer just distributes that address to every column.
8
8
 
9
+ require "carray/methods/is_in" # promote_value_set, for align's Array reference
10
+
9
11
  class CAFrame
10
12
  # Default disambiguation for non-key columns present on both sides (§12-C):
11
13
  # left gets "_left", right gets "_right". +_left+/+_right+ read clearer than
@@ -13,13 +15,21 @@ class CAFrame
13
15
  DEFAULT_JOIN_SUFFIXES = ["_left", "_right"].freeze
14
16
  private_constant :DEFAULT_JOIN_SUFFIXES
15
17
 
16
- # Join +other+ on a shared key column.
18
+ # Join +other+ on a shared key column, as SQL joins: each pair of a left
19
+ # row and a right row with the same key gives one row, so a key on k left
20
+ # rows and m right rows gives k * m rows. A masked key matches nothing.
21
+ #
22
+ # how: :left (default) — every pair, and each left row with no match
23
+ # once, its right columns UNDEF.
24
+ # how: :inner — every pair.
25
+ # how: :right — every pair, and each right row with no match once.
26
+ # how: :outer — every pair, each unmatched left row, then each unmatched
27
+ # right row.
17
28
  #
18
- # how: :left (default) — keep all left rows; right columns gathered per
19
- # left row (locate_addr), misses become UNDEF.
20
- # how: :inner/:outer/:right — set-align both key sets (align_addr) and
21
- # gather both sides; the aligned key values form
22
- # the +on+ column.
29
+ # Rows come in left-row order (right-row order for :right), the pairs of
30
+ # one row in the other side's order. The left columns are views of this
31
+ # frame's, picked by row, wherever every row comes from the left (:left,
32
+ # :inner); the right columns are gathered into new arrays.
23
33
  #
24
34
  # A non-key column present on both sides collides. The key column (+on+) is
25
35
  # kept once, never suffixed. By default the collision is resolved by suffixing
@@ -28,18 +38,14 @@ class CAFrame
28
38
  # raise instead. Rename afterward with +rename+ if needed (§12-C).
29
39
  def join(other, on:, how: :left, suffixes: DEFAULT_JOIN_SUFFIXES)
30
40
  on = on.to_s
31
- lkey = self[on]
32
- rkey = other[on]
41
+ lkey = join_key(self[on], on, "join")
42
+ rkey = join_key(other[on], on, "join")
33
43
  plan = join_name_plan(other, on, suffixes)
34
44
 
35
- case how
36
- when :left
37
- join_left(other, on, lkey, rkey, plan)
38
- when :inner, :outer, :right
39
- join_align(other, on, lkey, rkey, how, plan)
40
- else
45
+ unless %i[left inner outer right].include?(how)
41
46
  raise ArgumentError, "unknown join mode #{how.inspect}"
42
47
  end
48
+ join_pairs(other, on, lkey, rkey, how, plan)
43
49
  end
44
50
 
45
51
  # As-of (nearest-key) left join for irregular series (memo §11.7). Each
@@ -47,16 +53,20 @@ class CAFrame
47
53
  # locate_nearest_addr (the per-row, row-preserving counterpart of
48
54
  # align_nearest_addr); +direction:+ follows CArray (:floor = most recent
49
55
  # at-or-before, :ceil = next, :round = nearest), and rows with no match
50
- # in range or beyond +tolerance:+ come back UNDEF. Same wiring as the left
51
- # join, only the address primitive differs.
56
+ # in that direction or beyond +tolerance:+ come back UNDEF. +tolerance:+
57
+ # is a number for a numeric key and a duration for a time key
58
+ # ("10 minutes", or a CATimedelta value). Same wiring as the left join,
59
+ # only the address primitive differs.
52
60
  def join_asof(other, on:, direction: :floor, tolerance: nil, suffixes: DEFAULT_JOIN_SUFFIXES)
53
61
  on = on.to_s
54
62
  plan = join_name_plan(other, on, suffixes)
55
- addr = self[on].locate_nearest_addr(other[on], direction: direction, tolerance: tolerance)
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)
56
66
  join_by_addr(other, on, addr, plan)
57
67
  end
58
68
 
59
- # Conform every variable to an externally supplied reference key set (the
69
+ # Conform every column to an externally supplied reference key set (the
60
70
  # asymmetric sibling of +join+; pandas +reindex+). Given a +reference+ array
61
71
  # of key values, each column is gathered onto it by exact key match
62
72
  # (+locate_addr+): the aligned key column (or index) becomes the reference
@@ -71,16 +81,25 @@ class CAFrame
71
81
  # df.align("time", reftime) # reftime = a CArray of reference key values
72
82
  #
73
83
  # +key+ may be a column name or the index axis name. +reference+ is a CArray
74
- # (or Array); its keys are matched against the source key by value, so their
75
- # data types must be comparable (a DateTime object key matches by eql?/hash).
84
+ # or an Array; its keys are matched against the source key by value. An
85
+ # Array is read as values of the key's kind, as +is_in+ reads one: numbers
86
+ # against a numeric key, Time / Date / String instants against a time key.
76
87
  def align(key, reference)
77
88
  key = key.to_s
78
- ref = reference.is_a?(CArray) ? reference : reference.to_ca
79
89
  on_index = !@columns.key?(key) && @axis_name == key && @index
80
- addr = ref.locate_addr(on_index ? @index : self[key])
90
+ keys = on_index ? @index : join_key(self[key], key, "align")
91
+ # An Array reference is read as values of the key's kind, as is_in reads
92
+ # one: 1 and 2.5 against a float key are numbers, and Time or String
93
+ # values against a time key are instants.
94
+ ref = reference.is_a?(CArray) ? reference : keys.send(:promote_value_set, reference)[1]
95
+ unless ref.ndim == 1
96
+ raise ArgumentError,
97
+ "align: the reference has shape #{ref.shape.inspect}; give one key value per row of the result"
98
+ end
99
+ addr = ref.locate_addr(keys)
81
100
 
82
101
  cols = {}
83
- variable_names.each { |name| cols[name] = project_rows(self[name], addr) }
102
+ column_names.each { |name| cols[name] = project_rows(self[name], addr) }
84
103
  if on_index
85
104
  CAFrame.new(cols, axis_name: key, index: ref)
86
105
  else
@@ -90,7 +109,7 @@ class CAFrame
90
109
  end
91
110
  end
92
111
 
93
- # Paste +other+'s variables beside this frame's, matched by **row position**
112
+ # Paste +other+'s columns beside this frame's, matched by **row position**
94
113
  # (memo §12-C) — a keyless column merge (the column-direction counterpart of
95
114
  # the row-stacking +concat+, and the positional counterpart of the key-aligned
96
115
  # +join+; named after the UNIX +paste+). Both frames must have the same
@@ -137,7 +156,7 @@ class CAFrame
137
156
 
138
157
  # Detect columns that appear in 2+ frames (collisions across the K inputs).
139
158
  name_count = Hash.new(0)
140
- frames.each { |f| f.variable_names.each { |name| name_count[name] += 1 } }
159
+ frames.each { |f| f.column_names.each { |name| name_count[name] += 1 } }
141
160
  collisions = name_count.select { |_, n| n > 1 }.keys
142
161
 
143
162
  if collisions.any?
@@ -160,7 +179,7 @@ class CAFrame
160
179
 
161
180
  cols = {}
162
181
  frames.each_with_index do |f, i|
163
- f.variable_names.each do |name|
182
+ f.column_names.each do |name|
164
183
  key = collisions.include?(name) ? "#{name}#{suffixes[i]}" : name
165
184
  if cols.key?(key)
166
185
  raise ArgumentError,
@@ -173,8 +192,17 @@ class CAFrame
173
192
  new(cols, axis_name: first.axis_name, index: first.index)
174
193
  end
175
194
 
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])"
202
+ end
203
+
176
204
  private def join_name_plan(other, on, suffixes)
177
- collisions = (variable_names & other.variable_names) - [on]
205
+ collisions = (column_names & other.column_names) - [on]
178
206
  return { collisions: [], lsuf: nil, rsuf: nil } if collisions.empty?
179
207
 
180
208
  if suffixes == false
@@ -202,41 +230,101 @@ class CAFrame
202
230
  cols[name] = col
203
231
  end
204
232
 
205
- private def join_left(other, on, lkey, rkey, plan)
206
- join_by_addr(other, on, lkey.locate_addr(rkey), plan)
207
- end
208
-
209
233
  private def join_by_addr(other, on, addr, plan)
210
234
  cols = {}
211
- variable_names.each { |name| put_join_column(cols, join_name(name, plan[:lsuf], plan), self[name]) }
212
- other.variable_names.each do |name|
235
+ column_names.each { |name| put_join_column(cols, join_name(name, plan[:lsuf], plan), self[name]) }
236
+ other.column_names.each do |name|
213
237
  next if name == on
214
238
  put_join_column(cols, join_name(name, plan[:rsuf], plan), project_rows(other[name], addr))
215
239
  end
216
240
  CAFrame.new(cols, axis_name: @axis_name, index: @index)
217
241
  end
218
242
 
219
- private def join_align(other, on, lkey, rkey, how, plan)
220
- common, a_idx, b_idx = CArray.align_addr(lkey, rkey, join: how)
243
+ # The rows of a key join: li / ri are the left / right row of each result
244
+ # 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)
252
+ if how == :right
253
+ ri, li = join_expand(rcode, lcode, common.elements, true)
254
+ else
255
+ li, ri = join_expand(lcode, rcode, common.elements, how != :inner)
256
+ if how == :outer
257
+ lcount = lcode.bincount(length: common.elements)
258
+ alone = rcode.is_masked | lcount.project(rcode).strip_mask(0).eq(0)
259
+ alone = alone.strip_mask(true)
260
+ extra = CArray.int64(rcode.elements).seq[alone]
261
+ if extra.elements > 0
262
+ undef_rows = CArray.int64(extra.elements).fill(UNDEF)
263
+ li = CArray.concatenate([li, undef_rows])
264
+ ri = CArray.concatenate([ri, extra])
265
+ code = CArray.concatenate([lcode.project(li[0...(li.elements - extra.elements)]),
266
+ rcode.project(extra)])
267
+ end
268
+ end
269
+ end
270
+ every_left = how == :left || how == :inner
271
+ identity = every_left && li.elements == nrow &&
272
+ (nrow == 0 || li.eq(CArray.int64(nrow).seq).all)
221
273
  cols = {}
222
- variable_names.each do |name|
223
- if name == on
224
- put_join_column(cols, on, common)
225
- else
226
- put_join_column(cols, join_name(name, plan[:lsuf], plan), project_rows(self[name], a_idx))
274
+ column_names.each do |name|
275
+ col = if identity then self[name]
276
+ elsif every_left then self[name][li, *([nil] * (self[name].ndim - 1))]
277
+ elsif name == on then nil
278
+ else project_rows(self[name], li)
279
+ end
280
+ if name == on && col.nil?
281
+ col = how == :right ? project_rows(rkey, ri) :
282
+ common.project(code || lcode.project(li))
227
283
  end
284
+ name = join_name(name, plan[:lsuf], plan) unless name == on
285
+ put_join_column(cols, name, col)
228
286
  end
229
- other.variable_names.each do |name|
287
+ other.column_names.each do |name|
230
288
  next if name == on
231
- put_join_column(cols, join_name(name, plan[:rsuf], plan), project_rows(other[name], b_idx))
289
+ put_join_column(cols, join_name(name, plan[:rsuf], plan), project_rows(other[name], ri))
232
290
  end
233
- # Carry the left index the same way its columns are gathered (by a_idx),
234
- # so it stays consistent with the left join; rows with no left match
235
- # (outer/right) become UNDEF.
236
- new_index = @index && project_rows(@index, a_idx)
291
+ new_index = if @index.nil? then nil
292
+ elsif identity then @index
293
+ elsif every_left then @index[li]
294
+ else project_rows(@index, li)
295
+ end
237
296
  CAFrame.new(cols, axis_name: @axis_name, index: new_index)
238
297
  end
239
298
 
299
+ # Pairs a side's rows (codes +acode+) with the other side's (+bcode+), in
300
+ # a-row order and, for one a-row, in b-row order. An a-row with no match
301
+ # comes once with no b-row when +keep_alone+. Returns [ai, bi].
302
+ private def join_expand(acode, bcode, k, keep_alone)
303
+ na, nb = acode.elements, bcode.elements
304
+ # b's rows by code, each code's rows in b-row order.
305
+ border = CArray.sort_addr(bcode, CArray.int64(nb).seq)
306
+ bcount = bcode.bincount(length: k).int64
307
+ bstart = bcount.cumsum.int64 - bcount
308
+ matches = bcount.project(acode).strip_mask(0)
309
+ reps = keep_alone ? matches.pmax(1) : matches
310
+ total = reps.sum.to_i
311
+ return [CArray.int64(0), CArray.int64(0)] if total == 0
312
+ # Each a-row repeated reps times: mark where each run starts and count.
313
+ rows = CArray.int64(na).seq[reps.gt(0)]
314
+ first = (reps.cumsum.int64 - reps)[reps.gt(0)]
315
+ mark = CArray.int64(total).fill(0)
316
+ mark[first] = 1
317
+ run = mark.cumsum.int64 - 1
318
+ ai = rows[run]
319
+ j = CArray.int64(total).seq - first[run]
320
+ bi = CArray.int64(total).fill(UNDEF)
321
+ hit = matches.project(ai).gt(0)
322
+ if hit.any
323
+ bi[hit] = border.project(bstart.project(acode.project(ai[hit])) + j[hit])
324
+ end
325
+ [ai, bi]
326
+ end
327
+
240
328
  private def project_rows(col, addr)
241
329
  return col.project(addr) if col.ndim == 1
242
330
  trailing = col.shape[1..]
@@ -16,9 +16,11 @@
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:+ casts named
20
- # columns afterward (same map / array-key forms as +cast+).
21
- def self.from_records(records, types: nil)
19
+ # in first-appearance order; keys are stringified. +types:+ takes
20
+ # anything +cast+ takes as a map (a name => type map, :infer, or a map with
21
+ # +default:+) and casts the built frame with it, with +on_error:+ as for
22
+ # +cast+.
23
+ def self.from_records(records, types: nil, on_error: :mask)
22
24
  unless records.is_a?(Array) && records.all? { |r| r.is_a?(Hash) }
23
25
  raise ArgumentError, "from_records expects an Array of Hashes"
24
26
  end
@@ -33,7 +35,7 @@ class CAFrame
33
35
  end
34
36
 
35
37
  frame = new(cols)
36
- frame.cast(types) if types
38
+ frame.cast(types, on_error: on_error, option_name: :types) unless types.nil?
37
39
  frame
38
40
  end
39
41
 
@@ -59,40 +61,73 @@ class CAFrame
59
61
  # native scalar column (nil -> UNDEF); anything else -> object column.
60
62
  def self.build_record_column(values, n)
61
63
  present = values.reject(&:nil?)
62
- return CArray.object(n) { values } if present.empty?
64
+ return mask_missing(CArray.object(n) { values }) if present.empty?
63
65
 
64
66
  if present.all? { |v| v.is_a?(Array) || v.is_a?(CArray) }
65
67
  build_nd_column(values, present, n)
66
68
  else
67
69
  type = numeric_leaf_type(present)
68
70
  col = CArray.object(n) { values }
69
- type ? col.to_type(type) : col
71
+ type ? col.to_type(type) : mask_missing(col)
70
72
  end
71
73
  end
72
74
  private_class_method :build_record_column
73
75
 
74
- # Stack equal-length array cells into an (N, L) column via an object 2-D fill
75
- # + to_type (nil rows -> UNDEF, int/float by leaf). Ragged lengths or
76
- # non-numeric leaves fall back to a 1-D object column of the raw cells.
76
+ # A missing cell is UNDEF, not a Ruby nil sitting in a cell. to_records
77
+ # writes a masked cell as nil, so nil on the way back in is the only
78
+ # spelling a missing cell has; to_type does this conversion for a numeric
79
+ # column, and an object column would otherwise keep the nil as a value --
80
+ # a row with no label coming back as a row labelled nil. The CSV reader
81
+ # takes the same position (see build_frame in io.rb).
82
+ def self.mask_missing(col)
83
+ col[:eq, nil] = UNDEF
84
+ col
85
+ end
86
+ private_class_method :mask_missing
87
+
88
+ # Stack array cells of one shape into an (N, *shape) column via an object
89
+ # fill + to_type (nil rows -> UNDEF, int/float by leaf); a cell may itself
90
+ # be nested, an (N, L, M) column from [[..], [..]] cells. Cells of
91
+ # different shapes, nesting that is not rectangular, or non-numeric leaves
92
+ # fall back to a 1-D object column of the raw cells.
77
93
  def self.build_nd_column(values, present, n)
78
- lengths = present.map { |v| v.is_a?(CArray) ? v.shape[0] : v.size }
79
- len = lengths.first
80
- return CArray.object(n) { values } unless lengths.all? { |x| x == len }
94
+ shapes = present.map { |v| v.is_a?(CArray) ? v.shape : nested_shape(v) }
95
+ shape = shapes.first
96
+ unless shape && shapes.all? { |x| x == shape }
97
+ return mask_missing(CArray.object(n) { values })
98
+ end
81
99
 
82
- nested = values.map { |v| v.nil? ? Array.new(len) : (v.is_a?(CArray) ? v.to_a : v) }
83
- type = numeric_leaf_type(nested.flatten.compact)
84
- table = CArray.object(n, len) { nested }
85
- type ? table.to_type(type) : table
100
+ 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) }
102
+ leaves = nested.flatten.compact
103
+ type = leaves.empty? ? nil : numeric_leaf_type(leaves)
104
+ table = CArray.object(n, *shape) { nested }
105
+ type ? table.to_type(type) : mask_missing(table)
86
106
  end
87
107
  private_class_method :build_nd_column
88
108
 
89
- # :int64 if every value is an Integer, :float64 if all are Numeric (int/float
90
- # mix), otherwise nil (strings / DateTime / booleans / mixed -> keep object).
109
+ # The shape of a rectangular nested Array, or nil when it is not
110
+ # rectangular. A leaf (anything but an Array) has shape [].
111
+ def self.nested_shape(v)
112
+ return [] unless v.is_a?(Array)
113
+ inner = v.map { |e| nested_shape(e) }
114
+ return [v.size] if v.empty?
115
+ return nil unless inner.first && inner.all? { |x| x == inner.first }
116
+ [v.size, *inner.first]
117
+ end
118
+ private_class_method :nested_shape
119
+
120
+ # :int64 if every value is an Integer, :float64 for Integers and Floats,
121
+ # :cmplx128 when Complex values join them, otherwise nil (keep object).
122
+ # 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.
91
124
  def self.numeric_leaf_type(values)
92
125
  if values.all? { |v| v.is_a?(Integer) }
93
126
  :int64
94
- elsif values.all? { |v| v.is_a?(Numeric) }
127
+ elsif values.all? { |v| v.is_a?(Integer) || v.is_a?(Float) }
95
128
  :float64
129
+ elsif values.all? { |v| v.is_a?(Integer) || v.is_a?(Float) || v.is_a?(Complex) }
130
+ :cmplx128
96
131
  end
97
132
  end
98
133
  private_class_method :numeric_leaf_type