carray 3.0.2 → 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 (218) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +304 -0
  3. data/README.md +3 -4
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_dispatch.c +225 -0
  6. data/ext/ca_axis_group.c +177 -99
  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 +66 -71
  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 +663 -214
  14. data/ext/ca_kernel_iterator.h +231 -167
  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 +132 -22
  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 +1 -0
  23. data/ext/ca_obj_byte_swap.c +40 -40
  24. data/ext/ca_obj_const_string.c +96 -25
  25. data/ext/ca_obj_face.c +406 -64
  26. data/ext/ca_obj_face.h +92 -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 +21 -21
  31. data/ext/ca_obj_grid.c +181 -124
  32. data/ext/ca_obj_meld.c +189 -107
  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 +543 -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 +47 -49
  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 -118
  46. data/ext/ca_obj_stride.c +195 -34
  47. data/ext/ca_obj_string.c +17 -18
  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 +149 -27
  54. data/ext/ca_op_ipower.c +24 -11
  55. data/ext/ca_op_powi.h +57 -30
  56. data/ext/ca_pow5_table.h +656 -0
  57. data/ext/ca_sort_kernels.h +70 -0
  58. data/ext/ca_sweep_engine.c +102 -79
  59. data/ext/ca_sweep_engine.h +7 -20
  60. data/ext/ca_triop_dispatch.c +4 -1
  61. data/ext/ca_triop_dispatch.h +5 -2
  62. data/ext/caframe_csv_reader.c +463 -0
  63. data/ext/carray.h +70 -27
  64. data/ext/carray_access.c +455 -211
  65. data/ext/carray_address_basis.c +177 -36
  66. data/ext/carray_attribute.c +45 -3
  67. data/ext/carray_bincount.c +32 -2
  68. data/ext/carray_broadcast.c +46 -14
  69. data/ext/carray_call_cfunc.c +0 -28
  70. data/ext/carray_cast.c +1444 -209
  71. data/ext/carray_cast_func.rb +34 -21
  72. data/ext/carray_conversion.c +197 -73
  73. data/ext/carray_copy.c +32 -11
  74. data/ext/carray_core.c +514 -122
  75. data/ext/carray_count.c +140 -87
  76. data/ext/carray_element.c +18 -27
  77. data/ext/carray_factorize.c +569 -277
  78. data/ext/carray_generate.c +76 -40
  79. data/ext/carray_histogram.c +152 -94
  80. data/ext/carray_hold.c +2 -1
  81. data/ext/carray_index_classifier.c +63 -96
  82. data/ext/carray_internal.h +385 -3
  83. data/ext/carray_lazy.c +834 -79
  84. data/ext/carray_loop.c +81 -65
  85. data/ext/carray_mask.c +682 -316
  86. data/ext/carray_math_kernel.h +1 -0
  87. data/ext/carray_median_percentile.c +221 -76
  88. data/ext/carray_memory_view.c +254 -248
  89. data/ext/carray_operator.c +1089 -359
  90. data/ext/carray_order.c +133 -69
  91. data/ext/carray_partition.c +102 -56
  92. data/ext/carray_random.c +273 -239
  93. data/ext/carray_scatter.c +308 -65
  94. data/ext/carray_segment.c +257 -0
  95. data/ext/carray_slab.c +170 -70
  96. data/ext/carray_sort.c +215 -218
  97. data/ext/carray_sort_kernel.c +8 -1
  98. data/ext/carray_struct.c +99 -21
  99. data/ext/carray_test.c +167 -64
  100. data/ext/carray_time_text.c +1182 -0
  101. data/ext/carray_utils.c +363 -98
  102. data/ext/extconf.rb +27 -2
  103. data/ext/mk_call_cfunc.rb +0 -2
  104. data/ext/mk_pow5_table.rb +35 -0
  105. data/ext/mkkernel.rb +1076 -846
  106. data/ext/ruby_carray.c +7 -1
  107. data/ext/version.h +4 -4
  108. data/ext/version.rb +4 -2
  109. data/lib/carray/attribute.rb +39 -0
  110. data/lib/carray/autoload_carray.rb +19 -0
  111. data/lib/carray/autoload_method_extension.rb +6 -0
  112. data/lib/carray/axis_group.rb +174 -26
  113. data/lib/carray/basics.rb +91 -25
  114. data/lib/carray/bincount_nd.rb +6 -0
  115. data/lib/carray/block_iterator.rb +118 -48
  116. data/lib/carray/boolean_reduce.rb +21 -29
  117. data/lib/carray/categorical.rb +94 -26
  118. data/lib/carray/categorical_iterator.rb +333 -826
  119. data/lib/carray/complex.rb +36 -29
  120. data/lib/carray/conditional.rb +94 -7
  121. data/lib/carray/const_string.rb +46 -0
  122. data/lib/carray/frame/concat.rb +157 -6
  123. data/lib/carray/frame/convert.rb +13 -7
  124. data/lib/carray/frame/csv_parser.rb +589 -82
  125. data/lib/carray/frame/describe.rb +79 -0
  126. data/lib/carray/frame/frame.rb +124 -25
  127. data/lib/carray/frame/group.rb +189 -63
  128. data/lib/carray/frame/io.rb +412 -72
  129. data/lib/carray/frame/join.rb +134 -46
  130. data/lib/carray/frame/records.rb +37 -16
  131. data/lib/carray/frame/reshape.rb +448 -0
  132. data/lib/carray/frame/sort.rb +26 -2
  133. data/lib/carray/frame/verbs.rb +715 -48
  134. data/lib/carray/frame.rb +2 -0
  135. data/lib/carray/fuse_source.rb +111 -2
  136. data/lib/carray/fusion.rb +229 -38
  137. data/lib/carray/inspect.rb +29 -11
  138. data/lib/carray/iterator.rb +14 -2
  139. data/lib/carray/lazy.rb +54 -30
  140. data/lib/carray/mask_gap_fill.rb +66 -52
  141. data/lib/carray/math.rb +32 -19
  142. data/lib/carray/meld_reduce.rb +40 -7
  143. data/lib/carray/methods/align_addr.rb +8 -4
  144. data/lib/carray/methods/bincount.rb +2 -2
  145. data/lib/carray/methods/bit_string.rb +14 -3
  146. data/lib/carray/methods/composition.rb +16 -10
  147. data/lib/carray/methods/factorize.rb +2 -2
  148. data/lib/carray/methods/gather_nd.rb +13 -2
  149. data/lib/carray/methods/insert_block.rb +9 -14
  150. data/lib/carray/methods/is_in.rb +117 -4
  151. data/lib/carray/methods/join.rb +1 -1
  152. data/lib/carray/methods/locate_addr.rb +177 -36
  153. data/lib/carray/methods/mode.rb +18 -1
  154. data/lib/carray/methods/repeat.rb +1 -20
  155. data/lib/carray/methods/resize.rb +21 -23
  156. data/lib/carray/methods/snap.rb +34 -12
  157. data/lib/carray/methods/unique.rb +11 -2
  158. data/lib/carray/record_order.rb +208 -0
  159. data/lib/carray/runtime.rb +14 -11
  160. data/lib/carray/segment_iterator.rb +990 -0
  161. data/lib/carray/serialize.rb +197 -27
  162. data/lib/carray/slab_iterator.rb +51 -27
  163. data/lib/carray/stack.rb +10 -4
  164. data/lib/carray/string_operation_extension.rb +4 -3
  165. data/lib/carray/struct.rb +142 -51
  166. data/lib/carray/struct_builder.rb +110 -14
  167. data/lib/carray/table.rb +2 -2
  168. data/lib/carray/time.rb +517 -103
  169. data/lib/carray/window_iterator.rb +127 -72
  170. data/lib/carray.rb +2 -2
  171. data/yard-stubs/ca_obj_array.rb +4 -4
  172. data/yard-stubs/ca_obj_bitarray.rb +7 -0
  173. data/yard-stubs/ca_obj_bitfield.rb +23 -12
  174. data/yard-stubs/ca_obj_fake.rb +1 -1
  175. data/yard-stubs/ca_obj_field.rb +1 -1
  176. data/yard-stubs/ca_obj_refer.rb +3 -3
  177. data/yard-stubs/ca_obj_roll.rb +0 -1
  178. data/yard-stubs/ca_obj_stride.rb +10 -7
  179. data/yard-stubs/ca_obj_transpose.rb +1 -1
  180. data/yard-stubs/ca_obj_window.rb +1 -2
  181. data/yard-stubs/carray_access.rb +5 -4
  182. data/yard-stubs/carray_broadcast.rb +1 -1
  183. data/yard-stubs/carray_cast.rb +78 -43
  184. data/yard-stubs/carray_conversion.rb +13 -1
  185. data/yard-stubs/carray_copy.rb +8 -3
  186. data/yard-stubs/carray_core.rb +3 -3
  187. data/yard-stubs/carray_count.rb +9 -0
  188. data/yard-stubs/carray_lazy.rb +5 -1
  189. data/yard-stubs/carray_loop.rb +4 -1
  190. data/yard-stubs/carray_mask.rb +34 -10
  191. data/yard-stubs/carray_math.rb +25 -13
  192. data/yard-stubs/carray_median_percentile.rb +10 -6
  193. data/yard-stubs/carray_order.rb +34 -13
  194. data/yard-stubs/carray_scatter.rb +10 -3
  195. data/yard-stubs/carray_segment.rb +72 -0
  196. data/yard-stubs/carray_slab.rb +2 -1
  197. data/yard-stubs/carray_sort.rb +43 -3
  198. data/yard-stubs/carray_test.rb +10 -0
  199. data/yard-stubs/carray_utils.rb +0 -26
  200. metadata +11 -19
  201. data/ext/ca_for_each_element.h +0 -239
  202. data/ext/carray_build_flags.h +0 -3
  203. data/ext/carray_config.h +0 -28
  204. data/ext/carray_kernels_bincmp.c +0 -4446
  205. data/ext/carray_kernels_binop.c +0 -11001
  206. data/ext/carray_kernels_init.c +0 -1131
  207. data/ext/carray_kernels_map.c +0 -3467
  208. data/ext/carray_kernels_moncmp.c +0 -2097
  209. data/ext/carray_kernels_monop.c +0 -18313
  210. data/ext/carray_kernels_reduce_aggregate.c +0 -26005
  211. data/ext/carray_kernels_reduce_boolean.c +0 -330
  212. data/ext/carray_kernels_reduce_cumulative.c +0 -14862
  213. data/ext/carray_kernels_reduce_extreme.c +0 -17494
  214. data/ext/carray_kernels_reduce_variance.c +0 -3910
  215. data/ext/carray_kernels_scan.c +0 -3693
  216. data/ext/carray_kernels_search.c +0 -32218
  217. data/ext/carray_kernels_sort.c +0 -10626
  218. data/ext/carray_kernels_triop.c +0 -1392
@@ -75,7 +75,8 @@ class CArray
75
75
  # Use `CArray.meld` for the uniform-shape view-default counterpart.
76
76
  #
77
77
  # @param list [Array<CArray>] pieces to concatenate.
78
- # @param axis [Integer] axis to concatenate along.
78
+ # @param axis [Integer, nil] axis to concatenate along; `nil` is the
79
+ # same as `0`.
79
80
  # @param data_type [Symbol, Integer, nil] result `data_type`;
80
81
  # inferred via `result_type` when `nil`.
81
82
  # @return [CArray] fresh CArray with per-piece `axis` sizes summed.
@@ -83,7 +84,8 @@ class CArray
83
84
  # inconsistent.
84
85
  def self.concatenate (list, axis: 0, data_type: nil)
85
86
  raise ArgumentError, "concatenate: list must not be empty" if list.empty?
86
- __ragged_paste(list, [list.size], axis, data_type)
87
+ axis = 0 if axis.nil?
88
+ __ragged_paste(list, [list.size], axis, data_type, "concatenate")
87
89
  end
88
90
 
89
91
  # @overload concatenate(*others, axis: 0, data_type: nil)
@@ -128,7 +130,7 @@ class CArray
128
130
  raise ArgumentError,
129
131
  "mosaic: tdim product (#{expected}) must equal list size (#{list.size})"
130
132
  end
131
- __ragged_paste(list, tdim, axis, data_type)
133
+ __ragged_paste(list, tdim, axis, data_type, "mosaic")
132
134
  end
133
135
 
134
136
  # Shared eager paste-loop helper (= ex-`combine` paste implementation,
@@ -136,7 +138,7 @@ class CArray
136
138
  # shape obtained by tiling `list` over `tdim` starting at `axis`, with
137
139
  # per-piece dim sizes summed along each tile axis (= block-matrix
138
140
  # consistency required across rows / columns).
139
- def self.__ragged_paste (list, tdim, axis, data_type)
141
+ def self.__ragged_paste (list, tdim, axis, data_type, name)
140
142
  list = CArray.promote_list(list, data_type: data_type)
141
143
  # promote_list has already enforced homogeneity (= common data_type, and
142
144
  # for Face elements: same Face class + portable + state-compatible). So,
@@ -144,12 +146,16 @@ class CArray
144
146
  # result with face_lift, rather than special-casing data_class / DATA_SIZE
145
147
  # here (= which only ever handled CARecord and silently corrupted plain
146
148
  # fixlen / numeric Faces by leaving bytes nil).
147
- face_parent = list[0].face? ? list[0] : nil
149
+ # The Face is re-wrapped from the widest piece: a fixlen Face carries
150
+ # its cell width, and the result is as wide as its widest piece.
151
+ face_parent = list[0].face? ? list.max_by(&:bytes) : nil
148
152
  if face_parent
149
- list = list.map { |x| s = x; s = s.parent while s.face?; s }
153
+ list = list.map { |x| x.__send__(:__face_storage__) }
150
154
  end
151
155
  data_type = list[0].data_type
152
- bytes = (data_type == :fixlen) ? list[0].bytes : nil
156
+ # Fixlen pieces of different widths join at the widest, so no cell is
157
+ # cut; each narrower piece is padded with NUL as it is pasted.
158
+ bytes = (data_type == :fixlen) ? list.map(&:bytes).max : nil
153
159
  # promote_list guarantees a non-empty list of CArrays, so the reference
154
160
  # shape is simply the first element (a CScalar carries shape [1], so a
155
161
  # leading scalar is fine; the scalar-expansion pass below broadcasts it).
@@ -157,7 +163,7 @@ class CArray
157
163
  dim = ref.shape
158
164
  ndim = ref.ndim
159
165
  tndim = tdim.size
160
- axis = CArray.normalize_axis(axis, ndim - tndim + 1, "concatenate/mosaic")
166
+ axis = CArray.normalize_axis(axis, ndim - tndim + 1, name)
161
167
 
162
168
  list = list.map do |x|
163
169
  if x.scalar?
@@ -190,7 +196,7 @@ class CArray
190
196
  block.each_with_index do |item, *tidx|
191
197
  unless item.ndim == ndim
192
198
  raise ArgumentError,
193
- "concatenate/mosaic: piece at tile #{tidx.inspect} has ndim " \
199
+ "#{name}: piece at tile #{tidx.inspect} has ndim " \
194
200
  "#{item.ndim} (expected #{ndim})"
195
201
  end
196
202
  ndim.times do |d|
@@ -199,7 +205,7 @@ class CArray
199
205
  expected = on_tile ? tile_sizes[i][tidx[i]] : dim[d]
200
206
  unless item.shape[d] == expected
201
207
  raise ArgumentError,
202
- "concatenate/mosaic: piece at tile #{tidx.inspect} has " \
208
+ "#{name}: piece at tile #{tidx.inspect} has " \
203
209
  "#{on_tile ? 'tile' : 'non-tile'} axis #{d} size " \
204
210
  "#{item.shape[d]} (expected #{expected}); pieces must agree " \
205
211
  "on non-tile axes and be block-matrix consistent along tile axes"
@@ -23,8 +23,8 @@ class CArray
23
23
  # Distinctness is the family's hash-key judgement (see {#unique}):
24
24
  # `==` for numeric with all NaN collapsed to one value and
25
25
  # -0.0 / +0.0 the same value; `eql?` / `hash` for `CA_OBJECT` and
26
- # `CA_FIXLEN`. Complex is not a lane the factorizer takes and raises
27
- # {CArray::DataTypeError}.
26
+ # `CA_FIXLEN`. Complex values compare by value, with NaN and -0.0
27
+ # treated the same way in each component.
28
28
  #
29
29
  # There is no `sort:` here, unlike {#unique} and {#value_counts}:
30
30
  # the codes index the levels, so reordering the vocabulary would
@@ -64,7 +64,8 @@ class CArray
64
64
  flat_addr, outer, rest = gather_nd_flat_addr(indices, "gather_nd")
65
65
  out_shape = outer + rest
66
66
  # flatten + 1-D fancy indexing -> CAMapping view -> materialise via .copy.
67
- result = self.flatten[flat_addr].copy
67
+ # A masked coordinate reads as UNDEF: project carries the address mask.
68
+ result = flat_addr.has_mask? ? flatten.project(flat_addr) : self.flatten[flat_addr].copy
68
69
  out_shape.empty? ? result : result.reshape(*out_shape)
69
70
  end
70
71
 
@@ -91,7 +92,12 @@ class CArray
91
92
  # @raise [IndexError] when a coordinate is out of range on any axis.
92
93
  def put_nd (indices, values)
93
94
  flat_addr, _outer, _rest = gather_nd_flat_addr(indices, "put_nd")
94
- self.flatten[flat_addr] = values
95
+ if flat_addr.has_mask?
96
+ # A masked coordinate writes nothing: scatter_replace! skips it.
97
+ flatten.scatter_replace!(flat_addr, values)
98
+ else
99
+ self.flatten[flat_addr] = values
100
+ end
95
101
  self
96
102
  end
97
103
 
@@ -150,6 +156,11 @@ class CArray
150
156
  flat_idx = (flat_idx_raw + dims) % dims
151
157
 
152
158
  base = (flat_idx * strides).sum(axis: 1).int64.reshape(m) # (M,)
159
+ # sum skips a masked coordinate, which would leave its row pointing at
160
+ # the wrong cell: a row with any masked coordinate has no address.
161
+ if flat_idx_raw.has_mask?
162
+ base[flat_idx_raw.is_masked.any(axis: 1)] = UNDEF
163
+ end
153
164
 
154
165
  flat_addr =
155
166
  if rest.empty?
@@ -11,8 +11,8 @@ class CArray
11
11
  # appends at the end) and a negative value counts from the end;
12
12
  # `bsize[i]` must be non-negative. Inserted cells are filled by
13
13
  # the block's return value, or left at the type's default when
14
- # no block is given. Preserves fixlen bytes and Face identity
15
- # (built on the storage layout, re-wrapped via `face_lift`).
14
+ # no block is given. Preserves fixlen bytes and Face identity; the
15
+ # block returns a value of the array (a Time for a time array).
16
16
  # The `offset` array is not mutated.
17
17
  #
18
18
  # @param offset [Array<Integer>] insertion offset per axis.
@@ -40,21 +40,16 @@ class CArray
40
40
  # offset == dim (append) leaves nothing on the upper side to shift.
41
41
  grids[i][offset[i]..-1].seq!(offset[i]+bsize[i]) if offset[i] < shape[i]
42
42
  end
43
- # Build at the storage layout (preserving bytes for fixlen / Face),
44
- # then re-wrap as the same Face.
45
- face_parent = self.face? ? self : nil
46
- src = self
47
- src = src.parent while src.face?
48
- dt = src.data_type
49
- bytes = (dt == :fixlen) ? src.bytes : nil
50
- out = CArray.new(dt, newdim, bytes: bytes)
43
+ # The original cells are copied as storage, so a read-only Face inserts
44
+ # too; the block's value is written as a value of the array, which a
45
+ # read-only Face refuses.
46
+ out = __blank_like__(newdim)
51
47
  if block_given?
52
- sel = out.true
53
- sel[*grids] = 0
48
+ sel = CArray.boolean(*newdim) { true }
49
+ sel[*grids] = false
54
50
  out[sel] = block.call
55
51
  end
56
- out[*grids] = src
57
- out = out.face_lift(face_parent) if face_parent
52
+ out.__send__(:__face_storage__)[*grids] = __face_storage__
58
53
  return out
59
54
  end
60
55
 
@@ -15,8 +15,11 @@ class CArray
15
15
  # {CArray.result_type}), so membership is value-correct across
16
16
  # data types (e.g. an int cell equals a float set element of the same
17
17
  # value, and a fractional set element never truncates onto an int
18
- # cell). Genuinely incompatible data types (e.g. numeric vs fixlen)
19
- # raise.
18
+ # cell). Integers of mixed sign are matched by value, as {#eq}
19
+ # compares them: `-1` in an int8 array never matches `255` in a uint8
20
+ # one, and the set operations answer such a pair in a type that holds
21
+ # both (int16 for uint8 with int8, object for uint64 with int64).
22
+ # Genuinely incompatible data types (e.g. numeric vs fixlen) raise.
20
23
  #
21
24
  # Membership is value-based and shares the distinctness of the
22
25
  # value-hash discovery family ({#unique} / {#value_counts}):
@@ -27,6 +30,15 @@ class CArray
27
30
  # (so a 5-byte array written from `"be"` is `is_in(["be"])`). A
28
31
  # `CA_FIXLEN` set given as a CArray must already be of that width.
29
32
  #
33
+ # An object array keeps Ruby's own split between its two kinds of
34
+ # equality. A lookup through a hash -- `is_in`, the set operations,
35
+ # {#locate_addr}, {#unique} and the rest of the discovery family --
36
+ # uses `eql?`, as `Array#uniq`, `Array#&` and `Set` do, so `1` and
37
+ # `1.0` are different values there. A cell-by-cell comparison --
38
+ # {#eq}, {#count}, {#search}, {#bsearch} -- uses `==`, as
39
+ # `Array#include?` does, so they are equal there. To match by value
40
+ # across Integer and Float, convert the array to a numeric type first.
41
+ #
30
42
  # Masked cells of `values` do not enter the set. Masked cells of
31
43
  # `self` stay masked in the result (membership is unknown), so
32
44
  # `is_in` propagates `self`'s mask like an element-wise comparison.
@@ -64,6 +76,7 @@ class CArray
64
76
  def intersection (other, sort: false)
65
77
  a, b = promote_value_set(other)
66
78
  r = a.__send__(:__intersection__, b)
79
+ r = r.to_unit(unit) if !a.equal?(self) && respond_to?(:in_common_unit_with)
67
80
  sort ? r.sort : r
68
81
  end
69
82
 
@@ -78,13 +91,17 @@ class CArray
78
91
  def difference (other, sort: false)
79
92
  a, b = promote_value_set(other)
80
93
  r = a.__send__(:__difference__, b)
94
+ r = r.to_unit(unit) if !a.equal?(self) && respond_to?(:in_common_unit_with)
81
95
  sort ? r.sort : r
82
96
  end
83
97
 
84
98
  # @overload union(other, sort: false)
85
99
  # Returns a 1-D CArray of the distinct values appearing in either
86
100
  # `self` or `other`, in self-then-other first-appearance order.
87
- # See {#intersection} for the shared semantics and options.
101
+ # See {#intersection} for the shared semantics and options. Two time
102
+ # arrays of different units give their union in the finer unit, which
103
+ # holds the values of both ({#intersection} and {#difference} answer in
104
+ # `self`'s unit, since their values are `self`'s).
88
105
  #
89
106
  # @param other [CArray, Array, Range] promoted with `self`.
90
107
  # @param sort [Boolean] when true, return the values sorted ascending
@@ -113,13 +130,60 @@ class CArray
113
130
  # operand (Numo, a MemoryView producer, ...) comes in through wrap_readonly,
114
131
  # the canonical type-coercion entry, so its format's data type drives the promote.
115
132
  def promote_value_set (values)
133
+ # Two time arrays of different units meet in the finer one.
134
+ if values.is_a?(CArray) && values.class == self.class &&
135
+ respond_to?(:in_common_unit_with)
136
+ return in_common_unit_with(values)
137
+ end
138
+ values = values_for_lookup(values) if values.is_a?(CArray)
139
+ # Two fixed-width string arrays of different widths meet at the wider.
140
+ if values.is_a?(CArray) && data_type == CA_FIXLEN && values.data_type == CA_FIXLEN
141
+ return fixlen_widened_with(values)
142
+ end
143
+ # A Face that reconciles operands (CATime) reads a bare Array as values
144
+ # of its own kind -- instants written as Time / String / elements.
145
+ if values.is_a?(Array) && face? && respond_to?(:to_comparable)
146
+ return [self, to_comparable(values)]
147
+ end
148
+ # A single value (a Time, an element, a bare number) is a set of one,
149
+ # read the same way; a bare number is refused like any other operand
150
+ # that is not of its kind.
151
+ if !values.is_a?(CArray) && !values.is_a?(Range) && face? &&
152
+ respond_to?(:to_comparable)
153
+ return [self, to_comparable([values])]
154
+ end
155
+ # ...and so does an array of values that are not of its kind (an object
156
+ # column of Time values, a string column).
157
+ if values.is_a?(CArray) && respond_to?(:to_comparable) &&
158
+ !values.respond_to?(:to_comparable)
159
+ return [self, values_in_face(self, values) || to_comparable([])]
160
+ end
161
+ # The same from the other side: values of a time array looked for in an
162
+ # array that is not one.
163
+ if values.is_a?(CArray) && values.respond_to?(:to_comparable) &&
164
+ !respond_to?(:to_comparable)
165
+ return [values_in_face(values, self) || values.to_comparable([]), values]
166
+ end
167
+ # A Face whose storage is not of its surface type. When its cells are
168
+ # its values, the set meets it as it would meet an array of the storage
169
+ # type, and the Face stays while that type does not change. Any other
170
+ # Face leaves the set to the lookup, which brings it into the Face's
171
+ # space or refuses it.
172
+ if face? && (storage = __face_storage__).data_type != data_type
173
+ unless __comparable_storage__
174
+ set = values.is_a?(Range) ? values.to_a : values
175
+ return [self, set.is_a?(CArray) ? set : CArray.wrap_readonly(set)]
176
+ end
177
+ s, set = storage.send(:promote_value_set, values)
178
+ return [s.equal?(storage) ? self : s, set]
179
+ end
116
180
  case values
117
181
  when Array then return promote_elements(values)
118
182
  when Range then return promote_elements(values.to_a)
119
183
  end
120
184
  set = values.is_a?(CArray) ? values : CArray.wrap_readonly(values)
121
185
  return [self, set] if set.data_type == data_type # common fast path
122
- t = CArray.result_type(self, set)
186
+ t = value_match_type(CArray.result_type(self, set), set.data_type)
123
187
  [coerce_self(t), set.to_type(t)]
124
188
  end
125
189
 
@@ -141,10 +205,59 @@ class CArray
141
205
  [self, elems.to_ca.to_type(data_type)]
142
206
  else
143
207
  t = CArray.result_type(self, *elems)
208
+ t = value_match_type(t, CA_INT64) if elems.any? { |v| v.is_a?(Integer) && v < 0 }
144
209
  [coerce_self(t), elems.to_ca.to_type(t)]
145
210
  end
146
211
  end
147
212
 
213
+ # +other+ as a lookup against self reads it. A string array whose cells
214
+ # hold ranges into a pool of its own (CAConstString) is read by its
215
+ # strings, and a categorical by its labels, since their storage is not
216
+ # their values; anything else, or the same kind of array as self, is
217
+ # taken as it is. A record is left to be refused.
218
+ def values_for_lookup (other)
219
+ return other if other.send(:__orderable_storage__)
220
+ return other if face? && other.class == self.class
221
+ if other.respond_to?(:to_string)
222
+ other.to_string
223
+ elsif other.is_a?(CACategorical)
224
+ other.to_type(:object)
225
+ else
226
+ other
227
+ end
228
+ end
229
+
230
+ # [self, other] at one cell width when both are fixed-width string arrays
231
+ # (CAFixlenString) of different widths: the narrower is padded with NUL to
232
+ # the wider, as concatenate joins them, so a string matches whatever width
233
+ # it is held at. Raw fixlen cells of different widths are values of
234
+ # different lengths, not padded strings, and are left as they are (the
235
+ # lookup refuses them, naming the widths).
236
+ def fixlen_widened_with (other)
237
+ return [self, other] if bytes == other.bytes
238
+ return [self, other] unless is_a?(CAFixlenString) && other.is_a?(CAFixlenString)
239
+ w = [bytes, other.bytes].max
240
+ [widen_fixlen_string(self, w), widen_fixlen_string(other, w)]
241
+ end
242
+
243
+ def widen_fixlen_string (x, w)
244
+ x.bytes == w ? x : x.to_fixlen_string(bytes: w)
245
+ end
246
+
247
+ # The values of +other+, which has no to_comparable, read as values of +face+'s
248
+ # kind (instants written as Time / Date / String for a time key), in an
249
+ # array of +other+'s shape; a masked cell stays masked. nil when every
250
+ # cell is masked: there is nothing to read.
251
+ private def values_in_face(face, other)
252
+ return face.to_comparable(other) if other.is_a?(Array)
253
+ vals = other.flatten.to_a
254
+ filler = vals.find { |v| !UNDEF.equal?(v) }
255
+ return nil if filler.equal?(nil) && vals.all? { |v| UNDEF.equal?(v) }
256
+ out = face.to_comparable(vals.map { |v| UNDEF.equal?(v) ? filler : v })
257
+ out[other.is_masked.flatten] = UNDEF if other.has_mask?
258
+ out.reshape(*other.shape)
259
+ end
260
+
148
261
  def coerce_self (t)
149
262
  CArray.result_type(self) == t ? self : to_type(t)
150
263
  end
@@ -66,7 +66,7 @@ class CArray
66
66
  t = transpose(*order)
67
67
  end
68
68
  inner = t.shape[-1]
69
- outer_n = t.elements / inner # 1 when ndim == 1
69
+ outer_n = t.shape[0...-1].inject(1, :*) # 1 when ndim == 1
70
70
  flat = t.reshape(outer_n, inner)
71
71
 
72
72
  strings = Array.new(outer_n) { |i| flat[i, nil].to_a.join(sep_str) }
@@ -1,3 +1,5 @@
1
+ require "carray/methods/is_in" # fixlen_widened_with, values_in_face, values_for_lookup
2
+
1
3
  class CArray
2
4
 
3
5
  # @!group Sorting and searching
@@ -27,8 +29,13 @@ class CArray
27
29
  # `self` and `ref` are compared at their common type, negotiated by
28
30
  # `CArray.result_type` — a fractional query against an integer `ref`
29
31
  # 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.
32
+ # longer matches `1`, and integers of mixed sign are matched by value
33
+ # (`-1` never matches `255`). Cross-family input (numeric against
34
+ # fixlen) raises.
35
+ #
36
+ # A time key (`CATime`) reads values that are not time arrays -- Time,
37
+ # Date or String objects, in an Array, an `:object` array or a string
38
+ # array -- as instants, on either side of the lookup, as {#is_in} does.
32
39
  #
33
40
  # Typical use is time-axis lookup: compute the address once against a
34
41
  # reference axis, then reuse it to gather from many `ref`-shaped
@@ -41,15 +48,40 @@ class CArray
41
48
  # numeric lane.
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
+ if respond_to?(:to_comparable) && !ref.respond_to?(:to_comparable)
62
+ ref = values_in_face(self, ref)
63
+ return CArray.int64(*shape).fill(UNDEF) unless ref
64
+ elsif ref.respond_to?(:to_comparable) && !respond_to?(:to_comparable)
65
+ q = values_in_face(ref, self)
66
+ return CArray.int64(*shape).fill(UNDEF) unless q
67
+ return q.locate_addr(ref)
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
46
72
  ref = ref.to_ca unless ref.is_a?(CArray)
73
+ ref = values_for_lookup(ref)
74
+ if data_type == CA_FIXLEN && ref.data_type == CA_FIXLEN && bytes != ref.bytes
75
+ a, b = fixlen_widened_with(ref)
76
+ return a.locate_addr(b) unless a.equal?(self) && b.equal?(ref)
77
+ end
47
78
  # Put self and ref in a common lane via the single-source promotion rule
48
79
  # (CArray.result_type), so a fractional query against an int ref is compared
49
80
  # at the promoted type instead of truncating (1.5 no longer matches 1).
50
81
  # 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)
82
+ # indices into ref. result_type raises for cross-family input; an integer
83
+ # pair of mixed sign is matched by value (see value_match_type).
84
+ t = value_match_type(CArray.result_type(self, ref), ref.data_type)
53
85
  q = (data_type == t) ? self : to_type(t)
54
86
  r = (ref.data_type == t) ? ref : ref.to_type(t)
55
87
  q.send(:__locate_addr__, r)
@@ -57,16 +89,18 @@ class CArray
57
89
 
58
90
  # @overload locate_nearest_addr(ref, direction: :round, tolerance: nil)
59
91
  # 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`.
92
+ # the nearest reference value. Continuous sibling of {#locate_addr}:
93
+ # each cell is placed among the sorted reference values by comparing
94
+ # it with them, so an integer key is exact at any size and an infinite
95
+ # one keeps its place. `:round` takes the nearer neighbour, the upper
96
+ # one on a tie.
63
97
  #
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]`.
98
+ # Outside `ref`'s span the direction still decides: `:floor` matches
99
+ # a cell after the last value to that last value, `:ceil` matches a
100
+ # cell before the first value to that first value, and `:round`
101
+ # matches either to the nearer end. The other side has no match
102
+ # (`:floor` before the first value, `:ceil` after the last) and is
103
+ # masked. Use `tolerance:` to refuse matches that are too far away.
70
104
  #
71
105
  # `tolerance:` (default `nil`) sets a maximum accepted absolute
72
106
  # distance between `self[i]` and its matched `ref` value. When
@@ -76,50 +110,157 @@ class CArray
76
110
  #
77
111
  # `ref` need not be given in ascending order: it is sorted internally
78
112
  # and the returned addresses are mapped back to positions in `ref` as
79
- # passed.
113
+ # passed. A masked or NaN `ref` value matches nothing. Two time keys
114
+ # of different units are compared in the finer of the two.
115
+ # With one value present in `ref`, that value is the match for
116
+ # every cell the direction allows (`:floor` at or after it, `:ceil`
117
+ # at or before it, `:round` all). With none, every cell is masked;
118
+ # an empty `self` gives an empty result. A masked or NaN cell of
119
+ # `self` is always masked.
80
120
  #
81
121
  # @param ref [CArray] 1-D reference grid to match against.
82
122
  # @param direction [Symbol] `:round`, `:floor`, or `:ceil` — rounding
83
123
  # 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.
124
+ # @param tolerance [Numeric, String, CATimedelta::Element, nil] maximum
125
+ # accepted `|self - ref|` distance; cells beyond this are masked.
126
+ # For a time key it is a duration: a String such as `"10 minutes"` or
127
+ # a CATimedelta value (a bare number is refused, since its length
128
+ # would depend on the key's unit). `nil` disables the check.
86
129
  # @return [CArray] `:int64` flat addresses into `ref`, same shape as
87
- # `self`; out-of-range and beyond-tolerance cells are masked.
130
+ # `self`; cells with no match in the direction and beyond-tolerance
131
+ # cells are masked.
88
132
  # @raise [ArgumentError] when `direction` is not one of the accepted
89
- # symbols.
133
+ # symbols, or `tolerance` is not a duration for a time key.
90
134
  def locate_nearest_addr (ref, direction: :round, tolerance: nil)
91
135
  unless [:round, :floor, :ceil].include?(direction)
92
136
  raise ArgumentError,
93
137
  "locate_nearest_addr: direction must be :round / :floor / " \
94
138
  ":ceil (got #{direction.inspect})"
95
139
  end
140
+ return CArray.int64(*shape) if elements == 0
141
+ # A masked reference value matches nothing, as in locate_addr: sorting
142
+ # puts the masked ones last, and only the others are searched. A NaN
143
+ # one has no place among the others either; it sorts after them.
96
144
  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
145
+ ri = ri[0...ref.count_not_masked] if ref.has_mask?
146
+ if ref.float? && ri.elements > 0
147
+ ri = ri[0...(ri.elements - ref[ri].is_nan.count(true))]
148
+ end
149
+ if ri.elements == 0
150
+ out = CArray.int64(*shape)
151
+ out[] = UNDEF
152
+ return out
108
153
  end
109
- masked = sec.mask_invalid
154
+ rs = ref[ri]
155
+ n = rs.elements
156
+ rk, qk = nearest_keys(rs, self)
157
+ # The position of each cell among the sorted references is counted, not
158
+ # interpolated: :floor takes the last reference at or below it, :ceil the
159
+ # first at or above it.
160
+ # Counting keeps an integer key exact past 2**53 and an infinite one in
161
+ # its place, where an interpolated position would not.
110
162
  si = case direction
111
- when :round then masked.round
112
- when :floor then masked.floor
113
- when :ceil then masked.ceil
114
- end.int64
163
+ when :floor then nearest_count_below(rk, qk, true) - 1
164
+ when :ceil then nearest_count_below(rk, qk, false)
165
+ when :round then nearest_round(rk, qk, nearest_count_below(rk, qk, false))
166
+ end
167
+ si[si.lt(0) | si.ge(n)] = UNDEF
168
+ si[qk.is_nan.strip_mask(false)] = UNDEF if qk.float?
169
+ si = si.reshape(*shape)
170
+ si[is_masked] = UNDEF if has_mask?
115
171
  idx = ri.project(si)
116
172
  if tolerance
117
173
  dist = (ref.project(idx) - self).abs
118
- idx[dist > tolerance] = UNDEF
174
+ idx[dist > nearest_tolerance(tolerance, dist)] = UNDEF
119
175
  end
120
176
  idx
121
177
  end
122
178
 
179
+ # The keys locate_nearest_addr orders by: the storage of the sorted
180
+ # reference and of the query, in one numeric type. A Face key is first
181
+ # brought into the reference's space (two time keys of different units
182
+ # into the finer of the two, which holds both exactly); plain keys meet at
183
+ # the type CArray.result_type gives, an integer pair of mixed sign at one
184
+ # that holds both.
185
+ private def nearest_keys(rs, q)
186
+ if rs.face? || q.face?
187
+ if rs.respond_to?(:to_unit) && q.class == rs.class && q.unit != rs.unit
188
+ u = CATimeUnitAlgebra.finer(rs.unit, q.unit)
189
+ rs = rs.to_unit(u)
190
+ q = q.to_unit(u)
191
+ end
192
+ q = rs.__send__(:__reconcile_operand__, q, "locate_nearest_addr")
193
+ rs = rs.__send__(:__face_storage__)
194
+ end
195
+ t = value_match_type(CArray.result_type(q, rs), rs.data_type)
196
+ rk = (rs.data_type == t) ? rs : rs.to_type(t)
197
+ qk = (q.data_type == t) ? q : q.to_type(t)
198
+ [rk, qk.flatten]
199
+ end
200
+
201
+ # For each query key, how many of the sorted reference keys are below it
202
+ # (inclusive: at or below). A numeric key is binary-searched in C; an
203
+ # object key is sorted together with the references, a tie putting the
204
+ # reference first when it counts, and counted against the references met
205
+ # before it.
206
+ private def nearest_count_below(rk, qk, inclusive)
207
+ return rk.send(:__count_below__, qk, inclusive) unless rk.data_type == CA_OBJECT
208
+ n, m = rk.elements, qk.elements
209
+ keys = CArray.concatenate([rk, qk])
210
+ tag = CArray.int8(n + m)
211
+ tag[0...n] = inclusive ? 0 : 1
212
+ tag[n...n+m] = inclusive ? 1 : 0
213
+ perm = CArray.sort_addr(keys, tag)
214
+ seen = perm.lt(n).cumsum.int64
215
+ query = perm.ge(n)
216
+ count = CArray.int64(m)
217
+ count[perm[query] - n] = seen[query]
218
+ count
219
+ end
220
+
221
+ # :round -- an exact match takes its first reference; otherwise the
222
+ # nearer of the two neighbours, the upper one on a tie. +lt+ counts the
223
+ # references below each key, so the upper neighbour is at lt and an exact
224
+ # match is the reference there.
225
+ private def nearest_round(rk, qk, lt)
226
+ lo = rk.project(lt - 1)
227
+ hi = rk.project(lt)
228
+ upper = (hi - qk).le(qk - lo).strip_mask(false)
229
+ upper[lo.is_masked] = true if lo.has_mask?
230
+ upper[hi.is_masked] = false if hi.has_mask?
231
+ upper[hi.eq(qk).strip_mask(false)] = true
232
+ si = lt - 1
233
+ si[upper] = lt[upper]
234
+ si
235
+ end
236
+
237
+ # The tolerance in the units of the distance. A time key measures its
238
+ # distance as a CATimedelta, so its tolerance is a duration: a String in
239
+ # the spelling CArray.time takes for a unit ("10 minutes"), or a
240
+ # CATimedelta value. A bare number would mean a different length in each
241
+ # unit, so it is refused there; any other key takes a number.
242
+ private def nearest_tolerance(tolerance, dist)
243
+ unless dist.is_a?(CATimedelta)
244
+ if tolerance.is_a?(String)
245
+ raise ArgumentError,
246
+ "tolerance: #{tolerance.inspect} is a duration, but the key is " \
247
+ "#{data_type_name}; give a number"
248
+ end
249
+ return tolerance
250
+ end
251
+ case tolerance
252
+ when String
253
+ res = CATime::Resolution.parse(tolerance)
254
+ CATimedelta::Element.new(res.count, res.base)
255
+ when CATimedelta::Element
256
+ tolerance
257
+ else
258
+ raise ArgumentError,
259
+ "tolerance: a time key needs a duration such as \"10 minutes\" " \
260
+ "or a CATimedelta value (got #{tolerance.inspect})"
261
+ end
262
+ end
263
+
123
264
  # @!endgroup
124
265
 
125
266
  end