carray 3.0.4 → 3.0.5

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 (111) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +205 -0
  3. data/carray.gemspec +1 -1
  4. data/ext/ca_array_pool.c +25 -1
  5. data/ext/ca_axis_dispatch.c +13 -20
  6. data/ext/ca_categorical_iterator.c +27 -20
  7. data/ext/ca_compare.c +4 -4
  8. data/ext/ca_composite_dispatch.c +9 -9
  9. data/ext/ca_kernel_iterator.c +6 -6
  10. data/ext/ca_kernel_iterator.h +112 -83
  11. data/ext/ca_obj_array.c +69 -19
  12. data/ext/ca_obj_bincmp.c +1 -0
  13. data/ext/ca_obj_binop.c +1 -0
  14. data/ext/ca_obj_bitarray.c +5 -1
  15. data/ext/ca_obj_bitfield.c +58 -43
  16. data/ext/ca_obj_byte_swap.c +2 -1
  17. data/ext/ca_obj_const_string.c +15 -3
  18. data/ext/ca_obj_face.c +42 -1
  19. data/ext/ca_obj_field.c +13 -3
  20. data/ext/ca_obj_grid.c +52 -5
  21. data/ext/ca_obj_monop.c +46 -10
  22. data/ext/ca_obj_object.c +64 -12
  23. data/ext/ca_obj_record.c +3 -0
  24. data/ext/ca_obj_reduce.c +4 -0
  25. data/ext/ca_obj_refer.c +18 -9
  26. data/ext/ca_obj_roll.c +26 -6
  27. data/ext/ca_obj_select.c +34 -5
  28. data/ext/ca_obj_shift.c +31 -7
  29. data/ext/ca_obj_stack.c +1 -0
  30. data/ext/ca_obj_stride.c +28 -19
  31. data/ext/ca_obj_string.c +11 -2
  32. data/ext/ca_obj_tile.c +27 -7
  33. data/ext/ca_obj_transpose.c +8 -4
  34. data/ext/ca_obj_triop.c +3 -0
  35. data/ext/ca_obj_window.c +79 -50
  36. data/ext/ca_op_ipower.c +4 -4
  37. data/ext/ca_sort_kernels.h +6 -3
  38. data/ext/carray.h +33 -0
  39. data/ext/carray_access.c +16 -3
  40. data/ext/carray_attribute.c +24 -0
  41. data/ext/carray_cast.c +243 -82
  42. data/ext/carray_cast_func.rb +56 -26
  43. data/ext/carray_conversion.c +2 -2
  44. data/ext/carray_core.c +3 -0
  45. data/ext/carray_count.c +6 -0
  46. data/ext/carray_element.c +9 -4
  47. data/ext/carray_factorize.c +20 -20
  48. data/ext/carray_generate.c +8 -2
  49. data/ext/carray_histogram.c +1 -1
  50. data/ext/carray_index_classifier.c +4 -4
  51. data/ext/carray_internal.h +12 -0
  52. data/ext/carray_kernels_bincmp.c +168 -168
  53. data/ext/carray_kernels_binop.c +900 -900
  54. data/ext/carray_kernels_init.c +4 -4
  55. data/ext/carray_kernels_moncmp.c +96 -96
  56. data/ext/carray_kernels_monop.c +1984 -1984
  57. data/ext/carray_kernels_reduce_aggregate.c +1733 -1733
  58. data/ext/carray_kernels_reduce_cumulative.c +642 -642
  59. data/ext/carray_kernels_reduce_extreme.c +1150 -1150
  60. data/ext/carray_kernels_reduce_variance.c +258 -258
  61. data/ext/carray_kernels_scan.c +128 -128
  62. data/ext/carray_kernels_search.c +340 -340
  63. data/ext/carray_kernels_sort.c +295 -304
  64. data/ext/carray_kernels_triop.c +114 -114
  65. data/ext/carray_key_windows.c +4 -0
  66. data/ext/carray_mask.c +45 -22
  67. data/ext/carray_memory_view.c +2 -2
  68. data/ext/carray_operator.c +5 -5
  69. data/ext/carray_order.c +227 -8
  70. data/ext/carray_random.c +18 -18
  71. data/ext/carray_scatter.c +24 -7
  72. data/ext/carray_slab.c +32 -9
  73. data/ext/carray_sort.c +224 -16
  74. data/ext/carray_sort_kernel.c +5 -7
  75. data/ext/carray_struct.c +35 -43
  76. data/ext/carray_time_text.c +4 -2
  77. data/ext/carray_utils.c +63 -21
  78. data/ext/mkkernel.rb +210 -115
  79. data/ext/version.h +4 -4
  80. data/lib/carray/attribute.rb +17 -4
  81. data/lib/carray/basics.rb +27 -7
  82. data/lib/carray/categorical.rb +30 -16
  83. data/lib/carray/categorical_iterator.rb +6 -6
  84. data/lib/carray/const_string.rb +10 -0
  85. data/lib/carray/data_type_extension.rb +8 -1
  86. data/lib/carray/fixlen_string.rb +41 -2
  87. data/lib/carray/frame/column_verbs.rb +4 -3
  88. data/lib/carray/frame/concat.rb +17 -3
  89. data/lib/carray/frame/frame.rb +6 -7
  90. data/lib/carray/frame/group.rb +100 -13
  91. data/lib/carray/frame/io.rb +3 -1
  92. data/lib/carray/fuse_source.rb +31 -2
  93. data/lib/carray/lazy.rb +35 -0
  94. data/lib/carray/methods/is_in.rb +8 -1
  95. data/lib/carray/segment_iterator.rb +12 -8
  96. data/lib/carray/serialize.rb +19 -2
  97. data/lib/carray/stack.rb +15 -12
  98. data/lib/carray/string.rb +8 -0
  99. data/lib/carray/string_operation_extension.rb +13 -5
  100. data/lib/carray/struct_builder.rb +5 -3
  101. data/lib/carray/time.rb +107 -68
  102. data/yard-stubs/ca_obj_transpose.rb +5 -4
  103. data/yard-stubs/carray_attribute.rb +3 -1
  104. data/yard-stubs/carray_cast.rb +2 -0
  105. data/yard-stubs/carray_conversion.rb +5 -1
  106. data/yard-stubs/carray_element.rb +7 -6
  107. data/yard-stubs/carray_mask.rb +8 -3
  108. data/yard-stubs/carray_math.rb +14 -4
  109. data/yard-stubs/carray_order.rb +100 -4
  110. data/yard-stubs/carray_scatter.rb +7 -5
  111. metadata +1 -1
data/ext/version.h CHANGED
@@ -1,6 +1,6 @@
1
- #define CA_VERSION "3.0.4"
2
- #define CA_VERSION_CODE 30004
1
+ #define CA_VERSION "3.0.5"
2
+ #define CA_VERSION_CODE 30005
3
3
  #define CA_VERSION_MAJOR 3
4
4
  #define CA_VERSION_MINOR 0
5
- #define CA_VERSION_TEENY 4
6
- #define CA_VERSION_DATE "2026/10/19"
5
+ #define CA_VERSION_TEENY 5
6
+ #define CA_VERSION_DATE "2026/10/20"
@@ -33,14 +33,17 @@
33
33
  # parent's cells, so the parent's attributes do not describe them.
34
34
  # `a.lazy` itself (CALazyMarker) is a view of `a` and walks through,
35
35
  # and so does a lazy type conversion (the cast lane `promote_list`
36
- # and `wrap_readonly` build), for the reason `to_type` carries them.
36
+ # and `wrap_readonly` build), for the reason `to_type` carries them,
37
+ # and `swap_bytes`: like `refer`, `bitarray` and `bitfield`, a view
38
+ # that reads the same cells another way carries them.
37
39
  # A view of a Face shows the attributes of the Face it was taken
38
40
  # from (the walk goes to that Face, which the view's own parent
39
41
  # chain skips).
40
42
  # A view of several arrays (CAStack / CAMeld, so `stack`, `meld` and
41
43
  # `montage`) shows, below its own @attr, the keys every one of its
42
44
  # arrays shows with the same value; a key they disagree on, or one
43
- # some of them lack, is not shown.
45
+ # some of them lack, is not shown. An array with no cells is left out
46
+ # of that count, unless all of them are empty.
44
47
  #
45
48
  # Stored values are frozen copies, nested Arrays and Hashes included, so
46
49
  # an attribute changes only through set_attr / set_attrs: a value read
@@ -58,7 +61,9 @@
58
61
  # `categorize`, a CAFrame `cast` / `parse_to_time` / `to_time`) carries
59
62
  # them. Arithmetic and reductions do not -- the result is a different
60
63
  # quantity -- including those that pick a value (`first`, `last`, `min`,
61
- # `max`, `unique`). A CAFrame index built from a key column's values
64
+ # `max`, `unique`) and the element-wise ones that keep the unit (`abs`,
65
+ # `round`, `clip`): an attribute does not say which operations leave it
66
+ # true, so none carries it. Set it on the result again. A CAFrame index built from a key column's values
62
67
  # (group and pivot) shows the key column's attributes.
63
68
 
64
69
  class CArray
@@ -235,7 +240,12 @@ class CArray
235
240
  end
236
241
 
237
242
  # The attributes every array in `list` shows with the same value.
243
+ # An array with no cells holds no values for the attributes to describe,
244
+ # so it has no say (unless every one is empty), as CAFrame leaves a frame
245
+ # with no rows out of a meld.
238
246
  def self.attrs_shared_by (list)
247
+ with_cells = list.reject { |a| a.elements == 0 }
248
+ list = with_cells unless with_cells.empty?
239
249
  first, *rest = list.map(&:attrs)
240
250
  return {} if first.nil?
241
251
  first.select { |k, v| rest.all? { |h| h.key?(k) && h[k] == v } }
@@ -243,7 +253,10 @@ class CArray
243
253
  private_class_method :attrs_shared_by
244
254
 
245
255
  def attr_computing_view? (ca)
246
- return ca.__op_id__ < CAMonOp::CAST_BASE if ca.is_a?(CAMonOp)
256
+ if ca.is_a?(CAMonOp)
257
+ op = ca.__op_id__
258
+ return op < CAMonOp::CAST_BASE && op != CAMonOp::OP_BYTE_SWAP
259
+ end
247
260
  ca.is_a?(CABinOp) || ca.is_a?(CATriOp) ||
248
261
  ca.is_a?(CAMonCmp) || ca.is_a?(CABinCmp)
249
262
  end
data/lib/carray/basics.rb CHANGED
@@ -117,13 +117,15 @@ class CArray
117
117
  end
118
118
 
119
119
  # @overload drop_axis
120
- # Returns a view of `self` with every size-1 axis dropped.
120
+ # Returns a view of `self` with every size-1 axis dropped. When every
121
+ # axis has size 1, one axis of size 1 is kept.
121
122
  # @return [CArray] view with reduced `ndim`.
122
123
  def drop_axis
123
124
  if ndim == 1
124
125
  return self[]
125
126
  else
126
127
  newdim = shape.reject{|x| x == 1 }
128
+ newdim = [1] if newdim.empty? # every axis has size 1: keep one
127
129
  return ( ndim != newdim.size ) ? reshape(*newdim) : self[]
128
130
  end
129
131
  end
@@ -189,7 +191,13 @@ class CArray
189
191
  # @param src [CArray] source array.
190
192
  # @return [self]
191
193
  # @raise [ArgumentError] when `offset.length != self.ndim`.
194
+ # @raise [FrozenError] when `self` is frozen.
192
195
  def paste (offset, src)
196
+ # The write goes through a window view, which is not frozen itself:
197
+ # refuse a frozen self here, as a write into it directly is refused.
198
+ if frozen?
199
+ raise FrozenError.new("can't modify frozen #{self.class}: #{inspect}", receiver: self)
200
+ end
193
201
  raise ArgumentError, "offset length must equal ndim" if offset.length != ndim
194
202
  # A zero-length source covers no cell, so there is nothing to write --
195
203
  # and the empty range it would ask for is not a window CAWindow can
@@ -310,8 +318,7 @@ class CArray
310
318
  # @overload clip(min, max = nil, fill_value = nil, lfill: nil, ufill: nil)
311
319
  # Returns `self` with every element clamped to `[min, max]`.
312
320
  #
313
- # Either bound may be `nil` for a one-sided clip; that side
314
- # dispatches to the `pmax` / `pmin` binop kernels. When
321
+ # Either bound may be `nil` for a one-sided clip. When
315
322
  # `fill_value` (or `lfill` / `ufill`) is given, out-of-range
316
323
  # cells are replaced by the fill instead of clamped -- pass
317
324
  # `UNDEF` to mask that end. `fill_value` is sugar for symmetric
@@ -320,6 +327,8 @@ class CArray
320
327
  #
321
328
  # `min` may not exceed `max` (in any cell, when a bound is an
322
329
  # array); equal bounds give every unmasked cell that value.
330
+ # A NaN cell of `self` stays NaN, one-sided or two-sided (unlike
331
+ # `pmax` / `pmin`, which answer the other operand for it).
323
332
  # A NaN in a bound array leaves that cell unbounded on that side,
324
333
  # as `nil` does for the whole side. A bound is never UNDEF or
325
334
  # masked: a missing bound is not read as either, so fill it first.
@@ -373,19 +382,30 @@ class CArray
373
382
 
374
383
  if lfill.nil? && ufill.nil?
375
384
  return clamp_between(min, max) if !min.nil? && !max.nil?
376
- return pmax(min) if max.nil?
377
- return pmin(max)
385
+ return clamp_below(min) if max.nil?
386
+ return clamp_above(max)
378
387
  end
379
388
 
380
389
  # A side without a fill still clamps.
381
390
  out = self.copy
382
- out = out.pmax(min) if !min.nil? && lfill.nil?
383
- out = out.pmin(max) if !max.nil? && ufill.nil?
391
+ out = out.clamp_below(min) if !min.nil? && lfill.nil?
392
+ out = out.clamp_above(max) if !max.nil? && ufill.nil?
384
393
  out[self.lt(min)] = lfill unless min.nil? || lfill.nil?
385
394
  out[self.gt(max)] = ufill unless max.nil? || ufill.nil?
386
395
  out
387
396
  end
388
397
 
398
+ # A one-sided clip of a float array passes a NaN cell through, as the
399
+ # two-sided one does: the open side is a NaN bound, which leaves it
400
+ # unbounded. pmax / pmin would answer the bound for a NaN cell.
401
+ protected def clamp_below (min)
402
+ float? ? clamp_between(min, Float::NAN) : pmax(min)
403
+ end
404
+
405
+ protected def clamp_above (max)
406
+ float? ? clamp_between(Float::NAN, max) : pmin(max)
407
+ end
408
+
389
409
  # `contains` was retired in favour of {#is_in} (value-hash membership).
390
410
  # `a.contains(v1, v2)` -> `a.is_in([v1, v2])`;
391
411
  # `a.contains(v1, v2, axis: k)` -> `a.is_in([v1, v2]).any(axis: k)`.
@@ -299,7 +299,7 @@ class CACategorical < CAObject
299
299
  # `codes[i] = x` raises; use `codes.copy` for a mutable copy.
300
300
  # @return [CArray]
301
301
  def codes
302
- parent
302
+ face_storage
303
303
  end
304
304
 
305
305
  # Face hook: carry the vocabulary across lifted views (slice / reshape / …).
@@ -328,10 +328,17 @@ class CACategorical < CAObject
328
328
  # Face hook: decode a per-cell code into its category label, through the one
329
329
  # decode every code-to-label path shares (see #label_at).
330
330
  def storage_to_element(raw)
331
- code = raw.is_a?(String) ? raw.unpack1(UNPACK_FORMAT.fetch(parent.data_type)) : raw
331
+ code = raw.is_a?(String) ? raw.unpack1(UNPACK_FORMAT.fetch(face_storage.data_type)) : raw
332
332
  label_at(code)
333
333
  end
334
334
 
335
+ # Face hook: a value written into a cell (a fill_value of shift and
336
+ # window) is a label, and the cell holds its code.
337
+ # @raise [ArgumentError] if the value is not one of the labels.
338
+ def element_to_storage (value)
339
+ code_for_fill(value)
340
+ end
341
+
335
342
  # ---- category-space operations (by label, not code) -------------------
336
343
  #
337
344
  # The element value of a categorical IS its category, so these specialise
@@ -389,7 +396,14 @@ class CACategorical < CAObject
389
396
  # (0 for an unknown label).
390
397
  # @param label [Object] category to count.
391
398
  # @return [Integer]
392
- def count(label)
399
+ # @overload count
400
+ # Returns the number of cells in some category (not masked), as
401
+ # {CArray#count} with no value does.
402
+ # @return [Integer]
403
+ def count(*args)
404
+ return count_not_masked if args.empty?
405
+ raise ArgumentError, "count: wrong number of arguments (given #{args.size}, expected 0..1)" if args.size > 1
406
+ label = args[0]
393
407
  code = @labels.index(label)
394
408
  code ? codes.count(code) : 0
395
409
  end
@@ -400,7 +414,7 @@ class CACategorical < CAObject
400
414
  #
401
415
  # Memoised: the codes are read-only (immutable storage), so the counts are a
402
416
  # pure function of the categorical and stay valid for its lifetime. The same
403
- # cached array backs {#reduceat_index} and {#sort_addr}, so a wide aggregate
417
+ # cached array backs {#reduceat_index} and {#group_addr}, so a wide aggregate
404
418
  # over the same categorical pays the count once. (Do not mutate the returned
405
419
  # array — it is shared; take `.copy` for a scratch buffer.) A derived-view /
406
420
  # composite categorical caches too: read-only rides from the codes as a flag,
@@ -543,7 +557,7 @@ class CACategorical < CAObject
543
557
  # The code a fill value stands for: UNDEF stays UNDEF, a label is its code.
544
558
  def code_for_fill (value)
545
559
  return UNDEF if UNDEF.equal?(value)
546
- code = labels.index(value)
560
+ code = label_position(value)
547
561
  raise ArgumentError, "#{value.inspect} is not one of the labels #{labels.inspect}" unless code
548
562
  code
549
563
  end
@@ -583,27 +597,27 @@ class CACategorical < CAObject
583
597
  # two accessors are the building blocks for that other engine — lay the data
584
598
  # out as category-contiguous blocks, then select per block.
585
599
  #
586
- # The grouping plan — sort_addr (the counting sort, the dominant cost) plus
600
+ # The grouping plan — group_addr (the counting sort, the dominant cost) plus
587
601
  # reduceat_index and category_sizes — depends only on the codes, which are
588
602
  # read-only. It is therefore memoised (a pure function of the categorical): the
589
603
  # first grouping access builds it and every later one reuses it. This is what
590
604
  # lets a wide aggregate (many payload columns over one classifier) or a
591
605
  # CACategoricalIterator run the counting sort once, not once per column /
592
606
  # iterator. Only the plan is cached; the payload-dependent grouped copy
593
- # (value.reshape(n)[sort_addr]) is rebuilt per column (it varies with value).
607
+ # (value.reshape(n)[group_addr]) is rebuilt per column (it varies with value).
594
608
 
595
- # Force-build (and cache) the whole grouping plan up front — sort_addr,
609
+ # Force-build (and cache) the whole grouping plan up front — group_addr,
596
610
  # reduceat_index, category_sizes — for prepare-ahead use before a batch of
597
611
  # groupings. Lazy building already covers correctness; this is the explicit
598
612
  # "pay the counting sort now" handle (e.g. right after `df.group_by(col)`).
599
613
  # Returns self so it chains.
600
614
  # @overload build_grouping
601
- # Eagerly builds and caches the grouping plan (sort_addr / reduceat_index
615
+ # Eagerly builds and caches the grouping plan (group_addr / reduceat_index
602
616
  # / category_sizes). Optional — the plan is built lazily on first grouping
603
617
  # access — but useful to pay the counting sort once ahead of a batch.
604
618
  # @return [self]
605
619
  def build_grouping
606
- sort_addr # pulls category_sizes; reduceat_index shares category_sizes
620
+ group_addr # pulls category_sizes; reduceat_index shares category_sizes
607
621
  reduceat_index
608
622
  self
609
623
  end
@@ -617,26 +631,26 @@ class CACategorical < CAObject
617
631
  # order. The first {#category_sizes}.sum addresses are exactly the classified
618
632
  # cells in category order — CACategoricalIterator's permutation. For an N-D
619
633
  # categorical the addresses are into the raveled storage, so
620
- # `value.reshape(elements)[cat.sort_addr]` produces the contiguous blocks.
634
+ # `value.reshape(elements)[cat.group_addr]` produces the contiguous blocks.
621
635
  # Memoised (see the grouping-plan note above): the counting sort is the
622
636
  # dominant grouping cost, so it is computed once and shared by every payload
623
637
  # column and by CACategoricalIterator, and kept for the categorical's
624
638
  # lifetime (an int64 per cell). Do not mutate the returned array.
625
- # @overload sort_addr
639
+ # @overload group_addr
626
640
  # Returns a length-{#elements} integer CArray of flat storage
627
641
  # addresses that order the cells by category (excluded cells last).
628
642
  # @return [CArray]
629
- def sort_addr
630
- return @sort_addr if @sort_addr
643
+ def group_addr
644
+ return @group_addr if @group_addr
631
645
  out = codes.reshape(elements).__send__(:addresses_by_category,
632
646
  reduceat_index.copy, @labels.size,
633
647
  category_sizes.sum)
634
- @sort_addr = out
648
+ @group_addr = out
635
649
  out
636
650
  end
637
651
 
638
652
  # Segment start offsets into the category-contiguous layout produced by
639
- # {#sort_addr}: reduceat_index[c] is where category c's block begins, so
653
+ # {#group_addr}: reduceat_index[c] is where category c's block begins, so
640
654
  # block c spans `reduceat_index[c] ... reduceat_index[c] + category_sizes[c]`.
641
655
  # Empty categories repeat the following start (a zero-width block). Length k,
642
656
  # aligned to #labels; pair with {#category_sizes} for the block lengths. On
@@ -15,7 +15,7 @@
15
15
  # grp.median # order statistics share the same surface
16
16
  # grp.stddev
17
17
  #
18
- # This is the consumer of the sort_addr / reduceat_index foundation on
18
+ # This is the consumer of the group_addr / reduceat_index foundation on
19
19
  # CACategorical. Order statistics (median / percentile / quantile) cannot be
20
20
  # scattered — they need every value of a group held together — so the values
21
21
  # are materialized into contiguous blocks once, and every reduction (monoid or
@@ -82,7 +82,7 @@ class CACategoricalIterator < CASegmentIterator
82
82
  # once and shared by every payload column and iterator — see
83
83
  # CACategorical's grouping-plan note). The plan gives the segment STARTS
84
84
  # (reduceat_index) and the group-major permutation (perm[slot] = source index
85
- # at that grouped slot, = the valid prefix of sort_addr); gathering value
85
+ # at that grouped slot, = the valid prefix of group_addr); gathering value
86
86
  # through perm is the only per-column work. Excluded cells (masked or
87
87
  # out-of-vocabulary code) are absent from perm, so they never join a group;
88
88
  # the value mask rides the gather into the grouped copy.
@@ -109,7 +109,7 @@ class CACategoricalIterator < CASegmentIterator
109
109
  @offsets = cat.reduceat_index # cached segment STARTS (int64[k])
110
110
  @bounds = CArray.segment_offsets(lengths: @elements) # every boundary (int64[k+1])
111
111
  # The group-major source indices (#perm) are taken from the cached
112
- # sort_addr when first asked for.
112
+ # group_addr when first asked for.
113
113
  @nvalid = nvalid
114
114
  @codes = cat.codes.reshape(cat.elements) # flat codes view (map re-walk / weights)
115
115
  # Gather value into category-contiguous order via the cached permutation
@@ -316,12 +316,12 @@ class CACategoricalIterator < CASegmentIterator
316
316
 
317
317
  # Both are built with the grouping plan, and only on the flat path; the
318
318
  # per-fiber form has neither and must not build a segment's.
319
- # Group-major source indices = the valid prefix of the cached sort_addr.
319
+ # Group-major source indices = the valid prefix of the cached group_addr.
320
320
  # With no classified cell the prefix is empty (and slicing a length-0
321
- # sort_addr would be out of range), so take the empty permutation directly.
321
+ # group_addr would be out of range), so take the empty permutation directly.
322
322
  def perm
323
323
  if @nvalid && ! @perm
324
- @perm = @nvalid > 0 ? @cat.sort_addr[0...@nvalid] : CArray.int64(0)
324
+ @perm = @nvalid > 0 ? @cat.group_addr[0...@nvalid] : CArray.int64(0)
325
325
  end
326
326
  @perm
327
327
  end
@@ -92,6 +92,16 @@ class CAConstString
92
92
  # override the L1 generics where present.
93
93
  include CArray::StringOperationMixin
94
94
 
95
+ # Face hook: a value written into a cell (a fill_value of shift and
96
+ # window). A cell is a byte range of the shared buffer, which has no room
97
+ # for a String it does not hold, so only UNDEF can be written.
98
+ # @raise [ArgumentError] for any value but UNDEF.
99
+ def element_to_storage (value)
100
+ raise ArgumentError,
101
+ "a CAConstString can't hold a new String (its strings share one " \
102
+ "buffer); use UNDEF, or to_string first"
103
+ end
104
+
95
105
  # ---- ordering family ---------------------------------------------------
96
106
  #
97
107
  # A storage cell is a `(start, end)` byte range into the shared buffer, so
@@ -164,8 +164,15 @@ class CArray
164
164
  # midpoints go the same way as numpy's integer linspace).
165
165
  float_out = CArray.new(CA_FLOAT64, [n]).span(x1.to_f..x2.to_f)
166
166
  return float_out if data_type == CA_FLOAT64
167
+ # to_type masks a cell the type cannot hold; linspace refuses it
168
+ # instead, as arange does.
167
169
  # coerce: the type the caller asked for
168
- float_out.floor.to_type(data_type)
170
+ out = float_out.floor.to_type(data_type)
171
+ if out.any_masked?
172
+ raise RangeError,
173
+ "linspace: #{x1.inspect}..#{x2.inspect} is out of range for #{out.data_type_name}"
174
+ end
175
+ out
169
176
  end
170
177
 
171
178
  # @overload arange(stop)
@@ -32,9 +32,11 @@ class CArray
32
32
  # zero-copy, other string-bearing arrays materialise, numeric is rejected.
33
33
  # @overload fixlen_string(values, bytes: nil, truncate: :error)
34
34
  # @param values [Array<String, nil>] source values.
35
- # @param bytes [Integer, nil] slot width; defaults to the max bytesize.
35
+ # @param bytes [Integer, nil] slot width, 1 or more; defaults to the
36
+ # max bytesize (1 when every string is empty).
36
37
  # @param truncate [Symbol] `:error` or `:silent`.
37
38
  # @return [CAFixlenString]
39
+ # @raise [ArgumentError] when `bytes` is given below 1.
38
40
  # @overload fixlen_string(ca, bytes: nil, truncate: :error)
39
41
  # @param ca [CArray] a String Face, CA_OBJECT, or raw CA_FIXLEN array.
40
42
  # @return [CAFixlenString]
@@ -63,8 +65,11 @@ class CArray
63
65
  values = arg.to_a
64
66
  end
65
67
 
68
+ unless bytes.nil? || (bytes.is_a?(Integer) && bytes >= 1)
69
+ raise ArgumentError, "CArray.fixlen_string: invalid bytes: #{bytes.inspect} (a cell needs bytes: of 1 or more)"
70
+ end
66
71
  width = bytes || values.compact.map { |s| s.to_s.bytesize }.max || 1
67
- width = 1 if width < 1
72
+ width = 1 if width < 1 # every string empty: the narrowest cell
68
73
 
69
74
  if truncate == :error
70
75
  values.each_with_index do |s, i|
@@ -98,4 +103,38 @@ class CAFixlenString
98
103
 
99
104
  include CArray::StringOperationMixin
100
105
  include CArray::StringOperationMixin::Mutable
106
+
107
+ # ---- order family beyond the fixlen kernels --------------------------
108
+ #
109
+ # min / max / sort run on the cells' bytes. The members those kernels do
110
+ # not cover answer here: minmax as the pair of them, and the running
111
+ # extremes through the strings ({#to_string}), as CAString answers them,
112
+ # written back at this width.
113
+
114
+ # @return [Array(String, String), Array(CAFixlenString, CAFixlenString)]
115
+ # `[min, max]`, with the keywords of {CArray#min} / {CArray#max}.
116
+ def minmax (*args, **kw)
117
+ [min(*args, **kw), max(*args, **kw)]
118
+ end
119
+
120
+ # @return [CAFixlenString] the running maximum, as {CArray#cummax}.
121
+ def cummax (*args, **kw)
122
+ to_string.cummax(*args, **kw).to_fixlen_string(bytes: bytes)
123
+ end
124
+
125
+ # @return [CAFixlenString] the running minimum, as {CArray#cummin}.
126
+ def cummin (*args, **kw)
127
+ to_string.cummin(*args, **kw).to_fixlen_string(bytes: bytes)
128
+ end
129
+
130
+ # A String has no distance, so there is no nearest one; refused in the
131
+ # words CAString uses.
132
+ def search_nearest (*args, **kw)
133
+ to_string.search_nearest(*args, **kw)
134
+ end
135
+
136
+ # @see #search_nearest
137
+ def search_nearest_addr (*args, **kw)
138
+ to_string.search_nearest_addr(*args, **kw)
139
+ end
101
140
  end
@@ -30,18 +30,19 @@ class CAFrame
30
30
  next if key_names.include?(name)
31
31
  col = @frame[name]
32
32
  next unless column_has?(col, verb, args, keywords)
33
- cols[name] = yield(col)
33
+ cols[name] = yield(col, name)
34
34
  end
35
35
  return cols
36
36
  end
37
37
  results = names.map do |name|
38
- col = @frame[@frame.__send__(:column_key, name, verb)]
38
+ key = @frame.__send__(:column_key, name, verb)
39
+ col = @frame[key]
39
40
  unless column_has?(col, verb, args, keywords)
40
41
  raise CArray::DataTypeError,
41
42
  "#{verb}: column #{name.inspect} " \
42
43
  "(#{CArray::Inspector.type_label(col)}) has no #{verb}"
43
44
  end
44
- yield(col)
45
+ yield(col, key)
45
46
  end
46
47
  results.size == 1 ? results.first : results
47
48
  end
@@ -20,9 +20,9 @@ class CAFrame
20
20
  # preserved: writes to the result flow back to whichever input frame owns
21
21
  # the target segment, and vice versa).
22
22
  #
23
- # Per-column +data_type+ must match across frames (CArray.meld is a view
24
- # constructor that refuses to auto-cast — silent promotion here would
25
- # hide schema drift). For an eager, auto-casting alternative use
23
+ # Per-column +data_type+ must match across frames: CArray.meld would bring
24
+ # the pieces to a common type, and in a frame that promotion would hide
25
+ # schema drift. For an eager, auto-casting alternative use
26
26
  # {CAFrame.concatenate}.
27
27
  #
28
28
  # Column matching is by name (output order follows the first frame);
@@ -54,6 +54,7 @@ class CAFrame
54
54
  frames = frames_with_rows(frames)
55
55
  cols = {}
56
56
  names.each do |name|
57
+ check_same_column_type(frames.map { |f| f[name] }, name, verb: "meld")
57
58
  cols[name] = CArray.meld(frames.map { |f| f[name] }, axis: 0)
58
59
  end
59
60
  new(cols, axis_name: first.axis_name, index: meld_index(frames))
@@ -229,6 +230,19 @@ class CAFrame
229
230
  end
230
231
  private_class_method :check_trailing_shapes
231
232
 
233
+ # The pieces of one column have one data type (and one class, for a
234
+ # Face): a frame does not promote a column to make its pieces agree.
235
+ def self.check_same_column_type(pieces, name, verb:)
236
+ first = pieces.first
237
+ pieces.each_with_index do |c, i|
238
+ next if CArray::Inspector.type_label(c) == CArray::Inspector.type_label(first)
239
+ raise ArgumentError,
240
+ "#{verb}: column #{name.inspect} is #{CArray::Inspector.type_label(c)} in piece #{i}, " \
241
+ "#{CArray::Inspector.type_label(first)} in piece 0 (cast it first, or use concatenate)"
242
+ end
243
+ end
244
+ private_class_method :check_same_column_type
245
+
232
246
  # Two indexes hold the same rows when their values agree cell by cell; a
233
247
  # NaN label matches a NaN label, and a masked cell a masked cell.
234
248
  def self.same_index_values?(a, b)
@@ -840,13 +840,12 @@ class CAFrame
840
840
  case pieces.size
841
841
  when 0 then col[CArray.int32(0), *tail] # replaced every row with none
842
842
  when 1 then pieces.first
843
- else CArray.meld(pieces, axis: 0) # CAMeld view; data type mismatch
844
- end # across pieces raises.
845
- # For data type conversion, cast
846
- # the incoming +other+'s
847
- # column beforehand — silent
848
- # promotion in a splice would
849
- # hide schema drift.
843
+ else
844
+ # A splice does not promote the column (it would hide schema
845
+ # drift); cast the incoming column beforehand.
846
+ CAFrame.send(:check_same_column_type, pieces, name, verb: "splice")
847
+ CArray.meld(pieces, axis: 0)
848
+ end
850
849
  splice_keep_attrs(new_cols[name], col)
851
850
  end
852
851
  new_index = splice_index(other, lo, hi)