carray 3.0.6 → 3.0.7

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 (139) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +110 -0
  3. data/README.md +24 -3
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_group.c +6 -6
  6. data/ext/ca_binop_dispatch.c +8 -0
  7. data/ext/ca_binop_dispatch.h +1 -0
  8. data/ext/ca_categorical_iterator.c +7 -7
  9. data/ext/ca_kernel_iterator.h +6 -4
  10. data/ext/ca_obj_array.c +4 -4
  11. data/ext/ca_obj_bincmp.c +19 -3
  12. data/ext/ca_obj_binop.c +36 -11
  13. data/ext/ca_obj_bitarray.c +2 -4
  14. data/ext/ca_obj_bitfield.c +86 -4
  15. data/ext/ca_obj_byte_swap.c +2 -4
  16. data/ext/ca_obj_const_string.c +422 -257
  17. data/ext/ca_obj_face.c +71 -33
  18. data/ext/ca_obj_fake.c +112 -13
  19. data/ext/ca_obj_farray.c +7 -1
  20. data/ext/ca_obj_fixlen_string.c +51 -17
  21. data/ext/ca_obj_grid.c +12 -7
  22. data/ext/ca_obj_meld.c +5 -5
  23. data/ext/ca_obj_monop.c +65 -39
  24. data/ext/ca_obj_object.c +9 -9
  25. data/ext/ca_obj_reduce.c +2 -4
  26. data/ext/ca_obj_refer.c +104 -10
  27. data/ext/ca_obj_repeat.c +1 -1
  28. data/ext/ca_obj_roll.c +2 -4
  29. data/ext/ca_obj_select.c +8 -6
  30. data/ext/ca_obj_select_axis.c +8 -6
  31. data/ext/ca_obj_stack.c +5 -5
  32. data/ext/ca_obj_stride.c +46 -8
  33. data/ext/ca_obj_string.c +2 -2
  34. data/ext/ca_obj_tile.c +2 -4
  35. data/ext/ca_obj_triop.c +15 -12
  36. data/ext/ca_obj_window.c +22 -15
  37. data/ext/ca_op_ipower.c +63 -15
  38. data/ext/ca_op_powi.h +6 -29
  39. data/ext/ca_sort_kernels.h +3 -0
  40. data/ext/ca_sweep_engine.c +90 -23
  41. data/ext/ca_sweep_engine.h +5 -6
  42. data/ext/caframe_csv_reader.c +1 -1
  43. data/ext/carray.h +3 -1
  44. data/ext/carray_access.c +98 -39
  45. data/ext/carray_address_basis.c +98 -24
  46. data/ext/carray_broadcast.c +2 -2
  47. data/ext/carray_call_cfunc.c +5 -8
  48. data/ext/carray_cast.c +311 -37
  49. data/ext/carray_cast_func.rb +37 -5
  50. data/ext/carray_conversion.c +56 -2
  51. data/ext/carray_copy.c +24 -0
  52. data/ext/carray_core.c +69 -6
  53. data/ext/carray_count.c +28 -9
  54. data/ext/carray_element.c +9 -3
  55. data/ext/carray_factorize.c +11 -11
  56. data/ext/carray_histogram.c +2 -2
  57. data/ext/carray_index_classifier.c +7 -5
  58. data/ext/carray_internal.h +94 -0
  59. data/ext/carray_kernels_binop.c +55 -55
  60. data/ext/carray_kernels_init.c +14 -14
  61. data/ext/carray_kernels_monop.c +90 -140
  62. data/ext/carray_lazy.c +16 -1
  63. data/ext/carray_loop.c +9 -2
  64. data/ext/carray_mask.c +118 -11
  65. data/ext/carray_median_percentile.c +13 -8
  66. data/ext/carray_memory_view.c +41 -22
  67. data/ext/carray_operator.c +72 -11
  68. data/ext/carray_order.c +52 -9
  69. data/ext/carray_partition.c +1 -1
  70. data/ext/carray_random.c +1 -1
  71. data/ext/carray_round.c +24 -1
  72. data/ext/carray_scatter.c +4 -0
  73. data/ext/carray_segment.c +1 -1
  74. data/ext/carray_slab.c +13 -4
  75. data/ext/carray_sort.c +6 -3
  76. data/ext/carray_sort_kernel.c +24 -0
  77. data/ext/carray_struct.c +21 -6
  78. data/ext/carray_test.c +85 -39
  79. data/ext/carray_time_text.c +6 -0
  80. data/ext/carray_utils.c +124 -54
  81. data/ext/mk_call_cfunc.rb +6 -11
  82. data/ext/mkkernel.rb +37 -27
  83. data/ext/version.h +4 -4
  84. data/lib/carray/axis_group.rb +9 -6
  85. data/lib/carray/basics.rb +26 -8
  86. data/lib/carray/bincount_nd.rb +8 -2
  87. data/lib/carray/block_iterator.rb +48 -21
  88. data/lib/carray/categorical.rb +21 -8
  89. data/lib/carray/categorical_iterator.rb +2 -1
  90. data/lib/carray/conditional.rb +52 -4
  91. data/lib/carray/const_string.rb +74 -0
  92. data/lib/carray/construct.rb +2 -2
  93. data/lib/carray/core_extensions.rb +19 -68
  94. data/lib/carray/data_type_extension.rb +1 -0
  95. data/lib/carray/fixlen_string.rb +83 -1
  96. data/lib/carray/frame/column_verbs.rb +61 -6
  97. data/lib/carray/frame/concat.rb +50 -6
  98. data/lib/carray/frame/csv_parser.rb +13 -4
  99. data/lib/carray/frame/frame.rb +38 -6
  100. data/lib/carray/frame/group.rb +67 -40
  101. data/lib/carray/frame/io.rb +10 -5
  102. data/lib/carray/frame/join.rb +20 -6
  103. data/lib/carray/frame/records.rb +6 -4
  104. data/lib/carray/frame/reshape.rb +25 -5
  105. data/lib/carray/frame/verbs.rb +78 -25
  106. data/lib/carray/frame/windows.rb +30 -6
  107. data/lib/carray/fuse_source.rb +4 -2
  108. data/lib/carray/iterator.rb +8 -0
  109. data/lib/carray/key_windows.rb +10 -4
  110. data/lib/carray/lazy.rb +12 -17
  111. data/lib/carray/methods/bin.rb +3 -0
  112. data/lib/carray/methods/composition.rb +1 -1
  113. data/lib/carray/methods/factorize.rb +2 -0
  114. data/lib/carray/methods/gather_nd.rb +4 -1
  115. data/lib/carray/methods/is_in.rb +23 -3
  116. data/lib/carray/methods/locate_addr.rb +63 -12
  117. data/lib/carray/methods/repeat.rb +11 -1
  118. data/lib/carray/record_order.rb +35 -6
  119. data/lib/carray/runtime.rb +10 -0
  120. data/lib/carray/segment_iterator.rb +19 -16
  121. data/lib/carray/serialize.rb +22 -0
  122. data/lib/carray/slab_iterator.rb +1 -1
  123. data/lib/carray/stack.rb +3 -3
  124. data/lib/carray/string.rb +6 -6
  125. data/lib/carray/string_operation_extension.rb +25 -4
  126. data/lib/carray/struct_builder.rb +4 -1
  127. data/lib/carray/time.rb +190 -46
  128. data/lib/carray/window_iterator.rb +48 -12
  129. data/yard-stubs/ca_obj_bitfield.rb +4 -1
  130. data/yard-stubs/ca_obj_refer.rb +3 -0
  131. data/yard-stubs/ca_obj_window.rb +2 -1
  132. data/yard-stubs/carray_access.rb +4 -2
  133. data/yard-stubs/carray_cast.rb +6 -0
  134. data/yard-stubs/carray_count.rb +6 -3
  135. data/yard-stubs/carray_math.rb +5 -0
  136. data/yard-stubs/carray_order.rb +6 -0
  137. data/yard-stubs/carray_scatter.rb +2 -1
  138. data/yard-stubs/carray_test.rb +18 -5
  139. metadata +2 -2
data/lib/carray/basics.rb CHANGED
@@ -162,6 +162,10 @@ class CArray
162
162
  lfill = fill_value if lfill.nil?
163
163
  ufill = fill_value if ufill.nil?
164
164
  out = copy
165
+ # Whatever is written must fit: a finer time, a wider fixed string.
166
+ [min, max, lfill, ufill].each { |v|
167
+ out, = branches_in_common(out, v) unless v.nil? || UNDEF.equal?(v)
168
+ }
165
169
  out[self < min] = lfill.nil? ? min : lfill unless min.nil?
166
170
  out[self > max] = ufill.nil? ? max : ufill unless max.nil?
167
171
  out
@@ -170,6 +174,12 @@ class CArray
170
174
  # An array of +shape+ of the same kind as self: its data type and cell
171
175
  # width, or its Face over a zero storage. Nothing is written into it, so
172
176
  # it can be built for a read-only Face too.
177
+ # Whether a cell can be given a value it has not seen. Not a CAConstString,
178
+ # whose strings share a buffer, or a CACategorical, whose codes index a
179
+ # vocabulary. A view that is read-only can still hold them: it is only
180
+ # that view that cannot be written.
181
+ private def holds_new_values? = true
182
+
173
183
  private def blank_like (shape)
174
184
  storage = face_storage
175
185
  out = CArray.new(storage.data_type, shape, bytes: storage.bytes)
@@ -334,7 +344,8 @@ class CArray
334
344
  # masked: a missing bound is not read as either, so fill it first.
335
345
  #
336
346
  # Boundary is strict `[min, max]`: values equal to a bound
337
- # remain unchanged in both the clamped and filled variants.
347
+ # remain unchanged in both the clamped and filled variants. A NaN
348
+ # is in neither range and stays NaN.
338
349
  #
339
350
  # @param min [Numeric, nil] lower bound; `nil` for one-sided
340
351
  # clip above.
@@ -597,14 +608,14 @@ class CArray
597
608
  # reductions do: each block value is written into a blank of the Face
598
609
  # (which reads a CATime::Element, a string, a Face array ...) and handed
599
610
  # to the slab engine as the storage it became; the result is lifted back
600
- # at the end. A read-only Face has no blank to write into, so its values
601
- # are collected as the surface objects they are. A `data_type:` from the
602
- # caller is the caller's, and passes through.
611
+ # at the end. A Face that holds no new value has no blank to write into,
612
+ # so its values are collected as the surface objects they are. A
613
+ # `data_type:` from the caller is the caller's, and passes through.
603
614
  module FaceSlabResult
604
615
  def map_slab (**kw, &block)
605
616
  return super unless face? && kw[:data_type].nil? && block
606
- return super(**kw, data_type: :object, &block) if read_only?
607
- out = super(**kw, data_type: face_storage.data_type) { |slab|
617
+ return super(**kw, data_type: :object, &block) unless holds_new_values?
618
+ out = super(**kw, **face_slab_type) { |slab|
608
619
  face_slab_storage(block.call(slab), slab.shape)
609
620
  }
610
621
  out.face_lift(self)
@@ -612,10 +623,10 @@ class CArray
612
623
 
613
624
  def reduce_slab (**kw, &block)
614
625
  return super unless face? && kw[:data_type].nil? && block
615
- return super(**kw, data_type: :object, &block) if read_only?
626
+ return super(**kw, data_type: :object, &block) unless holds_new_values?
616
627
  fiber = kw.key?(:init)
617
628
  init = kw.delete(:init)
618
- out = super(**kw, data_type: face_storage.data_type) { |slab|
629
+ out = super(**kw, **face_slab_type) { |slab|
619
630
  value = if fiber
620
631
  slab.to_a.flatten.inject(init) { |acc, e| block.call(acc, e) }
621
632
  else
@@ -628,6 +639,13 @@ class CArray
628
639
 
629
640
  private
630
641
 
642
+ # The engine's output type. Fixlen storage is named by leaving it out: the
643
+ # name alone has no width, and the source's is the one wanted.
644
+ def face_slab_type
645
+ storage = face_storage
646
+ storage.data_type == CA_FIXLEN ? {} : { data_type: storage.data_type }
647
+ end
648
+
631
649
  def face_slab_storage (value, shape)
632
650
  cell = blank_like(shape)
633
651
  cell[] = value
@@ -238,7 +238,10 @@ class CArray
238
238
  ext_sizes = @lengths.map { |l| l + 1 } # +1: upper overflow cell
239
239
  strides_ext = ext_sizes.each_with_index.map { |_, k| ext_sizes[(k + 1)..].inject(1, :*) }
240
240
  total_ext = ext_sizes.inject(:*)
241
- widen = total_ext > 0x7fffffff # int64 flat for big joint tables
241
+ # The flat index is worked out in the labels' own type, so it must reach
242
+ # the last cell of the table in that type.
243
+ label_bits = 8 * chunk.bytes - (chunk.data_type_name.start_with?("u") ? 0 : 1)
244
+ widen = total_ext - 1 >= 2 ** label_bits
242
245
 
243
246
  # canonical [fiber..., sample, channel] view (channel last); weights to
244
247
  # [fiber..., sample]. Skip the transpose when the layout is already
@@ -270,7 +273,10 @@ class CArray
270
273
  ravel = nil
271
274
  (0...@m).each do |k|
272
275
  b = tchunk[nil, k]
273
- b = b.clip(0, @lengths[k]) if b.max > @lengths[k] - 1
276
+ top = b.max
277
+ # No present cell in a channel: every sample is dropped.
278
+ return self if top == UNDEF
279
+ b = b.clip(0, @lengths[k]) if top > @lengths[k] - 1
274
280
  b = b.int64 if widen
275
281
  term = strides_ext[k] == 1 ? b : b * strides_ext[k]
276
282
  ravel = ravel.nil? ? term : ravel + term
@@ -88,10 +88,15 @@ class CABlockIterator < CAIterator
88
88
  @sizes = Array.new(@sndim)
89
89
  blocks.each_with_index do |b, i|
90
90
  if b.is_a?(Range)
91
+ unless b.begin.is_a?(Integer) && b.end.is_a?(Integer)
92
+ raise TypeError, "blocks: axis #{i} takes a Range of Integer positions, got #{b.inspect}"
93
+ end
91
94
  offsets[i] = b.begin
92
95
  @sizes[i] = b.end - b.begin + (b.exclude_end? ? 0 : 1)
96
+ elsif b.is_a?(Integer)
97
+ @sizes[i] = b
93
98
  else
94
- @sizes[i] = Integer(b)
99
+ raise TypeError, "blocks: a tile size must be an Integer (got #{b.class})"
95
100
  end
96
101
  if @sizes[i] < 1
97
102
  raise ArgumentError, "blocks: tile size on axis #{i} must be >= 1"
@@ -365,16 +370,9 @@ class CABlockIterator < CAIterator
365
370
  def count (*args, **kw)
366
371
  return count_not_masked(**kw) if args.empty?
367
372
  kw = CAIterator.reduce_keywords(:count, kw)
368
- out = nil
369
- each_region do |strip_ranges, tiles, out_ranges|
370
- view = @source[*strip_ranges].block_view(*tiles)
371
- # block_view is a CAStride, so #count is not shadowed; dispatch
372
- # CArray#count explicitly anyway, matching the family regularity.
373
- red = CArray.instance_method(:count).bind_call(view, *args, axis: @tile_axes, **kw)
374
- out ||= CArray.new(red.data_type, @shape, bytes: red.bytes)
375
- out[*out_ranges] = red
376
- end
377
- out
373
+ # block_view is a CAStride, so #count is not shadowed; dispatch
374
+ # CArray#count explicitly anyway, matching the family regularity.
375
+ assemble { |view, _| count_cells(view, *args, axis: @tile_axes, **kw) }
378
376
  end
379
377
 
380
378
  # @overload count_not_masked
@@ -477,6 +475,12 @@ class CABlockIterator < CAIterator
477
475
  return src[addr.strip_mask(hole[0])].copy
478
476
  end
479
477
  out = src[addr.strip_mask(0)].copy
478
+ unless out.__send__(:holds_new_values?)
479
+ # A const string's cells are written as Strings and packed again.
480
+ text = out.to_string
481
+ text[empty] = fill_value.nil? ? UNDEF : fill_value
482
+ return text.to_const_string
483
+ end
480
484
  out[empty] = fill_value.nil? ? UNDEF : fill_value
481
485
  out
482
486
  end
@@ -681,15 +685,24 @@ class CABlockIterator < CAIterator
681
685
  # into a source-shaped CArray (the out-of-bounds cells of a partial edge
682
686
  # tile are dropped). Well-defined because tiles do not overlap; the source
683
687
  # is not modified (a new array is returned). Without a block, an Enumerator.
688
+ # The result has the source's data type; a Face source gives a result of
689
+ # the same Face, and a read-only Face (a categorical, a const string) an
690
+ # object array of its surface values.
684
691
  # @yieldparam tile [CArray]
685
692
  # @return [CArray, Enumerator] source-shaped
686
693
  def map
687
694
  return to_enum(:map) unless block_given?
688
- pout = CArray.new(padded_source.data_type, padded_source.shape, bytes: padded_source.bytes)
695
+ src = padded_source
696
+ surface = src.face? && !src.__send__(:holds_new_values?)
697
+ pout = surface ? CArray.new(CA_OBJECT, src.shape) : src.__send__(:blank_like, src.shape)
689
698
  pgv = pout.block_view(*@sizes)
690
699
  tgv = tile_grid_view
691
700
  nils = Array.new(@sndim, nil)
692
- CArray.each_index(*@shape) { |*g| pgv[*g, *nils] = yield(tgv[*g, *nils]) }
701
+ CArray.each_index(*@shape) { |*g|
702
+ tile = yield(tgv[*g, *nils])
703
+ tile = tile.to_a if surface && tile.is_a?(CArray) && tile.face?
704
+ pgv[*g, *nils] = tile
705
+ }
693
706
  pout[*@sndim.times.map { |i| 0...@source.shape[i] }].copy
694
707
  end
695
708
 
@@ -797,20 +810,23 @@ class CABlockIterator < CAIterator
797
810
  tile = tgv[*g, *nils]
798
811
  scanned = tile.copy.reshape(tile.elements).public_send(op, *args).reshape(*@sizes)
799
812
  unless pout
800
- pout = CArray.new(scanned.data_type, padded_source.shape, bytes: scanned.bytes)
813
+ pout = scanned.face? ? scanned.__send__(:blank_like, padded_source.shape) :
814
+ CArray.new(scanned.data_type, padded_source.shape, bytes: scanned.bytes)
801
815
  pout[] = UNDEF
802
816
  pgv = pout.block_view(*@sizes)
803
817
  end
804
818
  pgv[*g, *nils] = scanned
805
819
  end
820
+ # a source with no cells has no tile; its own scan gives the type
821
+ return @source.copy.reshape(@source.elements).public_send(op, *args).reshape(*@source.shape) unless pout
806
822
  pout[*@sndim.times.map { |i| 0...@source.shape[i] }].copy
807
823
  end
808
824
 
809
825
  # A source-shaped array that can hold what #tile_wise gives: of its kind,
810
- # or an object array for a read-only Face, which cannot be written cell by
811
- # cell.
826
+ # or an object array for a Face that holds no new value, which cannot be
827
+ # written cell by cell.
812
828
  def tile_output (lagged)
813
- if lagged.face? && lagged.read_only?
829
+ if lagged.face? && !lagged.__send__(:holds_new_values?)
814
830
  CArray.object(*@source.shape)
815
831
  else
816
832
  lagged.__send__(:blank_like, @source.shape)
@@ -834,10 +850,21 @@ class CABlockIterator < CAIterator
834
850
  if pshape == @source.shape.to_a
835
851
  @source
836
852
  else
837
- pad = @source.__send__(:blank_like, pshape) # a Face stays one
838
- pad[] = UNDEF
839
- pad[*@sndim.times.map { |i| 0...@source.shape[i] }] = @source
840
- pad
853
+ interior = @sndim.times.map { |i| 0...@source.shape[i] }
854
+ if @source.face? && !@source.__send__(:holds_new_values?)
855
+ # A Face that holds no new value cannot be written: its storage is
856
+ # padded and the Face put back on.
857
+ storage = @source.__send__(:face_storage)
858
+ pad = CArray.new(storage.data_type, pshape, bytes: storage.bytes)
859
+ pad[] = UNDEF
860
+ pad[*interior] = storage
861
+ pad.face_lift(@source)
862
+ else
863
+ pad = @source.__send__(:blank_like, pshape) # a Face stays one
864
+ pad[] = UNDEF
865
+ pad[*interior] = @source
866
+ pad
867
+ end
841
868
  end
842
869
  end
843
870
  end
@@ -59,6 +59,8 @@ require "carray/methods/is_in" # refuse_range_set, for the set-valued verbs
59
59
  # kernels are gated off (`cat + 1` raises; arithmetic on category codes is
60
60
  # not meaningful).
61
61
  class CACategorical < CAObject
62
+ private def holds_new_values? = false
63
+
62
64
 
63
65
  # raw-byte unpack format per storage (codes) data_type, native endian.
64
66
  # The FIXLEN surface delivers a per-cell fetch as an N-byte String; this
@@ -373,12 +375,13 @@ class CACategorical < CAObject
373
375
  # Returns a boolean CArray marking cells whose category equals
374
376
  # `label`. A CArray is compared cell by cell, by label: another
375
377
  # categorical is matched whatever order its labels are in, and a
376
- # masked cell on either side gives UNDEF. Anything else, a Ruby Array
377
- # included, is one label, as for {CArray#eq}. An unknown label matches
378
- # nothing.
378
+ # masked cell on either side gives UNDEF. `UNDEF` asks the mask, as for
379
+ # {CArray#eq}: true where a cell is in no category. Anything else, a Ruby
380
+ # Array included, is one label. An unknown label matches nothing.
379
381
  # @param label [Object, CArray] category to match, or one per cell.
380
382
  # @return [CArray]
381
383
  def eq(label)
384
+ return is_masked if label.equal?(UNDEF)
382
385
  codes.eq(codes_of(label))
383
386
  end
384
387
 
@@ -388,24 +391,31 @@ class CACategorical < CAObject
388
391
  # cell.
389
392
  # @return [CArray]
390
393
  def ne(label)
394
+ return is_not_masked if label.equal?(UNDEF)
391
395
  codes.ne(codes_of(label))
392
396
  end
393
397
 
394
398
  # @overload count(label)
395
399
  # Returns the number of cells whose category equals `label`
396
- # (0 for an unknown label).
400
+ # (0 for an unknown label). `UNDEF` counts the cells in no category,
401
+ # as {#count_masked}. The keywords of {CArray#count} (`axis:`,
402
+ # `keep_axis:`, `min_count:`, `fill_value:`) are taken in both forms.
397
403
  # @param label [Object] category to count.
398
404
  # @return [Integer]
399
405
  # @overload count
400
406
  # Returns the number of cells in some category (not masked), as
401
407
  # {CArray#count} with no value does.
402
408
  # @return [Integer]
403
- def count(*args)
404
- return count_not_masked if args.empty?
409
+ def count(*args, **kw)
410
+ return count_not_masked(**kw) if args.empty?
405
411
  raise ArgumentError, "count: wrong number of arguments (given #{args.size}, expected 0..1)" if args.size > 1
406
412
  label = args[0]
413
+ return count_masked(**kw) if label.equal?(UNDEF)
407
414
  code = @labels.index(label)
408
- code ? codes.count(code) : 0
415
+ return codes.count(code, **kw) if code
416
+ # an unknown label matches no cell; counted over the same mask so the
417
+ # keywords (axis:, min_count:, fill_value:) answer as for a known label
418
+ kw.empty? ? 0 : codes.ne(codes).count(true, **kw)
409
419
  end
410
420
 
411
421
  # Per-category counts as a length-k array aligned to #labels. Trailing empty
@@ -494,7 +504,10 @@ class CACategorical < CAObject
494
504
  # @return [CArray]
495
505
  def is_in (values)
496
506
  refuse_range_set(values, "is_in")
497
- wanted = label_list(values).filter_map { |l| @labels.index(l) }
507
+ wanted = label_list(values).filter_map do |l|
508
+ # nan-order: looks a label up and orders nothing; == does not find a NaN.
509
+ l.is_a?(Float) && l.nan? ? @labels.index { |x| x.is_a?(Float) && x.nan? } : @labels.index(l)
510
+ end
498
511
  codes.is_in(CArray.int64(wanted.size) { |i| wanted[i] })
499
512
  end
500
513
 
@@ -492,6 +492,7 @@ class CACategoricalIterator < CASegmentIterator
492
492
  # each c) — bounded by K, typically small. A fused per-fiber variance
493
493
  # kernel is a natural follow-on if bench demands it.
494
494
  def axis_by_masked_copy (axis, op, out_data_type = core_reduce_type(op))
495
+ return axis_by_core(axis, op) if @value.face?
495
496
  h = @value
496
497
  axis = checked_axis(axis, op)
497
498
  full_c = resolve_axis_codes(@cat.codes, h.shape, axis)
@@ -534,7 +535,7 @@ class CACategoricalIterator < CASegmentIterator
534
535
  flat ? g.__send__(op, *args, axis: axis, keep_axis: true)
535
536
  : g.__send__(op, *args, axis: axis)
536
537
  end
537
- if answers.first.face? && answers.first.read_only?
538
+ if answers.first.face? && !answers.first.__send__(:holds_new_values?)
538
539
  answers = answers.map { |a| CArray.object(*a.shape) { a.to_a } }
539
540
  end
540
541
  out = flat ? CArray.concatenate(answers) : CArray.stack(answers, axis: 0).copy
@@ -130,6 +130,13 @@ class CArray
130
130
  CATriOp.__build__(self, branch.(x), branch.(y), CATriOp::OP_SELECT)
131
131
  end
132
132
 
133
+ # The two branches of a selection, in a form that holds the cells of both.
134
+ # A Face whose cells differ by more than their values (a time's unit, a
135
+ # fixed string's width) brings them to one here, whichever branch is which.
136
+ private def branches_in_common (x, y)
137
+ [x, y]
138
+ end
139
+
133
140
  private def face_then_else (x, y, face)
134
141
  # A Face branch decides the result: the other branch is written into a
135
142
  # copy of it through the Face's own store, so an instant stays an instant.
@@ -141,10 +148,11 @@ class CArray
141
148
  sy = y.is_a?(CAConstString) ? y.to_string : y
142
149
  return CArray.const_string(then_else(sx, sy), encoding: face.encoding)
143
150
  end
151
+ x, y = face.__send__(:branches_in_common, x, y)
144
152
  x_full = x.is_a?(CArray) && !x.scalar?
145
153
  y_full = y.is_a?(CArray) && !y.scalar?
146
154
  base = y_full ? y : x
147
- if base.read_only?
155
+ unless base.__send__(:holds_new_values?)
148
156
  # No store to write through (a CACategorical's codes): select among
149
157
  # the values themselves.
150
158
  result = CArray.object(*shape)
@@ -180,7 +188,8 @@ class CArray
180
188
  "replace_where: cond must be a boolean CArray (data_type == CA_BOOLEAN)"
181
189
  end
182
190
  result = self.copy
183
- result[cond] = b.is_a?(CArray) ? b[cond] : b
191
+ # A CScalar takes no index: like a number, it is the value for every cell.
192
+ result[cond] = b.is_a?(CArray) && !b.scalar? ? b[cond] : b
184
193
  result
185
194
  end
186
195
 
@@ -265,6 +274,8 @@ class CArray
265
274
  # @param condlist [Array<CArray>] boolean selectors.
266
275
  # @param choicelist [Array<CArray, Numeric, Object>] values, one
267
276
  # per condition (same length as `condlist`).
277
+ # When a choice is a Face the result is that Face (the choices meet as
278
+ # in {#then_else}), and `default` is UNDEF unless it is given.
268
279
  # @param default [CArray, Numeric, Object] value written where no
269
280
  # condition holds.
270
281
  # @param data_type [Symbol, Integer, nil] override for the result
@@ -278,7 +289,7 @@ class CArray
278
289
  # [-x, x * 10],
279
290
  # default: 999)
280
291
  # # => [5.0, 3.0, -10.0, 0.0, 10.0, 999.0]
281
- def self.select (condlist, choicelist, default: 0, data_type: nil)
292
+ def self.select (condlist, choicelist, default: (no_default = true; 0), data_type: nil)
282
293
  unless condlist.is_a?(Array) && choicelist.is_a?(Array)
283
294
  raise ArgumentError, "select: condlist and choicelist must be Arrays"
284
295
  end
@@ -296,6 +307,14 @@ class CArray
296
307
  end
297
308
  shape = first.shape
298
309
 
310
+ if data_type.nil?
311
+ face = choicelist.find { |v| v.is_a?(CArray) && !v.scalar? && v.face? }
312
+ if face
313
+ fallback = no_default ? UNDEF : default
314
+ return select_among_faces(condlist, choicelist, fallback, face, shape)
315
+ end
316
+ end
317
+
299
318
  dt = data_type || selection_type(choicelist + [default])
300
319
  # `default` can be either a same-shape CArray (per-cell fallback) or a
301
320
  # scalar (broadcast to every cell).
@@ -317,7 +336,7 @@ class CArray
317
336
  "select: condlist[#{k}] must be a same-shape boolean CArray"
318
337
  end
319
338
  v = choicelist[k]
320
- out[c] = v.is_a?(CArray) ? v[c] : v
339
+ out[c] = v.is_a?(CArray) && !v.scalar? ? v[c] : v
321
340
  end
322
341
  if !data_type && selection_text?(choicelist + [default])
323
342
  out = CAString.wrap(out)
@@ -331,6 +350,35 @@ class CArray
331
350
  # some type, so it leaves the type to the others. A String or Symbol is a
332
351
  # value, which CArray.result_type would read as the name of a data type
333
352
  # ("int32"), so it makes the result object.
353
+ # select over Face choices. Each condition is a then_else on the choices
354
+ # below it, so the Faces meet as they do there; a Face that cannot be
355
+ # written selects among its values.
356
+ def self.select_among_faces (condlist, choicelist, fallback, face, shape)
357
+ if face.is_a?(CAConstString)
358
+ choices = choicelist.map { |v| v.is_a?(CAConstString) ? v.to_string : v }
359
+ text = select(condlist, choices, default: fallback)
360
+ return CArray.const_string(text, encoding: face.encoding)
361
+ end
362
+ unless face.__send__(:holds_new_values?)
363
+ choices = choicelist.map { |v| v.is_a?(CArray) && v.face? ? v.to_type(CA_OBJECT) : v }
364
+ return select(condlist, choices, default: fallback)
365
+ end
366
+ unless face.shape == shape
367
+ raise ArgumentError, "select: choicelist shape #{face.shape} differs from condlist shape #{shape}"
368
+ end
369
+ out = CArray.boolean(*shape).zero.then_else(face, fallback)
370
+ (condlist.size - 1).downto(0) do |k|
371
+ c = condlist[k]
372
+ unless c.is_a?(CArray) && c.boolean? && c.shape == shape
373
+ raise ArgumentError,
374
+ "select: condlist[#{k}] must be a same-shape boolean CArray"
375
+ end
376
+ out = c.then_else(choicelist[k], out)
377
+ end
378
+ out
379
+ end
380
+ private_class_method :select_among_faces
381
+
334
382
  def self.selection_type (values)
335
383
  typed = values.reject { |v| v.equal?(UNDEF) }
336
384
  if typed.empty? || typed.any? { |v| v.is_a?(String) || v.is_a?(Symbol) }
@@ -86,6 +86,7 @@ end
86
86
  # (categorical labels) prefer {CACategorical}, which stores each distinct
87
87
  # value once.
88
88
  class CAConstString
89
+ private def holds_new_values? = false
89
90
 
90
91
  # Read-only String Face: L1 string operations only (no in-place Mutable).
91
92
  # Its C-native byte predicates / search / comparison (ext/ca_obj_const_string.c)
@@ -205,6 +206,17 @@ class CAConstString
205
206
  const_string_lift(to_string.clip(*args, **kw))
206
207
  end
207
208
 
209
+ # A String has no distance, so there is no nearest one; refused in the
210
+ # words CAString uses.
211
+ def search_nearest (*args, **kw)
212
+ to_string.search_nearest(*args, **kw)
213
+ end
214
+
215
+ # @see #search_nearest
216
+ def search_nearest_addr (*args, **kw)
217
+ to_string.search_nearest_addr(*args, **kw)
218
+ end
219
+
208
220
  # @overload min_index(axis: nil)
209
221
  # @return [Integer, CArray] where the byte-smallest string sits.
210
222
  def min_index (*args, **kw)
@@ -217,6 +229,68 @@ class CAConstString
217
229
  to_string.max_index(*args, **kw)
218
230
  end
219
231
 
232
+ # @overload min_addr(axis: nil)
233
+ # @return [Integer, CArray] the flat address of the byte-smallest string.
234
+ def min_addr (*args, **kw)
235
+ to_string.min_addr(*args, **kw)
236
+ end
237
+
238
+ # @overload max_addr(axis: nil)
239
+ # @return [Integer, CArray] the flat address of the byte-largest string.
240
+ def max_addr (*args, **kw)
241
+ to_string.max_addr(*args, **kw)
242
+ end
243
+
244
+ # @overload bsearch(value)
245
+ # @return [Integer, nil] where a sorted column holds `value`.
246
+ def bsearch (*args, **kw)
247
+ to_string.bsearch(*args, **kw)
248
+ end
249
+
250
+ # @overload bsearch_addr(value)
251
+ # @return [Integer, nil] the flat address of `value` in a sorted column.
252
+ def bsearch_addr (*args, **kw)
253
+ to_string.bsearch_addr(*args, **kw)
254
+ end
255
+
256
+ # @overload search_addr(value)
257
+ # @return [Integer, nil] the flat address of the first cell equal to `value`.
258
+ def search_addr (*args, **kw)
259
+ to_string.search_addr(*args, **kw)
260
+ end
261
+
262
+ # @overload match(pattern)
263
+ # @return [CArray] boolean, as {CAString} answers.
264
+ def match (*args)
265
+ to_string.match(*args)
266
+ end
267
+
268
+ # @!method pmax(other)
269
+ # The later String in byte order of each cell and `other`.
270
+ # @return [CAConstString]
271
+ # @!method pmin(other)
272
+ # The earlier String in byte order of each cell and `other`.
273
+ # @return [CAConstString]
274
+ # @!method maximum(other)
275
+ # Another name of {#pmax}.
276
+ # @return [CAConstString]
277
+ # @!method minimum(other)
278
+ # Another name of {#pmin}.
279
+ # @return [CAConstString]
280
+ # @!method fmax(other)
281
+ # Another name of {#pmax}.
282
+ # @return [CAConstString]
283
+ # @!method fmin(other)
284
+ # Another name of {#pmin}.
285
+ # @return [CAConstString]
286
+ CArray.send(:define_named_methods, self,
287
+ [:pmax, :pmin, :maximum, :minimum, :fmax, :fmin], :pick_among_strings)
288
+
289
+ private def pick_among_strings (op, other)
290
+ refuse_unordered_operands(op, other)
291
+ const_string_lift(to_string.public_send(op, string_operand(other)))
292
+ end
293
+
220
294
  # @overload partition_copy(kth, axis: nil)
221
295
  # @return [CAConstString] partitioned about the `kth` string in order.
222
296
  def partition_copy (*args, **kw)
@@ -191,7 +191,7 @@ class CArray
191
191
  extend DataTypeNewConstructor
192
192
  extend DataTypeExtension
193
193
  # @!visibility private
194
- TypeSymbol = :complex64
194
+ TypeSymbol = :cmplx64
195
195
  # @!visibility private
196
196
  DataType = CA_CMPLX64
197
197
  end
@@ -208,7 +208,7 @@ class CArray
208
208
  extend DataTypeNewConstructor
209
209
  extend DataTypeExtension
210
210
  # @!visibility private
211
- TypeSymbol = :complex128
211
+ TypeSymbol = :cmplx128
212
212
  # @!visibility private
213
213
  DataType = CA_CMPLX128
214
214
  end