carray 3.0.2 → 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 (247) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +568 -0
  3. data/README.md +6 -6
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_descriptor.h +27 -24
  6. data/ext/ca_axis_dispatch.c +220 -0
  7. data/ext/ca_axis_group.c +201 -123
  8. data/ext/ca_binop_dispatch.c +46 -52
  9. data/ext/ca_binop_dispatch.h +1 -1
  10. data/ext/ca_categorical_iterator.c +249 -183
  11. data/ext/ca_compare.c +7 -12
  12. data/ext/ca_for_buffer.h +21 -68
  13. data/ext/ca_group_iter.c +38 -23
  14. data/ext/ca_kernel_iterator.c +1765 -2076
  15. data/ext/ca_kernel_iterator.h +584 -566
  16. data/ext/ca_monop_dispatch.c +86 -63
  17. data/ext/ca_monop_dispatch.h +5 -0
  18. data/ext/ca_obj_array.c +223 -56
  19. data/ext/ca_obj_bincmp.c +172 -138
  20. data/ext/ca_obj_binop.c +116 -230
  21. data/ext/ca_obj_bitarray.c +40 -26
  22. data/ext/ca_obj_bitfield.c +100 -68
  23. data/ext/ca_obj_block.c +19 -9
  24. data/ext/ca_obj_byte_swap.c +45 -46
  25. data/ext/ca_obj_const_string.c +269 -44
  26. data/ext/ca_obj_face.c +612 -89
  27. data/ext/ca_obj_face.h +111 -47
  28. data/ext/ca_obj_fake.c +36 -79
  29. data/ext/ca_obj_farray.c +7 -3
  30. data/ext/ca_obj_field.c +33 -8
  31. data/ext/ca_obj_fixlen_string.c +23 -24
  32. data/ext/ca_obj_grid.c +541 -210
  33. data/ext/ca_obj_meld.c +331 -111
  34. data/ext/ca_obj_moncmp.c +80 -65
  35. data/ext/ca_obj_monop.c +177 -149
  36. data/ext/ca_obj_object.c +664 -248
  37. data/ext/ca_obj_record.c +63 -23
  38. data/ext/ca_obj_reduce.c +18 -16
  39. data/ext/ca_obj_refer.c +140 -58
  40. data/ext/ca_obj_remap.c +85 -24
  41. data/ext/ca_obj_repeat.c +24 -9
  42. data/ext/ca_obj_roll.c +121 -16
  43. data/ext/ca_obj_select.c +61 -58
  44. data/ext/ca_obj_select_axis.c +73 -107
  45. data/ext/ca_obj_shift.c +32 -102
  46. data/ext/ca_obj_stack.c +372 -122
  47. data/ext/ca_obj_stride.c +348 -140
  48. data/ext/ca_obj_string.c +88 -22
  49. data/ext/ca_obj_tile.c +38 -21
  50. data/ext/ca_obj_time.c +45 -31
  51. data/ext/ca_obj_timedelta.c +43 -29
  52. data/ext/ca_obj_transpose.c +17 -6
  53. data/ext/ca_obj_triop.c +125 -141
  54. data/ext/ca_obj_window.c +330 -44
  55. data/ext/ca_op_ipower.c +24 -11
  56. data/ext/ca_op_powi.h +57 -30
  57. data/ext/ca_pow5_table.h +656 -0
  58. data/ext/ca_sort_kernels.h +70 -0
  59. data/ext/ca_sweep_engine.c +104 -81
  60. data/ext/ca_sweep_engine.h +7 -20
  61. data/ext/ca_triop_dispatch.c +4 -1
  62. data/ext/ca_triop_dispatch.h +5 -2
  63. data/ext/caframe_csv_reader.c +542 -0
  64. data/ext/carray.h +241 -92
  65. data/ext/carray_access.c +608 -276
  66. data/ext/carray_address_basis.c +494 -97
  67. data/ext/carray_attribute.c +129 -4
  68. data/ext/carray_bincount.c +39 -7
  69. data/ext/carray_broadcast.c +56 -19
  70. data/ext/carray_build_flags.h +1 -1
  71. data/ext/carray_call_cfunc.c +108 -28
  72. data/ext/carray_cast.c +1576 -223
  73. data/ext/carray_cast_func.rb +77 -24
  74. data/ext/carray_class.c +4 -3
  75. data/ext/carray_conversion.c +208 -75
  76. data/ext/carray_copy.c +82 -24
  77. data/ext/carray_core.c +646 -162
  78. data/ext/carray_count.c +171 -88
  79. data/ext/carray_element.c +94 -60
  80. data/ext/carray_factorize.c +670 -324
  81. data/ext/carray_generate.c +260 -83
  82. data/ext/carray_histogram.c +209 -106
  83. data/ext/carray_hold.c +14 -16
  84. data/ext/carray_index_classifier.c +422 -243
  85. data/ext/carray_internal.h +692 -3
  86. data/ext/carray_kernels_bincmp.c +7 -0
  87. data/ext/carray_kernels_binop.c +487 -120
  88. data/ext/carray_kernels_init.c +653 -50
  89. data/ext/carray_kernels_moncmp.c +15 -8
  90. data/ext/carray_kernels_monop.c +99 -43
  91. data/ext/carray_kernels_reduce_aggregate.c +3925 -6112
  92. data/ext/carray_kernels_reduce_boolean.c +112 -36
  93. data/ext/carray_kernels_reduce_cumulative.c +2127 -6903
  94. data/ext/carray_kernels_reduce_extreme.c +3032 -2892
  95. data/ext/carray_kernels_reduce_variance.c +439 -500
  96. data/ext/carray_kernels_scan.c +1097 -488
  97. data/ext/carray_kernels_search.c +3787 -2176
  98. data/ext/carray_kernels_sort.c +1200 -920
  99. data/ext/carray_kernels_triop.c +479 -7
  100. data/ext/carray_key_windows.c +216 -0
  101. data/ext/carray_lazy.c +967 -153
  102. data/ext/carray_loop.c +81 -65
  103. data/ext/carray_mask.c +910 -324
  104. data/ext/carray_math_kernel.h +1 -0
  105. data/ext/carray_median_percentile.c +299 -100
  106. data/ext/carray_memory_view.c +872 -758
  107. data/ext/carray_operator.c +1171 -364
  108. data/ext/carray_order.c +505 -133
  109. data/ext/carray_partition.c +112 -65
  110. data/ext/carray_random.c +327 -239
  111. data/ext/carray_round.c +273 -0
  112. data/ext/carray_scatter.c +346 -78
  113. data/ext/carray_segment.c +257 -0
  114. data/ext/carray_slab.c +249 -86
  115. data/ext/carray_sort.c +224 -227
  116. data/ext/carray_sort_kernel.c +8 -1
  117. data/ext/carray_struct.c +118 -39
  118. data/ext/carray_test.c +193 -69
  119. data/ext/carray_time_text.c +1182 -0
  120. data/ext/carray_undef.c +4 -0
  121. data/ext/carray_utils.c +579 -113
  122. data/ext/extconf.rb +27 -2
  123. data/ext/mk_call_cfunc.rb +19 -4
  124. data/ext/mk_pow5_table.rb +35 -0
  125. data/ext/mkkernel.rb +1202 -1014
  126. data/ext/ruby_carray.c +17 -5
  127. data/ext/version.h +4 -4
  128. data/ext/version.rb +4 -2
  129. data/lib/carray/attribute.rb +160 -17
  130. data/lib/carray/autoload_carray.rb +23 -27
  131. data/lib/carray/autoload_method_extension.rb +20 -5
  132. data/lib/carray/axis_group.rb +360 -63
  133. data/lib/carray/basics.rb +162 -28
  134. data/lib/carray/bincount_nd.rb +18 -2
  135. data/lib/carray/block_iterator.rb +231 -58
  136. data/lib/carray/boolean_reduce.rb +34 -42
  137. data/lib/carray/categorical.rb +242 -52
  138. data/lib/carray/categorical_iterator.rb +437 -827
  139. data/lib/carray/complex.rb +41 -34
  140. data/lib/carray/conditional.rb +150 -14
  141. data/lib/carray/const_string.rb +59 -4
  142. data/lib/carray/construct.rb +15 -11
  143. data/lib/carray/data_type_extension.rb +1 -0
  144. data/lib/carray/distance_limit.rb +77 -0
  145. data/lib/carray/fixlen_string.rb +7 -1
  146. data/lib/carray/frame/column_verbs.rb +70 -0
  147. data/lib/carray/frame/concat.rb +157 -6
  148. data/lib/carray/frame/convert.rb +82 -7
  149. data/lib/carray/frame/csv_parser.rb +688 -87
  150. data/lib/carray/frame/describe.rb +80 -0
  151. data/lib/carray/frame/frame.rb +369 -45
  152. data/lib/carray/frame/group.rb +631 -90
  153. data/lib/carray/frame/io.rb +506 -79
  154. data/lib/carray/frame/join.rb +411 -62
  155. data/lib/carray/frame/records.rb +116 -19
  156. data/lib/carray/frame/reshape.rb +455 -0
  157. data/lib/carray/frame/sort.rb +30 -18
  158. data/lib/carray/frame/verbs.rb +868 -61
  159. data/lib/carray/frame/windows.rb +117 -0
  160. data/lib/carray/frame.rb +4 -0
  161. data/lib/carray/fuse_source.rb +118 -2
  162. data/lib/carray/fusion.rb +257 -42
  163. data/lib/carray/histogram.rb +6 -1
  164. data/lib/carray/inspect.rb +54 -13
  165. data/lib/carray/iterator.rb +99 -9
  166. data/lib/carray/key_windows.rb +620 -0
  167. data/lib/carray/lazy.rb +216 -193
  168. data/lib/carray/mask_gap_fill.rb +112 -92
  169. data/lib/carray/math.rb +32 -19
  170. data/lib/carray/meld_reduce.rb +52 -21
  171. data/lib/carray/methods/align_addr.rb +8 -4
  172. data/lib/carray/methods/bin.rb +26 -18
  173. data/lib/carray/methods/bincount.rb +18 -2
  174. data/lib/carray/methods/bit_string.rb +15 -4
  175. data/lib/carray/methods/choose.rb +16 -3
  176. data/lib/carray/methods/composition.rb +18 -10
  177. data/lib/carray/methods/factorize.rb +2 -2
  178. data/lib/carray/methods/gather_nd.rb +25 -5
  179. data/lib/carray/methods/insert_block.rb +9 -14
  180. data/lib/carray/methods/is_between.rb +59 -0
  181. data/lib/carray/methods/is_in.rb +140 -10
  182. data/lib/carray/methods/join.rb +2 -7
  183. data/lib/carray/methods/lag.rb +49 -0
  184. data/lib/carray/methods/locate_addr.rb +182 -45
  185. data/lib/carray/methods/mode.rb +18 -1
  186. data/lib/carray/methods/nunique.rb +11 -6
  187. data/lib/carray/methods/repeat.rb +1 -20
  188. data/lib/carray/methods/resize.rb +21 -23
  189. data/lib/carray/methods/snap.rb +34 -12
  190. data/lib/carray/methods/unique.rb +11 -2
  191. data/lib/carray/methods/value_counts.rb +4 -11
  192. data/lib/carray/record_order.rb +208 -0
  193. data/lib/carray/rng.rb +3 -0
  194. data/lib/carray/runtime.rb +65 -17
  195. data/lib/carray/segment_iterator.rb +1221 -0
  196. data/lib/carray/serialize.rb +214 -30
  197. data/lib/carray/slab_iterator.rb +101 -27
  198. data/lib/carray/stack.rb +10 -4
  199. data/lib/carray/string.rb +105 -5
  200. data/lib/carray/string_operation_extension.rb +12 -4
  201. data/lib/carray/struct.rb +137 -58
  202. data/lib/carray/struct_builder.rb +124 -17
  203. data/lib/carray/time.rb +1222 -186
  204. data/lib/carray/window_iterator.rb +242 -81
  205. data/lib/carray.rb +9 -2
  206. data/yard-stubs/ca_obj_array.rb +4 -4
  207. data/yard-stubs/ca_obj_bitarray.rb +7 -0
  208. data/yard-stubs/ca_obj_bitfield.rb +23 -12
  209. data/yard-stubs/ca_obj_fake.rb +1 -1
  210. data/yard-stubs/ca_obj_field.rb +5 -1
  211. data/yard-stubs/ca_obj_grid.rb +21 -10
  212. data/yard-stubs/ca_obj_refer.rb +5 -4
  213. data/yard-stubs/ca_obj_roll.rb +2 -3
  214. data/yard-stubs/ca_obj_shift.rb +5 -1
  215. data/yard-stubs/ca_obj_stride.rb +10 -7
  216. data/yard-stubs/ca_obj_tile.rb +1 -1
  217. data/yard-stubs/ca_obj_transpose.rb +1 -1
  218. data/yard-stubs/ca_obj_window.rb +9 -5
  219. data/yard-stubs/carray_access.rb +11 -5
  220. data/yard-stubs/carray_attribute.rb +0 -5
  221. data/yard-stubs/carray_broadcast.rb +1 -1
  222. data/yard-stubs/carray_cast.rb +88 -43
  223. data/yard-stubs/carray_conversion.rb +15 -2
  224. data/yard-stubs/carray_copy.rb +12 -5
  225. data/yard-stubs/carray_core.rb +3 -3
  226. data/yard-stubs/carray_count.rb +9 -0
  227. data/yard-stubs/carray_element.rb +10 -4
  228. data/yard-stubs/carray_generate.rb +21 -1
  229. data/yard-stubs/carray_lazy.rb +5 -1
  230. data/yard-stubs/carray_loop.rb +4 -1
  231. data/yard-stubs/carray_mask.rb +60 -12
  232. data/yard-stubs/carray_math.rb +52 -14
  233. data/yard-stubs/carray_median_percentile.rb +15 -8
  234. data/yard-stubs/carray_memory_view.rb +40 -23
  235. data/yard-stubs/carray_order.rb +61 -29
  236. data/yard-stubs/carray_random.rb +13 -4
  237. data/yard-stubs/carray_scatter.rb +21 -9
  238. data/yard-stubs/carray_segment.rb +72 -0
  239. data/yard-stubs/carray_slab.rb +10 -2
  240. data/yard-stubs/carray_sort.rb +49 -8
  241. data/yard-stubs/carray_test.rb +15 -3
  242. data/yard-stubs/carray_utils.rb +0 -26
  243. metadata +19 -5
  244. data/ext/ca_for_each_element.h +0 -239
  245. data/ext/carray_kernels_map.c +0 -3467
  246. data/lib/carray/arrow_tensor.rb +0 -401
  247. data/lib/carray/table.rb +0 -108
@@ -6,40 +6,70 @@
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
+ require "carray/methods/locate_addr" # nearest_refuse_bytes, for join_asof
11
+
9
12
  class CAFrame
10
13
  # Default disambiguation for non-key columns present on both sides (§12-C):
11
14
  # left gets "_left", right gets "_right". +_left+/+_right+ read clearer than
12
15
  # pandas' +_x+/+_y+ for the target audience.
13
16
  DEFAULT_JOIN_SUFFIXES = ["_left", "_right"].freeze
17
+ Ractor.make_shareable(DEFAULT_JOIN_SUFFIXES)
14
18
  private_constant :DEFAULT_JOIN_SUFFIXES
15
19
 
16
- # Join +other+ on a shared key column.
20
+ # Join +other+ on a shared key column, as SQL joins: each pair of a left
21
+ # row and a right row with the same key gives one row, so a key on k left
22
+ # rows and m right rows gives k * m rows. A masked key matches nothing.
23
+ #
24
+ # how: :left (default) — every pair, and each left row with no match
25
+ # once, its right columns UNDEF.
26
+ # how: :inner — every pair.
27
+ # how: :right — every pair, and each right row with no match once.
28
+ # how: :outer — every pair, each unmatched left row, then each unmatched
29
+ # right row.
17
30
  #
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.
31
+ # Rows come in left-row order (right-row order for :right), the pairs of
32
+ # one row in the other side's order. The left columns are views of this
33
+ # frame's, picked by row, wherever every row comes from the left (:left,
34
+ # :inner); the right columns are gathered into new arrays.
23
35
  #
24
36
  # A non-key column present on both sides collides. The key column (+on+) is
25
37
  # kept once, never suffixed. By default the collision is resolved by suffixing
26
38
  # both sides (+suffixes: ["_left", "_right"]+); pass a 2-element array to pick
27
39
  # meaningful names up front (+["_obs", "_fcst"]+), or +suffixes: false+ to
28
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
29
58
  def join(other, on:, how: :left, suffixes: DEFAULT_JOIN_SUFFIXES)
30
- on = on.to_s
31
- lkey = self[on]
32
- rkey = other[on]
33
- plan = join_name_plan(other, on, suffixes)
34
-
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
59
+ unless %i[left inner outer right semi anti].include?(how)
41
60
  raise ArgumentError, "unknown join mode #{how.inspect}"
42
61
  end
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)
43
73
  end
44
74
 
45
75
  # As-of (nearest-key) left join for irregular series (memo §11.7). Each
@@ -47,21 +77,47 @@ class CAFrame
47
77
  # locate_nearest_addr (the per-row, row-preserving counterpart of
48
78
  # align_nearest_addr); +direction:+ follows CArray (:floor = most recent
49
79
  # 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.
52
- def join_asof(other, on:, direction: :floor, tolerance: nil, suffixes: DEFAULT_JOIN_SUFFIXES)
53
- on = on.to_s
54
- plan = join_name_plan(other, on, suffixes)
55
- addr = self[on].locate_nearest_addr(other[on], direction: direction, tolerance: tolerance)
56
- join_by_addr(other, on, addr, plan)
80
+ # in that direction or beyond +tolerance:+ come back UNDEF. +tolerance:+
81
+ # is a number for a numeric key and a duration for a time key
82
+ # ("10 minutes", or a CATimedelta value). Same wiring as the left join,
83
+ # only the address primitive differs.
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)
57
113
  end
58
114
 
59
- # Conform every variable to an externally supplied reference key set (the
115
+ # Conform every column to an externally supplied reference key set (the
60
116
  # asymmetric sibling of +join+; pandas +reindex+). Given a +reference+ array
61
117
  # of key values, each column is gathered onto it by exact key match
62
- # (+locate_addr+): the aligned key column (or index) becomes the reference
63
- # itself, and a reference key absent from the source comes back UNDEF in
64
- # 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.
65
121
  #
66
122
  # CAFrame measures no interval and generates nothing -- the caller owns the
67
123
  # reference. This is the primitive for reindexing to a caller-built axis
@@ -71,17 +127,31 @@ class CAFrame
71
127
  # df.align("time", reftime) # reftime = a CArray of reference key values
72
128
  #
73
129
  # +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).
130
+ # or an Array; its keys are matched against the source key by value. An
131
+ # Array is read as values of the key's kind, as +is_in+ reads one: numbers
132
+ # against a numeric key, Time / Date / String instants against a time key.
76
133
  def align(key, reference)
77
- key = key.to_s
78
- ref = reference.is_a?(CArray) ? reference : reference.to_ca
134
+ key = column_key(key, "align")
79
135
  on_index = !@columns.key?(key) && @axis_name == key && @index
80
- addr = ref.locate_addr(on_index ? @index : self[key])
136
+ keys = key_column(key, "align")
137
+ # An Array reference is read as values of the key's kind, as is_in reads
138
+ # one: 1 and 2.5 against a float key are numbers, and Time or String
139
+ # values against a time key are instants.
140
+ ref = reference.is_a?(CArray) ? reference : keys.send(:promote_value_set, reference)[1]
141
+ unless ref.ndim == 1
142
+ raise ArgumentError,
143
+ "align: the reference has shape #{ref.shape.inspect}; give one key value per row of the result"
144
+ end
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)
81
150
 
82
151
  cols = {}
83
- variable_names.each { |name| cols[name] = project_rows(self[name], addr) }
152
+ column_names.each { |name| cols[name] = project_rows(self[name], addr) }
84
153
  if on_index
154
+ # key-labels: ref was given the key's attributes above
85
155
  CAFrame.new(cols, axis_name: key, index: ref)
86
156
  else
87
157
  cols[key] = ref
@@ -90,7 +160,7 @@ class CAFrame
90
160
  end
91
161
  end
92
162
 
93
- # Paste +other+'s variables beside this frame's, matched by **row position**
163
+ # Paste +other+'s columns beside this frame's, matched by **row position**
94
164
  # (memo §12-C) — a keyless column merge (the column-direction counterpart of
95
165
  # the row-stacking +concat+, and the positional counterpart of the key-aligned
96
166
  # +join+; named after the UNIX +paste+). Both frames must have the same
@@ -137,7 +207,10 @@ class CAFrame
137
207
 
138
208
  # Detect columns that appear in 2+ frames (collisions across the K inputs).
139
209
  name_count = Hash.new(0)
140
- frames.each { |f| f.variable_names.each { |name| name_count[name] += 1 } }
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
141
214
  collisions = name_count.select { |_, n| n > 1 }.keys
142
215
 
143
216
  if collisions.any?
@@ -160,7 +233,7 @@ class CAFrame
160
233
 
161
234
  cols = {}
162
235
  frames.each_with_index do |f, i|
163
- f.variable_names.each do |name|
236
+ f.column_names.each do |name|
164
237
  key = collisions.include?(name) ? "#{name}#{suffixes[i]}" : name
165
238
  if cols.key?(key)
166
239
  raise ArgumentError,
@@ -173,8 +246,159 @@ class CAFrame
173
246
  new(cols, axis_name: first.axis_name, index: first.index)
174
247
  end
175
248
 
176
- private def join_name_plan(other, on, suffixes)
177
- collisions = (variable_names & other.variable_names) - [on]
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)
308
+ end
309
+
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
178
402
  return { collisions: [], lsuf: nil, rsuf: nil } if collisions.empty?
179
403
 
180
404
  if suffixes == false
@@ -193,6 +417,7 @@ class CAFrame
193
417
  plan[:collisions].include?(name) ? "#{name}#{suffix}" : name
194
418
  end
195
419
 
420
+ # names-check: the output is checked as it is built, a column at a time
196
421
  private def put_join_column(cols, name, col)
197
422
  if cols.key?(name)
198
423
  raise ArgumentError,
@@ -202,41 +427,165 @@ class CAFrame
202
427
  cols[name] = col
203
428
  end
204
429
 
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
- private def join_by_addr(other, on, addr, plan)
430
+ private def join_by_addr(other, names, addr, plan)
210
431
  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|
213
- next if name == on
432
+ column_names.each { |name| put_join_column(cols, join_name(name, plan[:lsuf], plan), self[name]) }
433
+ other.column_names.each do |name|
434
+ next if names.include?(name)
214
435
  put_join_column(cols, join_name(name, plan[:rsuf], plan), project_rows(other[name], addr))
215
436
  end
216
437
  CAFrame.new(cols, axis_name: @axis_name, index: @index)
217
438
  end
218
439
 
219
- private def join_align(other, on, lkey, rkey, how, plan)
220
- common, a_idx, b_idx = CArray.align_addr(lkey, rkey, join: how)
221
- 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))
440
+ # The rows of a key join: li / ri are the left / right row of each result
441
+ # row, UNDEF on the side a row has no match on.
442
+ private def join_pairs(other, names, lkeys, rkeys, how, plan)
443
+ parts, lcode, rcode, k = join_codes(lkeys, rkeys)
444
+ extra = nil
445
+ if how == :right
446
+ ri, li = join_expand(rcode, lcode, k, true)
447
+ else
448
+ li, ri = join_expand(lcode, rcode, k, how != :inner)
449
+ if how == :outer
450
+ lcount = lcode.bincount(length: k)
451
+ alone = rcode.is_masked | lcount.project(rcode).strip_mask(0).eq(0)
452
+ alone = alone.strip_mask(true)
453
+ extra = CArray.int64(rcode.elements).seq[alone]
454
+ if extra.elements > 0
455
+ undef_rows = CArray.int64(extra.elements).fill(UNDEF)
456
+ li = CArray.concatenate([li, undef_rows])
457
+ ri = CArray.concatenate([ri, extra])
458
+ else
459
+ extra = nil
460
+ end
227
461
  end
228
462
  end
229
- other.variable_names.each do |name|
230
- next if name == on
231
- put_join_column(cols, join_name(name, plan[:rsuf], plan), project_rows(other[name], b_idx))
463
+ every_left = how == :left || how == :inner
464
+ identity = every_left && li.elements == nrow &&
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 }
467
+ cols = {}
468
+ column_names.each do |name|
469
+ j = names.index(name)
470
+ col = if j then joined_key(j, parts, lkeys, rkeys, rows)
471
+ elsif identity then self[name]
472
+ elsif every_left then self[name][li, *([nil] * (self[name].ndim - 1))]
473
+ else project_rows(self[name], li)
474
+ end
475
+ name = join_name(name, plan[:lsuf], plan) unless j
476
+ put_join_column(cols, name, col)
477
+ end
478
+ other.column_names.each do |name|
479
+ next if names.include?(name)
480
+ put_join_column(cols, join_name(name, plan[:rsuf], plan), project_rows(other[name], ri))
232
481
  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)
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
485
+ new_index = if @index.nil? then nil
486
+ elsif ij then joined_key(ij, parts, lkeys, rkeys, rows)
487
+ elsif identity then @index
488
+ elsif every_left then @index[li]
489
+ else project_rows(@index, li)
490
+ end
237
491
  CAFrame.new(cols, axis_name: @axis_name, index: new_index)
238
492
  end
239
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
+
535
+ # Pairs a side's rows (codes +acode+) with the other side's (+bcode+), in
536
+ # a-row order and, for one a-row, in b-row order. An a-row with no match
537
+ # comes once with no b-row when +keep_alone+. Returns [ai, bi].
538
+ private def join_expand(acode, bcode, k, keep_alone)
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
544
+ # b's rows by code, each code's rows in b-row order.
545
+ border = CArray.sort_addr(bcode, CArray.int64(nb).seq)
546
+ bstart = bcount.cumsum.int64 - bcount
547
+ matches = bcount.project(acode).strip_mask(0)
548
+ reps = keep_alone ? matches.pmax(1) : matches
549
+ total = reps.sum.to_i
550
+ return [CArray.int64(0), CArray.int64(0)] if total == 0
551
+ # Each a-row repeated reps times: mark where each run starts and count.
552
+ rows = CArray.int64(na).seq[reps.gt(0)]
553
+ first = (reps.cumsum.int64 - reps)[reps.gt(0)]
554
+ mark = CArray.int64(total).fill(0)
555
+ mark[first] = 1
556
+ run = mark.cumsum.int64 - 1
557
+ ai = rows[run]
558
+ j = CArray.int64(total).seq - first[run]
559
+ bi = CArray.int64(total).fill(UNDEF)
560
+ hit = matches.project(ai).gt(0)
561
+ if hit.any
562
+ bi[hit] = border.project(bstart.project(acode.project(ai[hit])) + j[hit])
563
+ end
564
+ [ai, bi]
565
+ end
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
+
240
589
  private def project_rows(col, addr)
241
590
  return col.project(addr) if col.ndim == 1
242
591
  trailing = col.shape[1..]