carray 3.0.1 → 3.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +824 -0
  3. data/README.md +5 -6
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_dispatch.c +258 -4
  6. data/ext/ca_axis_group.c +368 -184
  7. data/ext/ca_binop_dispatch.c +54 -32
  8. data/ext/ca_binop_dispatch.h +1 -0
  9. data/ext/ca_categorical_iterator.c +90 -41
  10. data/ext/ca_compare.c +8 -2
  11. data/ext/ca_for_buffer.h +21 -68
  12. data/ext/ca_group_iter.c +27 -11
  13. data/ext/ca_kernel_iterator.c +960 -245
  14. data/ext/ca_kernel_iterator.h +352 -181
  15. data/ext/ca_monop_dispatch.c +4 -4
  16. data/ext/ca_monop_dispatch.h +4 -0
  17. data/ext/ca_obj_array.c +194 -42
  18. data/ext/ca_obj_bincmp.c +152 -121
  19. data/ext/ca_obj_binop.c +107 -210
  20. data/ext/ca_obj_bitarray.c +37 -24
  21. data/ext/ca_obj_bitfield.c +95 -62
  22. data/ext/ca_obj_block.c +5 -4
  23. data/ext/ca_obj_byte_swap.c +40 -40
  24. data/ext/ca_obj_const_string.c +180 -50
  25. data/ext/ca_obj_face.c +428 -62
  26. data/ext/ca_obj_face.h +107 -47
  27. data/ext/ca_obj_fake.c +21 -50
  28. data/ext/ca_obj_farray.c +1 -0
  29. data/ext/ca_obj_field.c +16 -3
  30. data/ext/ca_obj_fixlen_string.c +39 -26
  31. data/ext/ca_obj_grid.c +181 -124
  32. data/ext/ca_obj_meld.c +235 -55
  33. data/ext/ca_obj_moncmp.c +61 -61
  34. data/ext/ca_obj_monop.c +125 -107
  35. data/ext/ca_obj_object.c +551 -233
  36. data/ext/ca_obj_record.c +51 -16
  37. data/ext/ca_obj_reduce.c +5 -8
  38. data/ext/ca_obj_refer.c +117 -33
  39. data/ext/ca_obj_remap.c +57 -17
  40. data/ext/ca_obj_repeat.c +21 -5
  41. data/ext/ca_obj_roll.c +115 -11
  42. data/ext/ca_obj_select.c +88 -75
  43. data/ext/ca_obj_select_axis.c +58 -60
  44. data/ext/ca_obj_shift.c +22 -102
  45. data/ext/ca_obj_stack.c +223 -123
  46. data/ext/ca_obj_stride.c +267 -35
  47. data/ext/ca_obj_string.c +25 -22
  48. data/ext/ca_obj_tile.c +32 -16
  49. data/ext/ca_obj_time.c +23 -25
  50. data/ext/ca_obj_timedelta.c +21 -23
  51. data/ext/ca_obj_transpose.c +12 -3
  52. data/ext/ca_obj_triop.c +110 -117
  53. data/ext/ca_obj_window.c +157 -29
  54. data/ext/ca_op_ipower.c +25 -13
  55. data/ext/ca_op_powi.h +57 -30
  56. data/ext/ca_pow5_table.h +656 -0
  57. data/ext/ca_rng_normal.h +42 -0
  58. data/ext/ca_rng_xoshiro256pp.h +105 -0
  59. data/ext/ca_sort_kernels.h +70 -0
  60. data/ext/ca_sweep_engine.c +385 -198
  61. data/ext/ca_sweep_engine.h +31 -23
  62. data/ext/ca_triop_dispatch.c +4 -1
  63. data/ext/ca_triop_dispatch.h +5 -2
  64. data/ext/caframe_csv_reader.c +463 -0
  65. data/ext/carray.h +90 -28
  66. data/ext/carray_access.c +487 -231
  67. data/ext/carray_address_basis.c +731 -0
  68. data/ext/carray_attribute.c +45 -3
  69. data/ext/carray_bincount.c +32 -2
  70. data/ext/carray_broadcast.c +49 -17
  71. data/ext/carray_call_cfunc.c +667 -511
  72. data/ext/carray_cast.c +1556 -247
  73. data/ext/carray_cast_func.rb +34 -21
  74. data/ext/carray_conversion.c +197 -73
  75. data/ext/carray_copy.c +84 -38
  76. data/ext/carray_core.c +591 -119
  77. data/ext/carray_count.c +148 -96
  78. data/ext/carray_element.c +18 -27
  79. data/ext/carray_factorize.c +604 -291
  80. data/ext/carray_generate.c +76 -40
  81. data/ext/carray_histogram.c +152 -94
  82. data/ext/carray_hold.c +2 -1
  83. data/ext/carray_index_classifier.c +63 -96
  84. data/ext/carray_internal.h +400 -1
  85. data/ext/carray_lazy.c +834 -79
  86. data/ext/carray_loop.c +88 -66
  87. data/ext/carray_mask.c +705 -324
  88. data/ext/carray_math_kernel.h +1 -0
  89. data/ext/carray_median_percentile.c +263 -63
  90. data/ext/carray_memory_view.c +254 -248
  91. data/ext/carray_operator.c +1093 -363
  92. data/ext/carray_order.c +133 -69
  93. data/ext/carray_partition.c +102 -56
  94. data/ext/carray_random.c +582 -204
  95. data/ext/carray_scatter.c +308 -65
  96. data/ext/carray_segment.c +257 -0
  97. data/ext/carray_slab.c +183 -70
  98. data/ext/carray_sort.c +235 -240
  99. data/ext/carray_sort_kernel.c +8 -1
  100. data/ext/carray_struct.c +99 -21
  101. data/ext/carray_test.c +167 -64
  102. data/ext/carray_time_text.c +1182 -0
  103. data/ext/carray_utils.c +363 -98
  104. data/ext/extconf.rb +27 -2
  105. data/ext/mk_call_cfunc.rb +103 -118
  106. data/ext/mk_pow5_table.rb +35 -0
  107. data/ext/mkkernel.rb +1349 -851
  108. data/ext/ruby_carray.c +17 -2
  109. data/ext/version.h +4 -4
  110. data/ext/version.rb +4 -2
  111. data/lib/carray/attribute.rb +39 -0
  112. data/lib/carray/autoload_carray.rb +24 -3
  113. data/lib/carray/autoload_method_extension.rb +18 -0
  114. data/lib/carray/axis_group.rb +245 -20
  115. data/lib/carray/basics.rb +95 -25
  116. data/lib/carray/bincount_nd.rb +6 -0
  117. data/lib/carray/block_iterator.rb +200 -54
  118. data/lib/carray/boolean_reduce.rb +21 -29
  119. data/lib/carray/categorical.rb +244 -59
  120. data/lib/carray/categorical_iterator.rb +391 -757
  121. data/lib/carray/complex.rb +36 -29
  122. data/lib/carray/conditional.rb +94 -7
  123. data/lib/carray/const_string.rb +177 -27
  124. data/lib/carray/construct.rb +40 -0
  125. data/lib/carray/data_type_extension.rb +3 -0
  126. data/lib/carray/data_type_limits.rb +91 -0
  127. data/lib/carray/fixlen_string.rb +1 -1
  128. data/lib/carray/frame/concat.rb +157 -6
  129. data/lib/carray/frame/convert.rb +13 -7
  130. data/lib/carray/frame/csv_parser.rb +590 -76
  131. data/lib/carray/frame/describe.rb +79 -0
  132. data/lib/carray/frame/frame.rb +205 -35
  133. data/lib/carray/frame/group.rb +199 -40
  134. data/lib/carray/frame/io.rb +452 -60
  135. data/lib/carray/frame/join.rb +134 -46
  136. data/lib/carray/frame/records.rb +54 -19
  137. data/lib/carray/frame/reshape.rb +448 -0
  138. data/lib/carray/frame/sort.rb +26 -2
  139. data/lib/carray/frame/verbs.rb +726 -56
  140. data/lib/carray/frame.rb +2 -0
  141. data/lib/carray/fuse_source.rb +111 -2
  142. data/lib/carray/fusion.rb +229 -38
  143. data/lib/carray/inspect.rb +71 -20
  144. data/lib/carray/iterator.rb +157 -2
  145. data/lib/carray/lazy.rb +54 -67
  146. data/lib/carray/mask_gap_fill.rb +66 -50
  147. data/lib/carray/math.rb +32 -19
  148. data/lib/carray/meld_reduce.rb +40 -7
  149. data/lib/carray/methods/align_addr.rb +8 -4
  150. data/lib/carray/methods/bincount.rb +2 -2
  151. data/lib/carray/methods/bit_string.rb +14 -3
  152. data/lib/carray/methods/composition.rb +16 -10
  153. data/lib/carray/methods/discovery_along.rb +74 -0
  154. data/lib/carray/methods/factorize.rb +50 -0
  155. data/lib/carray/methods/gather_nd.rb +13 -2
  156. data/lib/carray/methods/insert_block.rb +9 -14
  157. data/lib/carray/methods/is_in.rb +130 -6
  158. data/lib/carray/methods/join.rb +1 -1
  159. data/lib/carray/methods/locate_addr.rb +235 -21
  160. data/lib/carray/methods/mask_duplicates.rb +35 -1
  161. data/lib/carray/methods/mode.rb +18 -1
  162. data/lib/carray/methods/nunique.rb +22 -1
  163. data/lib/carray/methods/repeat.rb +91 -0
  164. data/lib/carray/methods/resize.rb +21 -23
  165. data/lib/carray/methods/snap.rb +34 -12
  166. data/lib/carray/methods/unique.rb +52 -3
  167. data/lib/carray/record_order.rb +208 -0
  168. data/lib/carray/rng.rb +86 -0
  169. data/lib/carray/runtime.rb +14 -11
  170. data/lib/carray/segment_iterator.rb +990 -0
  171. data/lib/carray/serialize.rb +197 -27
  172. data/lib/carray/slab_iterator.rb +109 -40
  173. data/lib/carray/stack.rb +10 -4
  174. data/lib/carray/string_operation_extension.rb +9 -4
  175. data/lib/carray/struct.rb +142 -51
  176. data/lib/carray/struct_builder.rb +110 -14
  177. data/lib/carray/table.rb +2 -2
  178. data/lib/carray/time.rb +535 -105
  179. data/lib/carray/window_iterator.rb +259 -82
  180. data/lib/carray.rb +4 -2
  181. data/yard-stubs/ca_obj_array.rb +4 -4
  182. data/yard-stubs/ca_obj_bitarray.rb +7 -0
  183. data/yard-stubs/ca_obj_bitfield.rb +23 -12
  184. data/yard-stubs/ca_obj_block.rb +2 -7
  185. data/yard-stubs/ca_obj_fake.rb +1 -1
  186. data/yard-stubs/ca_obj_field.rb +1 -1
  187. data/yard-stubs/ca_obj_refer.rb +3 -3
  188. data/yard-stubs/ca_obj_roll.rb +0 -1
  189. data/yard-stubs/ca_obj_stride.rb +10 -7
  190. data/yard-stubs/ca_obj_transpose.rb +1 -1
  191. data/yard-stubs/ca_obj_window.rb +11 -4
  192. data/yard-stubs/carray_access.rb +6 -5
  193. data/yard-stubs/carray_broadcast.rb +2 -2
  194. data/yard-stubs/carray_cast.rb +78 -43
  195. data/yard-stubs/carray_conversion.rb +13 -1
  196. data/yard-stubs/carray_copy.rb +8 -3
  197. data/yard-stubs/carray_core.rb +3 -83
  198. data/yard-stubs/carray_count.rb +16 -2
  199. data/yard-stubs/carray_lazy.rb +210 -1
  200. data/yard-stubs/carray_loop.rb +4 -1
  201. data/yard-stubs/carray_mask.rb +34 -10
  202. data/yard-stubs/carray_math.rb +1499 -4
  203. data/yard-stubs/carray_median_percentile.rb +26 -8
  204. data/yard-stubs/carray_order.rb +43 -82
  205. data/yard-stubs/carray_scatter.rb +10 -3
  206. data/yard-stubs/carray_segment.rb +72 -0
  207. data/yard-stubs/carray_slab.rb +11 -8
  208. data/yard-stubs/carray_sort.rb +50 -8
  209. data/yard-stubs/carray_test.rb +10 -0
  210. data/yard-stubs/carray_utils.rb +0 -26
  211. metadata +19 -19
  212. data/ext/ca_for_each_element.h +0 -239
  213. data/ext/carray_build_flags.h +0 -3
  214. data/ext/carray_config.h +0 -28
  215. data/ext/carray_kernels_bincmp.c +0 -4446
  216. data/ext/carray_kernels_binop.c +0 -11001
  217. data/ext/carray_kernels_init.c +0 -1131
  218. data/ext/carray_kernels_map.c +0 -3467
  219. data/ext/carray_kernels_moncmp.c +0 -2097
  220. data/ext/carray_kernels_monop.c +0 -18313
  221. data/ext/carray_kernels_reduce_aggregate.c +0 -25837
  222. data/ext/carray_kernels_reduce_boolean.c +0 -330
  223. data/ext/carray_kernels_reduce_cumulative.c +0 -14593
  224. data/ext/carray_kernels_reduce_extreme.c +0 -16948
  225. data/ext/carray_kernels_reduce_variance.c +0 -3910
  226. data/ext/carray_kernels_scan.c +0 -3693
  227. data/ext/carray_kernels_search.c +0 -32138
  228. data/ext/carray_kernels_sort.c +0 -10626
  229. data/ext/carray_kernels_triop.c +0 -1392
@@ -69,41 +69,33 @@ class CArray
69
69
  # @return [Boolean, CArray] a scalar, or an array when an axis is given.
70
70
  def none (skip_masked: true, **opts)
71
71
  return __none_skipna__(**opts) if skip_masked
72
- # none = not any (Kleene): not(true)=false, not(false)=true, not(UNDEF)=UNDEF
73
- r = __kleene_fold(:any, opts)
74
- r.is_a?(CArray) ? r.not : (r.equal?(UNDEF) ? UNDEF : !r)
72
+ __kleene_fold(:none, opts)
75
73
  end
76
74
 
77
75
  private
78
76
 
79
- # Three-valued fold via counts. `kind` is :any (OR) or :all (AND).
80
- # `opts` carries axis: / keep_axis: (same surface as the skipna reduction).
77
+ # Three-valued fold. `kind` is :any, :all or :none. The available-case
78
+ # reduction answers wherever an unmasked cell settles the fold (a true for
79
+ # any, a false for all, a true for none); elsewhere a masked cell leaves the
80
+ # answer undetermined. Going through that reduction checks the data type,
81
+ # the axes and the keywords exactly as `skip_masked: true` does.
81
82
  def __kleene_fold (kind, opts)
82
- axis = opts[:axis]
83
- if axis.nil?
84
- if kind == :any
85
- return true if count(true) > 0 # a known TRUE settles OR
86
- else
87
- return false if count(false) > 0 # a known FALSE settles AND
88
- end
89
- return UNDEF if count_masked > 0 # undetermined
90
- return( kind == :any ? false : true )
83
+ r = case kind
84
+ when :any then __any_skipna__(**opts)
85
+ when :all then __all_skipna__(**opts)
86
+ else __none_skipna__(**opts)
87
+ end
88
+ cm = count_masked(**opts)
89
+ if r.is_a?(CArray)
90
+ settled = ( kind == :any ) ? r : r.not
91
+ out = r.copy
92
+ out[settled.not & (cm > 0)] = UNDEF
93
+ out
94
+ else
95
+ settled = ( kind == :any ) ? r : !r
96
+ return r if settled
97
+ ( cm > 0 ) ? UNDEF : r
91
98
  end
92
-
93
- keep = opts[:keep_axis]
94
- cnt_kw = keep ? { axis: axis, keep_axis: keep } : { axis: axis }
95
- ck = ( kind == :any ) ? count(true, **cnt_kw) : count(false, **cnt_kw)
96
- cm = count_masked(axis: axis) # count_masked has no keep_axis
97
- cm = cm.reshape(*ck.shape) unless cm.shape == ck.shape
98
-
99
- # A cell is undetermined only where it is masked AND the known side does
100
- # not settle it (ck counts the settling value: true for OR, false for AND).
101
- # Masking only those cells avoids leaving an all-zero mask behind.
102
- out = CArray.boolean(*ck.shape)
103
- out[] = ( kind == :any ) ? 0 : 1 # OR default false / AND default true
104
- out[ck.eq(0) & (cm > 0)] = UNDEF # genuinely undetermined
105
- out[ck > 0] = ( kind == :any ) ? 1 : 0 # known side dominates
106
- out
107
99
  end
108
100
 
109
101
  end
@@ -17,8 +17,12 @@
17
17
  # (per-category counts), `cat.codes.count(code)`, etc.
18
18
  # - per-cell access = decode the code into its label (`cat[i]` -> category)
19
19
  #
20
- # Exclusion (missing / out-of-vocabulary) is encoded two ways at once, and
21
- # because the Face is READONLY they can never desync:
20
+ # Exclusion (missing / out-of-vocabulary) is encoded two ways at once. Both
21
+ # are written when the categorical is built — categorize produces them, and
22
+ # from_codes normalises whatever it receives into them — and the Face is
23
+ # READONLY afterwards, so they stay in step. Both are needed because they have
24
+ # different readers: the axis-group kernel classifies on the code byte, the
25
+ # materialising paths classify on the mask.
22
26
  #
23
27
  # - the cell is MASKED -> CArray-native idiom: `cat[i]` is
24
28
  # UNDEF, `is_masked` / mask-aware
@@ -76,46 +80,148 @@ class CACategorical < CAObject
76
80
  CA_UINT64 => 0xFFFFFFFFFFFFFFFF, CA_INT64 => -1,
77
81
  }.freeze
78
82
 
83
+ # The largest vocabulary each codes data type can carry: every valid code in
84
+ # 0...k has to be representable AND distinct from the exclusion sentinel. An
85
+ # unsigned type spends its top value on the sentinel; a signed one spends -1,
86
+ # which was never a valid index, so a signed type carries one more label than
87
+ # the unsigned type of the same width. categorize picks a width by this rule
88
+ # when it builds codes; from_codes has to check it when it receives them.
89
+ MAX_LABELS = {
90
+ CA_UINT8 => 0xFF, CA_INT8 => 0x80,
91
+ CA_UINT16 => 0xFFFF, CA_INT16 => 0x8000,
92
+ CA_UINT32 => 0xFFFFFFFF, CA_INT32 => 0x80000000,
93
+ CA_UINT64 => 0xFFFFFFFFFFFFFFFF, CA_INT64 => 0x8000000000000000,
94
+ }.freeze
95
+
79
96
  class << self
80
97
  # Wrap already-dense codes + labels with no discovery — the import receiver
81
- # for a pandas Categorical or an Arrow dictionary. `codes` becomes the
82
- # Face's storage parent verbatim (zero-copy when it is a wrapped memory
83
- # view), and from_codes takes ownership of it.
98
+ # for a pandas Categorical or an Arrow dictionary. from_codes takes
99
+ # ownership of `codes`, which becomes the Face's storage parent: verbatim
100
+ # when it is an entity that needs no rewriting, so a wrapped memory view
101
+ # stays a view, and materialised when it is a view over an array the caller
102
+ # still holds, since a view owns no bytes to take ownership of.
103
+ #
104
+ # This is the one door through which an already-built encoding enters, so
105
+ # it validates rather than assumes, and it normalises before handing over:
106
+ # a cell is excluded when it arrives masked OR holds the all-ones sentinel
107
+ # (type-max for unsigned codes, -1 for signed — both the pandas / Arrow
108
+ # missing code), and every excluded cell leaves here holding the sentinel
109
+ # AND masked. Writing both matters because the two encodings have different
110
+ # readers: the axis-group kernel classifies on the code byte, the
111
+ # materialising paths classify on the mask. A categorical that carries only
112
+ # one of them answers membership two ways, silently. An Arrow dictionary
113
+ # carries its missingness in a validity bitmap with arbitrary code bytes —
114
+ # frequently 0 — so arriving masked-only is the normal import, not an edge.
84
115
  #
85
- # Excluded cells are identified by the all-ones sentinel value (type-max
86
- # for unsigned codes, -1 for signed — both the pandas / Arrow missing code)
87
- # and masked here, so the categorical is well-formed regardless of whether
88
- # the caller pre-masked. Only the mask buffer is touched; the code bytes
89
- # are left intact (so a pandas byte-reinterpret round-trips).
116
+ # #initialize marks the codes read-only, so this is also the last point at
117
+ # which they can be written; a read-only argument is copied rather than
118
+ # refused.
90
119
  # @overload from_codes(codes, labels)
91
120
  # Returns a {CACategorical} wrapping already-dense integer
92
121
  # `codes` with the given `labels`, without discovery. `codes`
93
- # becomes the Face's storage parent; excluded cells (identified
94
- # by the type-max sentinel value) are masked automatically.
122
+ # becomes the Face's storage parent, materialised first if it is
123
+ # a view. A cell that is masked or holds the type-max sentinel is
124
+ # excluded, and leaves as both.
95
125
  # @param codes [CArray] integer code storage.
96
126
  # @param labels [Array, CArray] category vocabulary indexed by
97
- # code.
127
+ # code. Must be unique, and must fit the codes data type with
128
+ # the sentinel reserved.
98
129
  # @return [CACategorical]
99
- # @raise [ArgumentError] when `codes` is not an integer CArray.
130
+ # @raise [ArgumentError] when `codes` is not an integer CArray,
131
+ # when `labels` holds duplicates or is too large for the codes
132
+ # data type, or when an unmasked code is outside `0...labels.size`
133
+ # and is not the sentinel.
100
134
  def from_codes(codes, labels)
101
135
  unless codes.is_a?(CArray) && SENTINEL.key?(codes.data_type)
102
136
  got = codes.is_a?(CArray) ? codes.data_type : codes.class
103
137
  raise ArgumentError, "from_codes: codes must be an integer CArray (got #{got})"
104
138
  end
105
- excluded = codes.eq(SENTINEL[codes.data_type])
106
- if excluded.count(true) > 0
107
- codes.mask = codes.has_mask? ? (codes.mask | excluded) : excluded
139
+
140
+ labels_arr = labels.respond_to?(:to_a) ? labels.to_a : Array(labels)
141
+ if labels_arr.uniq.size != labels_arr.size
142
+ raise ArgumentError, "from_codes: labels must be unique (got duplicates)"
143
+ end
144
+ k = labels_arr.size
145
+ sentinel = SENTINEL[codes.data_type]
146
+ max = MAX_LABELS[codes.data_type]
147
+ if k > max
148
+ raise ArgumentError,
149
+ "from_codes: #{k} labels do not fit #{CArray.data_type_name(codes.data_type)} " \
150
+ "codes, which carry at most #{max} (the top value is reserved as the " \
151
+ "exclusion sentinel); widen the codes data type"
152
+ end
153
+
154
+ # Classify on the code bytes with the mask set aside (`.value`): a
155
+ # comparison against a masked cell yields UNDEF, which would read as
156
+ # "not excluded" and let the cell through carrying a valid-looking code.
157
+ raw = codes.value
158
+ if sentinel == -1 # signed codes: -1 is the sentinel
159
+ out = raw.lt(0)
160
+ # Only test the upper bound when k is representable in the codes data
161
+ # type. At the very top of the range (k == max) no value can reach k
162
+ # anyway, and comparing against an unrepresentable literal would wrap
163
+ # and flag every cell as out of range.
164
+ out = out | raw.ge(k) if k <= max - 1
165
+ else # unsigned codes: type-max is the sentinel
166
+ out = raw.ge(k) # k <= max here, so always representable
108
167
  end
109
- new(codes, labels)
168
+ masked = codes.has_mask? ? codes.is_masked : nil
169
+ excluded = masked ? (out | masked) : out
170
+ # A masked cell may hold any byte at all — that is the CArray contract —
171
+ # so it is never corrupt, only in need of normalising. An *unmasked* cell
172
+ # holding an out-of-range code that is not the sentinel is neither a
173
+ # category nor missingness; refuse it here, where the input is still in
174
+ # the caller's hands, instead of letting it surface later as an IndexError
175
+ # from the grouping plan or as a wrong label from a decode.
176
+ corrupt = out & raw.ne(sentinel)
177
+ corrupt = corrupt & masked.not if masked
178
+ if corrupt.any
179
+ bad = raw[corrupt].to_a.uniq.sort
180
+ shown = bad.first(4).join(", ") + (bad.size > 4 ? ", ..." : "")
181
+ raise ArgumentError,
182
+ "from_codes: code#{bad.size == 1 ? "" : "s"} #{shown} outside " \
183
+ "0...#{k} for #{k} label#{k == 1 ? "" : "s"} " \
184
+ "(use #{sentinel} to exclude a cell, or mask it)"
185
+ end
186
+
187
+ # Take real ownership, then normalise so the byte reader and the mask
188
+ # reader agree from here on.
189
+ #
190
+ # A view owns no bytes — its root does, and the caller still holds that
191
+ # root. #initialize marks what it is given read-only, which would stop
192
+ # `cat.codes[i] = x` but not `root[i] = x`, so the codes could still
193
+ # change underneath a Face that is supposed to be immutable (and
194
+ # underneath the grouping plan memoised against them). Marking the root
195
+ # instead is worse: it would freeze bytes outside the window the caller
196
+ # handed over. So a view is materialised. Read-only codes are copied for
197
+ # the adjacent reason — the normalising writes need somewhere to land.
198
+ #
199
+ # An entity that needs no rewriting is adopted verbatim, which is what
200
+ # keeps a wrapped memory view a view. That buffer's producer can still
201
+ # write it; that is the borrowed-buffer bargain, and not something this
202
+ # constructor can close.
203
+ needs_mask = masked ? (excluded & masked.not).any : excluded.any
204
+ needs_byte = (excluded & raw.ne(sentinel)).any
205
+ if !codes.entity? || ((needs_mask || needs_byte) && codes.read_only?)
206
+ codes = codes.copy
207
+ end
208
+ if needs_mask || needs_byte
209
+ codes.value[excluded] = sentinel if needs_byte
210
+ codes.mask = excluded
211
+ end
212
+
213
+ new(codes, labels_arr)
110
214
  end
111
215
  end
112
216
 
113
- # codes : integer CArray, the storage parent. Excluded cells are both
114
- # masked AND store the type-max sentinel value (= the all-ones bit
115
- # pattern, which is signed -1 byte-for-byte — the pandas / Arrow
116
- # missing code). Because the Face is READONLY the two never desync,
117
- # so consumers may rely on either: the mask (CArray-native) or the
118
- # sentinel (axis-group's out-of-range skip, zero-copy export).
217
+ # codes : integer CArray, the storage parent, already normalised by the
218
+ # caller (categorize builds it that way; from_codes rewrites what it
219
+ # receives). Excluded cells are both masked AND store the type-max
220
+ # sentinel value (= the all-ones bit pattern, which is signed -1
221
+ # byte-for-byte — the pandas / Arrow missing code). Marking the codes
222
+ # read-only below keeps the two in step from here on, so consumers may
223
+ # rely on either: the mask (CArray-native) or the sentinel
224
+ # (axis-group's out-of-range skip, zero-copy export).
119
225
  # labels: Array | CArray, the vocabulary; labels[code] = category.
120
226
  # @overload initialize(codes, labels)
121
227
  # Allocates a READONLY {CACategorical} Face whose storage is
@@ -143,8 +249,9 @@ class CACategorical < CAObject
143
249
  # through views/Faces (a reshape of frozen codes is frozen) and would block
144
250
  # the grouping cache from memoising. The flag gives the same write protection
145
251
  # (mutations raise) while keeping the object non-frozen. One-way: it takes
146
- # ownership of `codes` (categorize / from_codes build or receive it, mask
147
- # already derived above); a caller keeping a mutable array must pass `.copy`.
252
+ # ownership of `codes`, which from_codes has already validated, materialised
253
+ # if it was a view, and normalised; a caller keeping a mutable entity of its
254
+ # own must pass `.copy`.
148
255
  codes.set_read_only_flag
149
256
  end
150
257
 
@@ -188,12 +295,11 @@ class CACategorical < CAObject
188
295
  other.is_a?(CACategorical) && @labels == other.labels
189
296
  end
190
297
 
191
- # Face hook: decode a per-cell code into its category label. An out-of-range
192
- # code (e.g. an unmasked external sentinel) decodes to nil rather than a
193
- # wrong category via Ruby negative indexing.
194
- def storage_to_scalar(raw)
298
+ # Face hook: decode a per-cell code into its category label, through the one
299
+ # decode every code-to-label path shares (see #label_at).
300
+ def storage_to_element(raw)
195
301
  code = raw.is_a?(String) ? raw.unpack1(UNPACK_FORMAT.fetch(parent.data_type)) : raw
196
- (code < 0 || code >= @labels.size) ? nil : @labels[code]
302
+ label_at(code)
197
303
  end
198
304
 
199
305
  # ---- category-space operations (by label, not code) -------------------
@@ -211,6 +317,27 @@ class CACategorical < CAObject
211
317
  # an all-false result.
212
318
  # @param label [Object] category to match.
213
319
  # @return [CArray]
320
+ # @overload resize(*newdim, fill_value: UNDEF)
321
+ # Returns a categorical resized to `newdim` (see {CArray#resize}).
322
+ # The new cells are excluded (UNDEF) unless `fill_value` names one of
323
+ # the labels, since a zero code would read as the first label.
324
+ # @raise [ArgumentError] when `fill_value` is not a label.
325
+ def resize (*newdim, fill_value: UNDEF)
326
+ CACategorical.from_codes(codes.resize(*newdim, fill_value: code_for_fill(fill_value)),
327
+ labels)
328
+ end
329
+
330
+ # @overload insert_block(offset, bsize, &block)
331
+ # Returns a categorical with a block inserted (see
332
+ # {CArray#insert_block}). The inserted cells are excluded (UNDEF)
333
+ # unless the block returns one of the labels.
334
+ # @raise [ArgumentError] when the block returns something that is not
335
+ # a label.
336
+ def insert_block (offset, bsize, &block)
337
+ fill = block ? code_for_fill(block.call) : UNDEF
338
+ CACategorical.from_codes(codes.insert_block(offset, bsize) { fill }, labels)
339
+ end
340
+
214
341
  def eq(label)
215
342
  codes.eq(@labels.index(label) || @labels.size)
216
343
  end
@@ -250,11 +377,11 @@ class CACategorical < CAObject
250
377
  # so `labels.zip(category_sizes.to_a)` always pairs up.
251
378
  # @return [CArray]
252
379
  def category_sizes
253
- return @_category_sizes if @_category_sizes
380
+ return @category_sizes if @category_sizes
254
381
  bc = codes.bincount
255
- out = CArray.new(bc.data_type, [@labels.size]) # new zero-fills
382
+ out = CArray.new(bc.data_type, [@labels.size], bytes: bc.bytes) # new zero-fills
256
383
  out[0...bc.elements] = bc if bc.elements > 0
257
- @_category_sizes = out
384
+ @category_sizes = out
258
385
  out
259
386
  end
260
387
 
@@ -350,10 +477,29 @@ class CACategorical < CAObject
350
477
 
351
478
  private
352
479
 
353
- # Labels for a code array (the discovery kernels skip masked cells, so the
354
- # code arrays reaching here hold real codes only).
480
+ # The one decode from a code to its label. An out-of-range code — a negative
481
+ # one, or an unmasked sentinel that reached us from outside — has no label,
482
+ # so it decodes to nil. Writing `@labels[code]` instead would read a negative
483
+ # code from the *end* of the vocabulary and hand back a real label for a cell
484
+ # that has none. Every code-to-label path goes through here rather than
485
+ # indexing @labels itself, because two decodes are two chances to disagree,
486
+ # and disagreeing is how #unique came to contradict #to_a on the same cell.
487
+ # The code a fill value stands for: UNDEF stays UNDEF, a label is its code.
488
+ def code_for_fill (value)
489
+ return UNDEF if UNDEF.equal?(value)
490
+ code = labels.index(value)
491
+ raise ArgumentError, "#{value.inspect} is not one of the labels #{labels.inspect}" unless code
492
+ code
493
+ end
494
+
495
+ def label_at (code)
496
+ (code < 0 || code >= @labels.size) ? nil : @labels[code]
497
+ end
498
+
499
+ # Labels for a code array. The discovery kernels skip masked cells, so what
500
+ # reaches here is real codes — but it is decoded like any other code.
355
501
  def labels_for (code_array)
356
- CA_OBJECT(code_array.to_a.map { |c| @labels[c] })
502
+ CA_OBJECT(code_array.to_a.map { |c| label_at(c) })
357
503
  end
358
504
 
359
505
  # This categorical's cells as their labels; a masked cell stays masked.
@@ -424,13 +570,11 @@ class CACategorical < CAObject
424
570
  # addresses that order the cells by category (excluded cells last).
425
571
  # @return [CArray]
426
572
  def sort_addr
427
- return @_sort_addr if @_sort_addr
573
+ return @sort_addr if @sort_addr
428
574
  n = elements
429
575
  k = @labels.size
430
- cs = category_sizes # per-category counts
431
- nvalid = cs.sum
432
- cur = CArray.int64(k > 0 ? k : 0) # segment starts, consumed as cursor
433
- cur[1..-1] = cs.cumsum.int64[0..-2] if k > 1
576
+ nvalid = category_sizes.sum
577
+ cur = reduceat_index.copy # segment starts, consumed as cursor
434
578
  flat = codes.reshape(n)
435
579
  seq = CArray.int64(n).seq! # source indices, scattered as payload
436
580
  out = CArray.int64(n)
@@ -449,7 +593,7 @@ class CACategorical < CAObject
449
593
  excluded = excluded.or(flat.is_masked) if flat.has_mask?
450
594
  out[nvalid..-1] = seq[excluded] # excluded cells, source order
451
595
  end
452
- @_sort_addr = out
596
+ @sort_addr = out
453
597
  out
454
598
  end
455
599
 
@@ -464,17 +608,26 @@ class CACategorical < CAObject
464
608
  # offsets aligned to {#labels}. Pair with {#category_sizes} for lengths.
465
609
  # @return [CArray]
466
610
  def reduceat_index
467
- return @_reduceat_index if @_reduceat_index
468
- k = @labels.size
469
- out = CArray.int64(k > 0 ? k : 0)
470
- if k > 1
471
- out[0] = 0
472
- out[1..-1] = category_sizes.cumsum.int64[0..-2]
473
- elsif k == 1
474
- out[0] = 0
611
+ return @reduceat_index if @reduceat_index
612
+ # Every boundary but the last; the copy makes it an entity the
613
+ # reduceat kernels can read directly.
614
+ @reduceat_index = CArray.segment_offsets(lengths: category_sizes)[0...-1].copy
615
+ end
616
+
617
+ # @overload categorize(labels: nil, sort_labels: false)
618
+ # Returns a {CACategorical} keyed on the labels, as categorizing any
619
+ # key array does: the labels that occur, in first-appearance order (or
620
+ # sorted with `sort_labels:`), or the fixed `labels:` vocabulary. A
621
+ # category of `self` with no cells does not carry over; a masked cell
622
+ # is excluded.
623
+ # @return [CACategorical]
624
+ def categorize (labels: nil, sort_labels: false)
625
+ if labels || sort_labels
626
+ return label_values.categorize(labels: labels, sort_labels: sort_labels)
475
627
  end
476
- @_reduceat_index = out
477
- out
628
+ # Distinctness rides the codes; only the k labels cross the surface.
629
+ recoded = codes.categorize
630
+ CACategorical.from_codes(recoded.codes, recoded.labels.map { |c| label_at(c) })
478
631
  end
479
632
 
480
633
  # @overload inspect
@@ -512,6 +665,24 @@ class CArray
512
665
  # @return [CACategorical]
513
666
  # @raise [ArgumentError] when explicit `labels` contain
514
667
  # duplicates.
668
+ # @raise [CArray::DataTypeError] when `sort_labels: true` discovers
669
+ # the labels of a complex array, which have no order.
670
+ # The order sort_labels: puts two labels in: as sort orders the cells of an
671
+ # array, a Float NaN after every other value; two labels <=> cannot order
672
+ # are refused, by name.
673
+ private def label_order(a, b)
674
+ na = a.is_a?(Float) && a.nan?
675
+ nb = b.is_a?(Float) && b.nan?
676
+ return (na ? 1 : 0) - (nb ? 1 : 0) if na || nb
677
+ c = a <=> b
678
+ if c.nil?
679
+ raise ArgumentError,
680
+ "categorize(sort_labels: true): cannot order the labels " \
681
+ "#{a.inspect} and #{b.inspect}"
682
+ end
683
+ c
684
+ end
685
+
515
686
  def categorize(labels: nil, sort_labels: false)
516
687
  # Automatic appearance-order vocabulary: one linear pass (C
517
688
  # __factorize_appearance__) returns both codes and levels directly, over the
@@ -519,12 +690,26 @@ class CArray
519
690
  # lane). Distinctness is the hash-key judgement shared with the discovery
520
691
  # family: Float NaN collapses to one category and -0.0 == +0.0, while mixed
521
692
  # Integer / Float keys stay distinct (eql?, so 1 and 1.0 are separate
522
- # categories). The discovery path below is reserved for sort_labels (which
523
- # reorders the vocabulary, desyncing the appearance-order codes), an explicit
524
- # labels list, and the data types the factorize kernel does not take (complex).
525
- if labels.nil? && !sort_labels && (integer? || float? || object? || fixlen? || boolean?)
693
+ # categories). Complex values are distinct by value with the same NaN and
694
+ # -0.0 rules, and have no order, so sort_labels refuses them. The discovery
695
+ # path below is reserved for an explicit labels list.
696
+ if labels.nil? && sort_labels && complex?
697
+ raise CArray::DataTypeError,
698
+ "categorize(sort_labels: true): a #{data_type_name} value has no " \
699
+ "order (complex numbers are not ordered); without sort_labels: the " \
700
+ "labels are in first-appearance order"
701
+ end
702
+ if labels.nil? && (integer? || float? || complex? || object? || fixlen? || boolean?)
526
703
  codes, levels = __factorize_appearance__
527
- return CACategorical.from_codes(codes, levels.to_a)
704
+ levels = levels.to_a
705
+ return CACategorical.from_codes(codes, levels) unless sort_labels
706
+ # Sort the k levels as Ruby sorts them, then renumber every code through
707
+ # the rank in one gather. The sentinel of an excluded cell is out of the
708
+ # rank's range, so project makes it UNDEF and from_codes writes it back.
709
+ order = (0...levels.size).sort { |i, j| label_order(levels[i], levels[j]) }
710
+ rank = CArray.new(codes.data_type, [levels.size])
711
+ order.each_with_index { |old, new_code| rank[old] = new_code }
712
+ return CACategorical.from_codes(rank.project(codes), order.map { |i| levels[i] })
528
713
  end
529
714
 
530
715
  if labels.nil?
@@ -533,7 +718,7 @@ class CArray
533
718
  # masked keys stay excluded), so the non-masked cells are the levels.
534
719
  # Only the final list is Ruby, since labels are Ruby objects.
535
720
  labels_arr = mask_duplicates[:is_not_masked].to_a
536
- labels_arr.sort! if sort_labels
721
+ labels_arr.sort! { |a, b| label_order(a, b) } if sort_labels
537
722
  else
538
723
  labels_arr = labels.respond_to?(:to_a) ? labels.to_a : Array(labels)
539
724
  if labels_arr.uniq.size != labels_arr.size