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
@@ -5,18 +5,22 @@ class CArray
5
5
  # each cell whose value appears in the set `values`.
6
6
  #
7
7
  # `values` is treated as a set, not as an operand to broadcast: it
8
- # may be any shape (or an Array / Range) and is flattened to a
8
+ # may be any shape (or an Array) and is flattened to a
9
9
  # single seen-set, so its shape need not match `self`. Only one
10
10
  # argument is accepted; to test a few immediate values pass an
11
- # Array (`a.is_in([0, -1])`).
11
+ # Array (`a.is_in([0, -1])`). A single value is a set of one, as
12
+ # `[value]` is (`a.is_in(2)` is `a.is_in([2])`).
12
13
  #
13
14
  # When `self` and `values` have different numeric data types they are
14
15
  # promoted to a common type first (the same promotion binops use,
15
16
  # {CArray.result_type}), so membership is value-correct across
16
17
  # data types (e.g. an int cell equals a float set element of the same
17
18
  # value, and a fractional set element never truncates onto an int
18
- # cell). Genuinely incompatible data types (e.g. numeric vs fixlen)
19
- # raise.
19
+ # cell). Integers of mixed sign are matched by value, as {#eq}
20
+ # compares them: `-1` in an int8 array never matches `255` in a uint8
21
+ # one, and the set operations answer such a pair in a type that holds
22
+ # both (int16 for uint8 with int8, object for uint64 with int64).
23
+ # Genuinely incompatible data types (e.g. numeric vs fixlen) raise.
20
24
  #
21
25
  # Membership is value-based and shares the distinctness of the
22
26
  # value-hash discovery family ({#unique} / {#value_counts}):
@@ -27,6 +31,15 @@ class CArray
27
31
  # (so a 5-byte array written from `"be"` is `is_in(["be"])`). A
28
32
  # `CA_FIXLEN` set given as a CArray must already be of that width.
29
33
  #
34
+ # An object array keeps Ruby's own split between its two kinds of
35
+ # equality. A lookup through a hash -- `is_in`, the set operations,
36
+ # {#locate_addr}, {#unique} and the rest of the discovery family --
37
+ # uses `eql?`, as `Array#uniq`, `Array#&` and `Set` do, so `1` and
38
+ # `1.0` are different values there. A cell-by-cell comparison --
39
+ # {#eq}, {#count}, {#search}, {#bsearch} -- uses `==`, as
40
+ # `Array#include?` does, so they are equal there. To match by value
41
+ # across Integer and Float, convert the array to a numeric type first.
42
+ #
30
43
  # Masked cells of `values` do not enter the set. Masked cells of
31
44
  # `self` stay masked in the result (membership is unknown), so
32
45
  # `is_in` propagates `self`'s mask like an element-wise comparison.
@@ -34,8 +47,10 @@ class CArray
34
47
  # For a per-fiber "does this fiber contain any of these values"
35
48
  # reduction, compose with {#any}: `a.is_in(values).any(axis: k)`.
36
49
  #
37
- # @param values [CArray, Array, Range] the set to test membership
50
+ # @param values [CArray, Array] the set to test membership
38
51
  # against. Promoted with `self` to a common data type.
52
+ # @raise [ArgumentError] for a Range, which reads as an interval rather
53
+ # than the set of its steps.
39
54
  # Between two time arrays the question is about instants, not ticks:
40
55
  # `values` is reconciled into `self`'s unit first, so a `:D` array and an
41
56
  # `:h` array match on the instants they share. The same holds for the set
@@ -43,6 +58,7 @@ class CArray
43
58
  #
44
59
  # @return [CArray] boolean CArray of the same shape as `self`.
45
60
  def is_in (values)
61
+ refuse_range_set(values, "is_in")
46
62
  a, b = promote_value_set(values)
47
63
  a.__send__(:__is_in__, b)
48
64
  end
@@ -57,13 +73,15 @@ class CArray
57
73
  # always flat, like {#unique}, because the distinct values of a
58
74
  # fiber vary in number.
59
75
  #
60
- # @param other [CArray, Array, Range] promoted with `self`.
76
+ # @param other [CArray, Array] promoted with `self` (a Range is refused, as by {#is_in}).
61
77
  # @param sort [Boolean] when true, return the values sorted
62
78
  # ascending instead of in first-appearance order.
63
79
  # @return [CArray] 1-D CArray of the common distinct values.
64
80
  def intersection (other, sort: false)
81
+ refuse_range_set(other, "intersection")
65
82
  a, b = promote_value_set(other)
66
83
  r = a.__send__(:__intersection__, b)
84
+ r = r.to_unit(unit) if !a.equal?(self) && respond_to?(:in_common_unit_with)
67
85
  sort ? r.sort : r
68
86
  end
69
87
 
@@ -72,25 +90,31 @@ class CArray
72
90
  # absent from `other`, in `self`'s first-appearance order.
73
91
  # See {#intersection} for the shared semantics and options.
74
92
  #
75
- # @param other [CArray, Array, Range] promoted with `self`.
93
+ # @param other [CArray, Array] promoted with `self` (a Range is refused, as by {#is_in}).
76
94
  # @param sort [Boolean] when true, return the values sorted ascending.
77
95
  # @return [CArray] 1-D CArray of the self-only distinct values.
78
96
  def difference (other, sort: false)
97
+ refuse_range_set(other, "difference")
79
98
  a, b = promote_value_set(other)
80
99
  r = a.__send__(:__difference__, b)
100
+ r = r.to_unit(unit) if !a.equal?(self) && respond_to?(:in_common_unit_with)
81
101
  sort ? r.sort : r
82
102
  end
83
103
 
84
104
  # @overload union(other, sort: false)
85
105
  # Returns a 1-D CArray of the distinct values appearing in either
86
106
  # `self` or `other`, in self-then-other first-appearance order.
87
- # See {#intersection} for the shared semantics and options.
107
+ # See {#intersection} for the shared semantics and options. Two time
108
+ # arrays of different units give their union in the finer unit, which
109
+ # holds the values of both ({#intersection} and {#difference} answer in
110
+ # `self`'s unit, since their values are `self`'s).
88
111
  #
89
- # @param other [CArray, Array, Range] promoted with `self`.
112
+ # @param other [CArray, Array] promoted with `self` (a Range is refused, as by {#is_in}).
90
113
  # @param sort [Boolean] when true, return the values sorted ascending
91
114
  # (a merged, ordered set — e.g. a common time axis).
92
115
  # @return [CArray] 1-D CArray of the combined distinct values.
93
116
  def union (other, sort: false)
117
+ refuse_range_set(other, "union")
94
118
  a, b = promote_value_set(other)
95
119
  r = a.__send__(:__union__, b)
96
120
  sort ? r.sort : r
@@ -98,6 +122,16 @@ class CArray
98
122
 
99
123
  private
100
124
 
125
+ # A Range reads as an interval in Ruby, which a set of its steps is not
126
+ # (2.5 is not in [2, 3, 4]), so the set-valued verbs refuse one rather
127
+ # than answer for the steps.
128
+ def refuse_range_set (values, verb)
129
+ return unless values.is_a?(Range)
130
+ raise ArgumentError,
131
+ "#{verb}: a Range is not taken as a set; pass its values as an Array " \
132
+ "((#{values.inspect}).to_a), or test an interval with is_between(#{values.inspect})"
133
+ end
134
+
101
135
  # Reconcile a set-valued argument with self to a common data_type via
102
136
  # CArray.result_type (the single-source promotion rule the eager binop and
103
137
  # lazy CABinOp share), returning [self', set']. Only data types are reconciled,
@@ -113,13 +147,69 @@ class CArray
113
147
  # operand (Numo, a MemoryView producer, ...) comes in through wrap_readonly,
114
148
  # the canonical type-coercion entry, so its format's data type drives the promote.
115
149
  def promote_value_set (values)
150
+ # Two time arrays of different units meet in the finer one.
151
+ if values.is_a?(CArray) && values.class == self.class &&
152
+ respond_to?(:in_common_unit_with)
153
+ return in_common_unit_with(values)
154
+ end
155
+ values = values_for_lookup(values) if values.is_a?(CArray)
156
+ # Two fixed-width string arrays of different widths meet at the wider.
157
+ if values.is_a?(CArray) && data_type == CA_FIXLEN && values.data_type == CA_FIXLEN
158
+ return fixlen_widened_with(values)
159
+ end
160
+ # A Face that reconciles operands (CATime) reads a bare Array as values
161
+ # of its own kind -- instants written as Time / String / elements -- and
162
+ # meets them as a comparison does (comparable_pair).
163
+ if values.is_a?(Array) && face? && respond_to?(:comparable_pair)
164
+ return comparable_pair(values)
165
+ end
166
+ # A single value (a Time, an element, a bare number) is a set of one,
167
+ # read the same way; a bare number is refused like any other operand
168
+ # that is not of its kind.
169
+ if !values.is_a?(CArray) && !values.is_a?(Range) && face? &&
170
+ respond_to?(:comparable_pair)
171
+ return comparable_pair([values])
172
+ end
173
+ # ...and so does an array of values that are not of its kind (an object
174
+ # column of Time values, a string column).
175
+ if values.is_a?(CArray) && respond_to?(:comparable_pair) &&
176
+ !values.respond_to?(:comparable_pair)
177
+ return comparable_pair(values)
178
+ end
179
+ # The same from the other side: values of a time array looked for in an
180
+ # array that is not one.
181
+ if values.is_a?(CArray) && values.respond_to?(:comparable_pair) &&
182
+ !respond_to?(:comparable_pair)
183
+ set, own = values.comparable_pair(self)
184
+ return [own, set]
185
+ end
186
+ # A Face whose storage is not of its surface type. When its cells are
187
+ # its values, the set meets it as it would meet an array of the storage
188
+ # type, and the Face stays while that type does not change. Any other
189
+ # Face leaves the set to the lookup, which brings it into the Face's
190
+ # space or refuses it.
191
+ if face? && (storage = face_storage).data_type != data_type
192
+ unless comparable_storage?
193
+ set = values.is_a?(Range) ? values.to_a : values
194
+ return [self, set.is_a?(CArray) ? set : CArray.wrap_readonly(set)]
195
+ end
196
+ s, set = storage.send(:promote_value_set, values)
197
+ return [s.equal?(storage) ? self : s, set]
198
+ end
116
199
  case values
117
200
  when Array then return promote_elements(values)
118
201
  when Range then return promote_elements(values.to_a)
119
202
  end
203
+ # A single value is a set of one, as [value] is. wrap_readonly would make
204
+ # it an object array, which takes self into the object lane, where 2.0 is
205
+ # not eql? 2.
206
+ if !values.is_a?(CArray) && !values.respond_to?(:to_ca) &&
207
+ !CArray.memory_view_available?(values)
208
+ return promote_elements([values])
209
+ end
120
210
  set = values.is_a?(CArray) ? values : CArray.wrap_readonly(values)
121
211
  return [self, set] if set.data_type == data_type # common fast path
122
- t = CArray.result_type(self, set)
212
+ t = query_match_type(set)
123
213
  [coerce_self(t), set.to_type(t)]
124
214
  end
125
215
 
@@ -136,15 +226,55 @@ class CArray
136
226
  # set for not matching self. Same rule the scalar operand of a
137
227
  # comparison follows: a String standing in for a cell is padded to the
138
228
  # cell's width.
229
+ # coerce: a fixlen or object set holds values of self's kind (see above)
139
230
  [self, elems.to_ca.to_type(data_type, bytes: bytes)]
140
231
  elsif data_type == CA_OBJECT
232
+ # coerce: an object set holds values of self's kind
141
233
  [self, elems.to_ca.to_type(data_type)]
234
+ elsif elems.any? { |v| v.is_a?(String) || v.is_a?(Symbol) }
235
+ # A String is a value, which result_type would read as the name of a
236
+ # data type; a numeric self meets it in the object lane.
237
+ [to_type(CA_OBJECT), elems.to_ca.to_type(CA_OBJECT)]
142
238
  else
143
239
  t = CArray.result_type(self, *elems)
144
240
  [coerce_self(t), elems.to_ca.to_type(t)]
145
241
  end
146
242
  end
147
243
 
244
+ # +other+ as a lookup against self reads it. A string array whose cells
245
+ # hold ranges into a pool of its own (CAConstString) is read by its
246
+ # strings, and a categorical by its labels, since their storage is not
247
+ # their values; anything else, or the same kind of array as self, is
248
+ # taken as it is. A record is left to be refused.
249
+ def values_for_lookup (other)
250
+ return other if other.send(:orderable_storage?)
251
+ return other if face? && other.class == self.class
252
+ if other.respond_to?(:to_string)
253
+ other.to_string
254
+ elsif other.is_a?(CACategorical)
255
+ other.to_type(:object)
256
+ else
257
+ other
258
+ end
259
+ end
260
+
261
+ # [self, other] at one cell width when both are fixed-width string arrays
262
+ # (CAFixlenString) of different widths: the narrower is padded with NUL to
263
+ # the wider, as concatenate joins them, so a string matches whatever width
264
+ # it is held at. Raw fixlen cells of different widths are values of
265
+ # different lengths, not padded strings, and are left as they are (the
266
+ # lookup refuses them, naming the widths).
267
+ def fixlen_widened_with (other)
268
+ return [self, other] if bytes == other.bytes
269
+ return [self, other] unless is_a?(CAFixlenString) && other.is_a?(CAFixlenString)
270
+ w = [bytes, other.bytes].max
271
+ [widen_fixlen_string(self, w), widen_fixlen_string(other, w)]
272
+ end
273
+
274
+ def widen_fixlen_string (x, w)
275
+ x.bytes == w ? x : x.to_fixlen_string(bytes: w)
276
+ end
277
+
148
278
  def coerce_self (t)
149
279
  CArray.result_type(self) == t ? self : to_type(t)
150
280
  end
@@ -46,12 +46,7 @@ class CArray
46
46
  return sep.nil? ? to_a.join : to_a.join(sep)
47
47
  end
48
48
 
49
- ax = Integer(axis)
50
- ax += ndim if ax < 0
51
- if ax < 0 || ax >= ndim
52
- raise ArgumentError,
53
- "axis #{axis.inspect} out of range for ndim=#{ndim}"
54
- end
49
+ ax = normalize_axis(axis, "join")
55
50
 
56
51
  sep_str = sep || ""
57
52
 
@@ -66,7 +61,7 @@ class CArray
66
61
  t = transpose(*order)
67
62
  end
68
63
  inner = t.shape[-1]
69
- outer_n = t.elements / inner # 1 when ndim == 1
64
+ outer_n = t.shape[0...-1].inject(1, :*) # 1 when ndim == 1
70
65
  flat = t.reshape(outer_n, inner)
71
66
 
72
67
  strings = Array.new(outer_n) { |i| flat[i, nil].to_a.join(sep_str) }
@@ -0,0 +1,49 @@
1
+ class CArray
2
+
3
+ # @overload lag(n = 1, axis: nil)
4
+ # Returns the value `n` steps earlier along `axis`, cell by cell: cell
5
+ # `i` of the result is cell `i - n` of `self`. A cell with no value `n`
6
+ # steps back is UNDEF, so a difference against it is UNDEF rather than
7
+ # a difference against a stand-in value:
8
+ #
9
+ # ```
10
+ # x = CA_FLOAT64([1.0, 4.0, 9.0])
11
+ # x.lag # => [UNDEF, 1.0, 4.0]
12
+ # x - x.lag # => [UNDEF, 3.0, 5.0]
13
+ # x.lag(-1) # => [4.0, 9.0, UNDEF] (the value one step later)
14
+ # ```
15
+ #
16
+ # The cell `n` steps back is taken by position: when it is masked, the
17
+ # result is UNDEF there too. It is `shift` with `UNDEF` as the fill
18
+ # value, and returns the same kind of view: it shares storage with
19
+ # `self`. Use `copy` for an independent array.
20
+ #
21
+ # The steps run along one axis. A 1-D array has only one, so `axis:`
22
+ # may be left out; an array of more dimensions must name it, since
23
+ # stepping back through the cells in flatten order would carry a value
24
+ # across from the end of one row to the start of the next.
25
+ #
26
+ # @param n [Integer] steps back; a negative `n` looks ahead.
27
+ # @param axis [Integer, nil] the axis the steps run along.
28
+ # @return [CArray] a view, shaped like `self`.
29
+ # @raise [TypeError] when `n` or `axis` is not an Integer.
30
+ # @raise [ArgumentError] when `axis` is out of range, or left out for an
31
+ # array of more than one dimension.
32
+ def lag (n = 1, axis: nil)
33
+ unless n.is_a?(Integer)
34
+ raise TypeError, "lag: n must be an Integer (got #{n.class})"
35
+ end
36
+ if axis.nil?
37
+ unless ndim == 1
38
+ raise ArgumentError,
39
+ "lag: name the axis to step along (axis:) for an array of " \
40
+ "#{ndim} dimensions"
41
+ end
42
+ axis = 0
43
+ end
44
+ steps = Array.new(ndim, 0)
45
+ steps[normalize_axis(axis, "lag")] = n
46
+ shift(*steps, fill_value: UNDEF)
47
+ end
48
+
49
+ end
@@ -1,3 +1,6 @@
1
+ require "carray/methods/is_in" # fixlen_widened_with, values_for_lookup
2
+ require "carray/distance_limit"
3
+
1
4
  class CArray
2
5
 
3
6
  # @!group Sorting and searching
@@ -27,29 +30,63 @@ class CArray
27
30
  # `self` and `ref` are compared at their common type, negotiated by
28
31
  # `CArray.result_type` — a fractional query against an integer `ref`
29
32
  # is compared at the promoted type rather than truncated, so `1.5` no
30
- # longer matches `1`. Cross-family input (numeric against fixlen)
31
- # raises.
33
+ # longer matches `1`, and integers of mixed sign are matched by value
34
+ # (`-1` never matches `255`). Cross-family input (numeric against
35
+ # fixlen) raises.
36
+ #
37
+ # A time key (`CATime`) reads values that are not time arrays -- Time,
38
+ # Date or String objects, in an Array, an `:object` array or a string
39
+ # array -- as instants, on either side of the lookup, as {#is_in} does.
32
40
  #
33
41
  # Typical use is time-axis lookup: compute the address once against a
34
42
  # reference axis, then reuse it to gather from many `ref`-shaped
35
43
  # variables without repeating the lookup.
36
44
  #
37
45
  # @param ref [CArray, Array, Range] reference values to match against;
38
- # any shape (used as flat). An Array or Range is coerced with `to_ca`
39
- # and so lands in `:object`, where numeric matching follows Ruby
40
- # `eql?` (`2.0` does not match `2`); pass a CArray to match in a
41
- # numeric lane.
46
+ # any shape (used as flat). An Array or Range is the list of its
47
+ # values, met at a common type with `self` as {#is_in} meets one, so
48
+ # `2` finds `2.0`.
42
49
  # @return [CArray] `:int64` flat addresses into `ref`, same shape as
43
50
  # `self`; unmatched cells are masked.
44
- # @raise [RuntimeError] when `self` and `ref` have no common data type.
51
+ # @raise [CArray::DataTypeError] when `self` and `ref` have no common data type.
45
52
  def locate_addr (ref)
53
+ # A time key (any Face that reconciles operands) against values that do
54
+ # not -- Time, Date or String objects, a string column -- reads them as
55
+ # values of its kind, whichever side they are on.
56
+ if ref.is_a?(CArray) && ref.class == self.class &&
57
+ respond_to?(:in_common_unit_with) && ref.unit != unit
58
+ a, b = in_common_unit_with(ref)
59
+ return a.locate_addr(b)
60
+ end
61
+ # They meet as a comparison does (comparable_pair).
62
+ if respond_to?(:comparable_pair) && !ref.respond_to?(:comparable_pair)
63
+ key, r = comparable_pair(ref)
64
+ return key.locate_addr(r)
65
+ elsif ref.respond_to?(:comparable_pair) && !respond_to?(:comparable_pair)
66
+ r, key = ref.comparable_pair(self)
67
+ return key.locate_addr(r)
68
+ end
69
+ unless ref.is_a?(CArray) || ref.respond_to?(:to_ca)
70
+ raise TypeError, "locate_addr: the reference is an array of values (got #{ref.class})"
71
+ end
72
+ # An Array or Range of values meets self as is_in's does: the common type
73
+ # comes from self and the elements, so 2 finds 2.0 in a float array. The
74
+ # conversion keeps the order, so the addresses stay positions in ref.
75
+ if ref.is_a?(Array) || ref.is_a?(Range)
76
+ q, r = promote_elements(ref.is_a?(Range) ? ref.to_a : ref)
77
+ return q.send(:__locate_addr__, r)
78
+ end
46
79
  ref = ref.to_ca unless ref.is_a?(CArray)
47
- # Put self and ref in a common lane via the single-source promotion rule
48
- # (CArray.result_type), so a fractional query against an int ref is compared
49
- # at the promoted type instead of truncating (1.5 no longer matches 1).
50
- # to_type is elementwise and order-preserving, so the addresses stay valid
51
- # indices into ref. result_type raises for cross-family input.
52
- t = CArray.result_type(self, ref)
80
+ ref = values_for_lookup(ref)
81
+ if data_type == CA_FIXLEN && ref.data_type == CA_FIXLEN && bytes != ref.bytes
82
+ a, b = fixlen_widened_with(ref)
83
+ return a.locate_addr(b) unless a.equal?(self) && b.equal?(ref)
84
+ end
85
+ # Put self and ref in the type they match in (query_match_type), so a
86
+ # fractional query against an int ref is compared as a float (1.5 does
87
+ # not match 1) and a pair of mixed sign by value. to_type is elementwise
88
+ # and order-preserving, so the addresses stay valid indices into ref.
89
+ t = ref.send(:query_match_type, self)
53
90
  q = (data_type == t) ? self : to_type(t)
54
91
  r = (ref.data_type == t) ? ref : ref.to_type(t)
55
92
  q.send(:__locate_addr__, r)
@@ -57,16 +94,18 @@ class CArray
57
94
 
58
95
  # @overload locate_nearest_addr(ref, direction: :round, tolerance: nil)
59
96
  # Returns, for each element of `self`, the flat address into `ref` of
60
- # the nearest reference value. Continuous sibling of {#locate_addr};
61
- # uses `linear_section` + rounding for non-exact matching against a
62
- # sorted `ref`.
97
+ # the nearest reference value. Continuous sibling of {#locate_addr}:
98
+ # each cell is placed among the sorted reference values by comparing
99
+ # it with them, so an integer key is exact at any size and an infinite
100
+ # one keeps its place. `:round` takes the nearer neighbour, the upper
101
+ # one on a tie.
63
102
  #
64
- # Out-of-range cells of `self` (outside `ref`'s span) mask through the
65
- # pipeline: `linear_section` returns NaN, `mask_invalid` propagates
66
- # that as `UNDEF`, rounding and the int64 cast carry the mask, and
67
- # `project` scatters it into the final positions. `mask_invalid` runs
68
- # before rounding because `CArray#round` maps NaN to 0 and would
69
- # otherwise silently match `ref[0]`.
103
+ # Outside `ref`'s span the direction still decides: `:floor` matches
104
+ # a cell after the last value to that last value, `:ceil` matches a
105
+ # cell before the first value to that first value, and `:round`
106
+ # matches either to the nearer end. The other side has no match
107
+ # (`:floor` before the first value, `:ceil` after the last) and is
108
+ # masked. Use `tolerance:` to refuse matches that are too far away.
70
109
  #
71
110
  # `tolerance:` (default `nil`) sets a maximum accepted absolute
72
111
  # distance between `self[i]` and its matched `ref` value. When
@@ -76,50 +115,148 @@ class CArray
76
115
  #
77
116
  # `ref` need not be given in ascending order: it is sorted internally
78
117
  # and the returned addresses are mapped back to positions in `ref` as
79
- # passed.
118
+ # passed. A masked or NaN `ref` value matches nothing. Two time keys
119
+ # of different units are compared in the finer of the two.
120
+ # With one value present in `ref`, that value is the match for
121
+ # every cell the direction allows (`:floor` at or after it, `:ceil`
122
+ # at or before it, `:round` all). With none, every cell is masked;
123
+ # an empty `self` gives an empty result. A masked or NaN cell of
124
+ # `self` is always masked.
80
125
  #
81
126
  # @param ref [CArray] 1-D reference grid to match against.
82
127
  # @param direction [Symbol] `:round`, `:floor`, or `:ceil` — rounding
83
128
  # applied to the fractional position.
84
- # @param tolerance [Numeric, nil] maximum accepted `|self - ref|`
85
- # distance; cells beyond this are masked. `nil` disables the check.
129
+ # @param tolerance [Numeric, String, CATimedelta::Element, nil] maximum
130
+ # accepted `|self - ref|` distance; cells beyond this are masked.
131
+ # For a time key it is a duration: a String such as `"10 minutes"` or
132
+ # a CATimedelta value (a bare number is refused, since its length
133
+ # would depend on the key's unit). `nil` disables the check.
86
134
  # @return [CArray] `:int64` flat addresses into `ref`, same shape as
87
- # `self`; out-of-range and beyond-tolerance cells are masked.
135
+ # `self`; cells with no match in the direction and beyond-tolerance
136
+ # cells are masked.
88
137
  # @raise [ArgumentError] when `direction` is not one of the accepted
89
- # symbols.
138
+ # symbols, or `tolerance` is not a duration for a time key.
90
139
  def locate_nearest_addr (ref, direction: :round, tolerance: nil)
91
140
  unless [:round, :floor, :ceil].include?(direction)
92
141
  raise ArgumentError,
93
142
  "locate_nearest_addr: direction must be :round / :floor / " \
94
143
  ":ceil (got #{direction.inspect})"
95
144
  end
145
+ [self, ref].each { |key| nearest_refuse_bytes(key) }
146
+ limit = tolerance && DistanceLimit.read(tolerance, self, "tolerance:")
147
+ return CArray.int64(*shape) if elements == 0
148
+ # A masked reference value matches nothing, as in locate_addr: sorting
149
+ # puts the masked ones last, and only the others are searched. A NaN
150
+ # one has no place among the others either; it sorts after them.
96
151
  ri = ref.sort_addr
97
- rs = ref[ri]
98
- sec = rs.linear_section(self)
99
- unless sec.is_a?(CArray)
100
- # A single-element (scalar-like) self makes linear_section collapse to
101
- # its scalar-query path, which returns a bare Float (or nil when out of
102
- # range) instead of a CArray. Rebuild a self-shaped float64 CArray so
103
- # the mask_invalid -> direction -> project pipeline stays array-valued
104
- # and the returned addr array matches self's shape.
105
- fill = CArray.float64(*shape)
106
- fill[] = sec.nil? ? UNDEF : sec
107
- sec = fill
152
+ ri = ri[0...ref.count_not_masked] if ref.has_mask?
153
+ if ref.float? && ri.elements > 0
154
+ ri = ri[0...(ri.elements - ref[ri].is_nan.count(true))]
108
155
  end
109
- masked = sec.mask_invalid
156
+ if ri.elements == 0
157
+ out = CArray.int64(*shape)
158
+ out[] = UNDEF
159
+ return out
160
+ end
161
+ rs = ref[ri]
162
+ n = rs.elements
163
+ rk, qk = nearest_keys(rs, self)
164
+ # The position of each cell among the sorted references is counted, not
165
+ # interpolated: :floor takes the last reference at or below it, :ceil the
166
+ # first at or above it.
167
+ # Counting keeps an integer key exact past 2**53 and an infinite one in
168
+ # its place, where an interpolated position would not.
110
169
  si = case direction
111
- when :round then masked.round
112
- when :floor then masked.floor
113
- when :ceil then masked.ceil
114
- end.int64
170
+ when :floor then nearest_count_below(rk, qk, true) - 1
171
+ when :ceil then nearest_count_below(rk, qk, false)
172
+ when :round then nearest_round(rk, qk, nearest_count_below(rk, qk, false))
173
+ end
174
+ si[si.lt(0) | si.ge(n)] = UNDEF
175
+ si[qk.is_nan.strip_mask(false)] = UNDEF if qk.float?
176
+ si = si.reshape(*shape)
177
+ si[is_masked] = UNDEF if has_mask?
115
178
  idx = ri.project(si)
116
- if tolerance
179
+ if limit
117
180
  dist = (ref.project(idx) - self).abs
118
- idx[dist > tolerance] = UNDEF
181
+ idx[dist > limit] = UNDEF
119
182
  end
120
183
  idx
121
184
  end
122
185
 
186
+ # A key held as fixed-length bytes (raw fixlen, or a fixlen string) has
187
+ # no nearest: the search counts keys as numbers. A Face whose storage is
188
+ # a number (a time) is searched by that number.
189
+ # +subject+ and +remedy+ let a caller (CAFrame#join_asof) say it in its
190
+ # own words.
191
+ private def nearest_refuse_bytes(key, subject = nil, remedy = "match exact values with locate_addr")
192
+ # A Face whose cells encode the value (a const string, a categorical)
193
+ # is refused later by its own reconcile, which says why.
194
+ return unless key.is_a?(CArray) && key.__send__(:orderable_storage?)
195
+ storage = key.face? ? key.__send__(:face_storage) : key
196
+ return unless storage.data_type == CA_FIXLEN
197
+ subject ||= "locate_nearest_addr: a #{CArray::Inspector.type_label(key)} key"
198
+ raise CArray::DataTypeError,
199
+ "#{subject} is held as fixed-length bytes, which have no nearest; #{remedy}"
200
+ end
201
+
202
+ # The keys locate_nearest_addr orders by: the storage of the sorted
203
+ # reference and of the query, in one numeric type. A Face key is first
204
+ # brought into the reference's space (two time keys of different units
205
+ # into the finer of the two, which holds both exactly); plain keys meet at
206
+ # the type CArray.result_type gives, an integer pair of mixed sign at one
207
+ # that holds both.
208
+ private def nearest_keys(rs, q)
209
+ if rs.face? || q.face?
210
+ if rs.respond_to?(:to_unit) && q.class == rs.class && q.unit != rs.unit
211
+ u = CATimeUnitAlgebra.finer(rs.unit, q.unit)
212
+ rs = rs.to_unit(u)
213
+ q = q.to_unit(u)
214
+ end
215
+ q = rs.__send__(:reconcile_operand, q, "locate_nearest_addr")
216
+ rs = rs.__send__(:face_storage)
217
+ end
218
+ t = rs.send(:query_match_type, q)
219
+ rk = (rs.data_type == t) ? rs : rs.to_type(t)
220
+ qk = (q.data_type == t) ? q : q.to_type(t)
221
+ [rk, qk.flatten]
222
+ end
223
+
224
+ # For each query key, how many of the sorted reference keys are below it
225
+ # (inclusive: at or below). A numeric key is binary-searched in C; an
226
+ # object key is sorted together with the references, a tie putting the
227
+ # reference first when it counts, and counted against the references met
228
+ # before it.
229
+ private def nearest_count_below(rk, qk, inclusive)
230
+ return rk.send(:__count_below__, qk, inclusive) unless rk.data_type == CA_OBJECT
231
+ n, m = rk.elements, qk.elements
232
+ keys = CArray.concatenate([rk, qk])
233
+ tag = CArray.int8(n + m)
234
+ tag[0...n] = inclusive ? 0 : 1
235
+ tag[n...n+m] = inclusive ? 1 : 0
236
+ perm = CArray.sort_addr(keys, tag)
237
+ seen = perm.lt(n).cumsum.int64
238
+ query = perm.ge(n)
239
+ count = CArray.int64(m)
240
+ count[perm[query] - n] = seen[query]
241
+ count
242
+ end
243
+
244
+ # :round -- an exact match takes its first reference; otherwise the
245
+ # nearer of the two neighbours, the upper one on a tie. +lt+ counts the
246
+ # references below each key, so the upper neighbour is at lt and an exact
247
+ # match is the reference there.
248
+ private def nearest_round(rk, qk, lt)
249
+ lo = rk.project(lt - 1)
250
+ hi = rk.project(lt)
251
+ upper = (hi - qk).le(qk - lo).strip_mask(false)
252
+ upper[lo.is_masked] = true if lo.has_mask?
253
+ upper[hi.is_masked] = false if hi.has_mask?
254
+ upper[hi.eq(qk).strip_mask(false)] = true
255
+ si = lt - 1
256
+ si[upper] = lt[upper]
257
+ si
258
+ end
259
+
123
260
  # @!endgroup
124
261
 
125
262
  end
@@ -65,12 +65,25 @@ class CArray
65
65
  # Only meaningful for discrete / binned data: raw float is almost all
66
66
  # unique, so every value is modal and the per-axis Array grows to the
67
67
  # fiber length. Bin first. Same NaN / mask semantics as {#is_mode}.
68
+ # A complex array raises {CArray::DataTypeError}, since the modes
69
+ # are listed in ascending order; {#is_mode} marks its modal cells.
68
70
  #
69
71
  # @param axis [Integer, nil] axis to take the mode along; `nil` uses
70
72
  # the whole array.
71
73
  # @return [CArray, Array<CArray>] 1-D CArray (flat) or an Array of
72
74
  # reduced CArrays, one per mode rank (per-axis).
73
75
  def mode (axis: nil)
76
+ # An error from the methods mode is made of (sort, ...) names mode.
77
+ CArray.__name_errors_after_method__(:mode) { mode_answered(axis) }
78
+ end
79
+
80
+ private def mode_answered (axis)
81
+ if complex?
82
+ raise CArray::DataTypeError,
83
+ "mode: the modes are listed in ascending order, and a " \
84
+ "#{data_type_name} value has no order (complex numbers are not " \
85
+ "ordered); is_mode marks the cells that hold a mode"
86
+ end
74
87
  return __mode_flat if axis.nil?
75
88
  k = normalize_axis(axis, "mode")
76
89
 
@@ -120,7 +133,11 @@ class CArray
120
133
  def __mode_flat
121
134
  values, counts = value_counts
122
135
  return values if values.elements == 0
123
- values[counts.eq(counts.max)].sort
136
+ modes = values[counts.eq(counts.max)]
137
+ # Records are sorted only by the members their struct declares in
138
+ # order_by:; without one the modes stay in first-appearance order.
139
+ return modes if is_a?(CARecord) && data_class.order_by.nil?
140
+ modes.sort
124
141
  end
125
142
 
126
143
  end