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
@@ -59,7 +59,17 @@ class CArray
59
59
  end
60
60
  CArray.int64(width).fill(count)
61
61
  else
62
- given = count.to_ca.int64
62
+ unless count.respond_to?(:to_ca)
63
+ raise TypeError, "repeat: count must be an Integer or a list of them (got #{count.class})"
64
+ end
65
+ given = count.to_ca
66
+ # A list arrives as an object array; a Float in it is no count, as a
67
+ # float array is not.
68
+ unless given.integer? || (given.data_type == CA_OBJECT &&
69
+ given.to_a.flatten.all? { |c| c.is_a?(Integer) || c.equal?(UNDEF) })
70
+ raise TypeError, "repeat: counts must be Integers"
71
+ end
72
+ given = given.int64
63
73
  unless given.elements == width
64
74
  raise ArgumentError,
65
75
  "repeat: #{given.elements} counts for #{width} " \
@@ -52,20 +52,20 @@ class CARecord
52
52
  define_method(op, &Ractor.make_shareable(proc { |*args, **kw| record_rank.public_send(op, *args, **kw) }))
53
53
  end
54
54
 
55
- # @overload min(axis: nil, keep_axis: false)
55
+ # @overload min(axis: nil, keep_axis: false, min_count: nil, fill_value: nil)
56
56
  # Returns the smallest record by the declared order, skipping masked
57
57
  # records; UNDEF when every record is masked. With `axis:` a
58
58
  # {CARecord} of the smallest along that axis.
59
59
  # @return [CAStruct, CARecord]
60
- def min (axis: nil, keep_axis: false)
61
- record_extreme(:min_index, axis, keep_axis)
60
+ def min (axis: nil, keep_axis: false, min_count: nil, fill_value: nil)
61
+ record_extreme_kept(:min_index, axis, keep_axis, min_count, fill_value)
62
62
  end
63
63
 
64
- # @overload max(axis: nil, keep_axis: false)
64
+ # @overload max(axis: nil, keep_axis: false, min_count: nil, fill_value: nil)
65
65
  # The largest record; see {#min}.
66
66
  # @return [CAStruct, CARecord]
67
- def max (axis: nil, keep_axis: false)
68
- record_extreme(:max_index, axis, keep_axis)
67
+ def max (axis: nil, keep_axis: false, min_count: nil, fill_value: nil)
68
+ record_extreme_kept(:max_index, axis, keep_axis, min_count, fill_value)
69
69
  end
70
70
 
71
71
  # @overload minmax(axis: nil, keep_axis: false)
@@ -162,6 +162,28 @@ class CARecord
162
162
  end
163
163
 
164
164
  # The record at each min_index / max_index, gathered from self.
165
+ # An extreme under the reduction keywords every extreme takes: UNDEF where
166
+ # fewer than `min_count` cells are present, then `fill_value` for UNDEF.
167
+ def record_extreme_kept (index_op, axis, keep_axis, min_count, fill_value)
168
+ out = record_extreme(index_op, axis, keep_axis)
169
+ name = index_op.to_s.sub("_index", "").to_sym
170
+ need = CArray.__present_cells_needed_per_result_cell__(name, min_count, 0)
171
+ if need > 0
172
+ if axis.nil?
173
+ out = UNDEF if count_not_masked < need
174
+ else
175
+ counts = count_not_masked(axis: axis, keep_axis: keep_axis)
176
+ if counts.is_a?(CArray)
177
+ few = counts.lt(need)
178
+ out[few] = UNDEF if few.any
179
+ elsif counts < need
180
+ out = UNDEF
181
+ end
182
+ end
183
+ end
184
+ CArray.__fill_undefined_result_cells__(out, fill_value)
185
+ end
186
+
165
187
  def record_extreme (index_op, axis, keep_axis)
166
188
  rank = record_rank
167
189
  if axis.nil?
@@ -175,6 +197,13 @@ class CARecord
175
197
  return record_extreme_over(index_op, axes, keep_axis)
176
198
  end
177
199
  axis = normalize_axis(axis, name)
200
+ if ndim == 1
201
+ r = record_extreme(index_op, nil, false)
202
+ return r unless keep_axis
203
+ out = flatten[0..0].copy
204
+ out[0] = r
205
+ return out
206
+ end
178
207
  idx = rank.public_send(index_op, axis: axis)
179
208
  empty = idx.is_masked
180
209
  picked_shape = shape.dup
@@ -172,5 +172,15 @@ class CArray
172
172
  end
173
173
  private_class_method :define_named_methods
174
174
 
175
+ # CArray.promote_list, its refusal named after the verb that gave it the
176
+ # list (stack, meld, concatenate, ...), not after promote_list.
177
+ def self.promote_list_for (verb, list, data_type: nil)
178
+ CArray.promote_list(list, data_type: data_type)
179
+ rescue ArgumentError, CArray::DataTypeError => e
180
+ raise unless e.message.start_with?("promote_list: ")
181
+ raise e.class, "#{verb}: #{e.message.delete_prefix("promote_list: ")}"
182
+ end
183
+ private_class_method :promote_list_for
184
+
175
185
  end
176
186
 
@@ -125,8 +125,7 @@ class CASegmentIterator < CAIterator
125
125
  # CABlock, whose own #count is the block geometry accessor, so dispatch
126
126
  # CArray#count explicitly. (Not fused: a value-equality reduceat would have
127
127
  # to reproduce core's cross-type / out-of-range equality exactly.)
128
- cnt = CArray.instance_method(:count)
129
- per_segment(CA_INT64) { |s| cnt.bind_call(s, *args) }
128
+ per_segment(CA_INT64) { |s| count_cells(s, *args) }
130
129
  end
131
130
 
132
131
  # @overload count_masked
@@ -233,8 +232,10 @@ class CASegmentIterator < CAIterator
233
232
  # as five length-k float64 CArrays (matching `CArray#quantile`): the
234
233
  # percentiles at 0 / 25 / 50 / 75 / 100. Empty / all-masked segments are
235
234
  # MASKED. For a single fraction q in 0..1 use `percentile(q * 100)`.
235
+ # @param axis [Integer, nil] as for {#percentile}.
236
236
  # @return [Array<CArray>]
237
- def quantile
237
+ def quantile (axis: nil)
238
+ return CArray.__name_errors_after_method__(:quantile) { [0, 25, 50, 75, 100].map { |p| percentile(p, axis: axis) } } if axis
238
239
  unless kernel_reads?
239
240
  return CArray.__name_errors_after_method__(:quantile) { [0, 25, 50, 75, 100].map { |p| percentile(p) } }
240
241
  end
@@ -297,7 +298,8 @@ class CASegmentIterator < CAIterator
297
298
  # iff every present value is truthy (empty segment -> true, vacuously).
298
299
  # The value data type must be boolean, as for `CArray#all`.
299
300
  # @return [CArray]
300
- def all
301
+ def all (axis: nil)
302
+ return axis_by_core(axis, :all) if axis
301
303
  aa = all_any
302
304
  aa ? aa[:all] : per_segment(CA_BOOLEAN) { |s| s.all }
303
305
  end
@@ -307,7 +309,8 @@ class CASegmentIterator < CAIterator
307
309
  # iff some present value is truthy (empty segment -> false). The value
308
310
  # data type must be boolean, as for `CArray#any`.
309
311
  # @return [CArray]
310
- def any
312
+ def any (axis: nil)
313
+ return axis_by_core(axis, :any) if axis
311
314
  aa = all_any
312
315
  aa ? aa[:any] : per_segment(CA_BOOLEAN) { |s| s.any }
313
316
  end
@@ -731,7 +734,9 @@ class CASegmentIterator < CAIterator
731
734
 
732
735
  def scan_answered (op)
733
736
  # An empty Face has no cell to ask the core about, and nothing to scan.
734
- return blank_output(@src_shape) if @value.face? && @value.elements.zero?
737
+ if @value.face? && @value.elements.zero?
738
+ return op == :cumcount ? CArray.int64(*@src_shape) : blank_output(@src_shape)
739
+ end
735
740
  answer = core_scan_answer(op)
736
741
  unless kernel_scans?(op, answer)
737
742
  return scatter_segments(->(shape) { blank_like(answer, shape) }) { |s| s.public_send(op) }
@@ -757,7 +762,7 @@ class CASegmentIterator < CAIterator
757
762
  # (the core counts a boolean's running sum in uint64).
758
763
  def kernel_scans? (op, answer)
759
764
  return false unless CArray.__fast_paths_enabled__(:segment_scan_kernel)
760
- return false if @value.face? || answer.face? || @value.complex?
765
+ return false if @value.face? || answer.face? || @value.complex? || @value.data_type == CA_FIXLEN
761
766
  kernel_type =
762
767
  case op
763
768
  when :cumcount then CA_INT64
@@ -798,10 +803,10 @@ class CASegmentIterator < CAIterator
798
803
  end
799
804
 
800
805
  # A blank array of +shape+ of the same kind as +model+: its data type, or its
801
- # Face. A read-only Face cannot be written cell by cell, so its values are
802
- # held as the surface objects they are.
806
+ # Face. A Face that holds no new value cannot be written cell by cell, so its
807
+ # values are held as the surface objects they are.
803
808
  def blank_like (model, shape)
804
- if model.face? && model.read_only?
809
+ if model.face? && !model.__send__(:holds_new_values?)
805
810
  CArray.new(CA_OBJECT, shape)
806
811
  else
807
812
  model.__send__(:blank_like, shape)
@@ -1042,10 +1047,10 @@ class CASegmentIterator < CAIterator
1042
1047
 
1043
1048
  # Whether an output can be built by lifting one. A Face is filled by writing
1044
1049
  # surface values into storage, so this needs a Face that can be written
1045
- # into; a read-only one -- a const string's records index a shared pool, a
1050
+ # into; another -- a const string's records index a shared pool, a
1046
1051
  # categorical's codes index a vocabulary -- has no blank form to fill.
1047
1052
  def face_output?
1048
- @value.face? && ! @value.read_only?
1053
+ @value.face? && @value.__send__(:holds_new_values?)
1049
1054
  end
1050
1055
 
1051
1056
  # Build a length-k typed output by folding each segment's members with the
@@ -1056,8 +1061,6 @@ class CASegmentIterator < CAIterator
1056
1061
  # for free (an all-masked segment reduces like an empty one; identity-bearing
1057
1062
  # reductions return their identity, ratios return UNDEF).
1058
1063
  def per_segment (kind)
1059
- # a data type the values' own Face answers in names that Face
1060
- kind = @value if ! kind.is_a?(CArray) && @value.face? && kind == @value.data_type
1061
1064
  data_type = kind.is_a?(CArray) ? kind.data_type : kind
1062
1065
  out = if kind.nil?
1063
1066
  CArray.new(CA_OBJECT, [@k])
@@ -1066,13 +1069,13 @@ class CASegmentIterator < CAIterator
1066
1069
  elsif data_type == @value.data_type then @value.bytes
1067
1070
  else 0 end
1068
1071
  CArray.new(data_type, [@k], bytes: bytes)
1069
- elsif ! kind.read_only?
1072
+ elsif kind.__send__(:holds_new_values?)
1070
1073
  # the core answered in a Face, so the output is one too:
1071
1074
  # CATime#min hands back a CATime::Element, which only a CATime
1072
1075
  # has anywhere to put
1073
1076
  kind.__send__(:blank_like, [@k])
1074
1077
  else
1075
- # a read-only Face cannot be filled, so its answers are collected
1078
+ # a Face that holds no new value cannot be filled, so its answers are collected
1076
1079
  # as the surface objects they already are
1077
1080
  CArray.new(CA_OBJECT, [@k])
1078
1081
  end
@@ -662,6 +662,12 @@ class CArray
662
662
  case tag
663
663
  when "object"
664
664
  shape, values, mask, attrs = rest
665
+ # The shape is only a claim until the cells are seen to fill it, and
666
+ # a damaged one can ask for more memory than the machine has.
667
+ unless marshal_cells_fill_shape?(values, shape) &&
668
+ (mask.nil? || marshal_cells_fill_shape?(mask, shape))
669
+ raise ArgumentError, "corrupt CArray Marshal payload (the cells do not fill shape #{shape.inspect})"
670
+ end
665
671
  ca = CArray.object(*shape)
666
672
  ca[] = values
667
673
  if mask
@@ -683,6 +689,22 @@ class CArray
683
689
  end
684
690
  private_class_method :array_from_marshal_payload
685
691
 
692
+ # Whether `cells`, nested as #to_a nests them, has the extent of every axis
693
+ # of `shape`. A cell may itself hold an Array, so only shape.size levels
694
+ # are counted.
695
+ def self.marshal_cells_fill_shape? (cells, shape)
696
+ return false unless shape.is_a?(Array) && !shape.empty? &&
697
+ shape.all? { |extent| extent.is_a?(Integer) && extent >= 0 }
698
+ level = [cells]
699
+ shape.each do |extent|
700
+ return false unless level.all? { |row| row.is_a?(Array) && row.size == extent }
701
+ level = level.flat_map { |row| row }
702
+ return true if level.empty?
703
+ end
704
+ true
705
+ end
706
+ private_class_method :marshal_cells_fill_shape?
707
+
686
708
  end
687
709
 
688
710
  # Marshal of a view, a mask and a scalar: the {CArray#marshal_dump} payload,
@@ -204,7 +204,7 @@ class CASlabIterator < CAIterator
204
204
  return count_not_masked(**kw) if args.empty?
205
205
  # CABlock / CAWindow shadow #count with a geometry accessor, and @reference
206
206
  # may be such a view, so dispatch CArray#count explicitly for count(v).
207
- CArray.instance_method(:count).bind_call(@reference, *args, axis: @slab_axes,
207
+ count_cells(@reference, *args, axis: @slab_axes,
208
208
  **CAIterator.reduce_keywords(:count, kw))
209
209
  end
210
210
 
data/lib/carray/stack.rb CHANGED
@@ -60,7 +60,7 @@ class CArray
60
60
  def self.stack (list, axis: 0, data_type: nil)
61
61
  raise ArgumentError, "stack: list must not be empty" if list.empty?
62
62
  axis = 0 if axis.nil?
63
- list = CArray.promote_list(list, data_type: data_type)
63
+ list = promote_list_for("stack", list, data_type: data_type)
64
64
  axis = CArray.normalize_axis(axis, list[0].ndim + 1, "stack")
65
65
  CAStack.new(list, axis: axis) # CAStack.new does Face lift internally
66
66
  end
@@ -101,7 +101,7 @@ class CArray
101
101
  raise ArgumentError, "meld: entries must be CArray (got #{first.class})"
102
102
  end
103
103
  axis_norm = CArray.normalize_axis(axis, first.ndim, "meld")
104
- arrays = CArray.promote_list(arrays, data_type: data_type)
104
+ arrays = promote_list_for("meld", arrays, data_type: data_type)
105
105
  # Flatten nested CAMeld inputs that share our meld axis: they already
106
106
  # describe a segment sequence, so absorbing their parents keeps chain
107
107
  # depth at 1 (avoids 2-level xfer_all / reduce chains through the
@@ -183,7 +183,7 @@ class CArray
183
183
  "montage: tdim product (#{expected}) must equal list size (#{list.size})"
184
184
  end
185
185
 
186
- list = CArray.promote_list(list, data_type: data_type)
186
+ list = promote_list_for("montage", list, data_type: data_type)
187
187
  parent_shape = list[0].shape
188
188
  ntile = tdim.size
189
189
  nparent = parent_shape.size
data/lib/carray/string.rb CHANGED
@@ -115,22 +115,22 @@ class CAString
115
115
  def mod (other) ; as_text(super) ; end
116
116
  # The later String of each cell and `other`, cell by cell.
117
117
  # @return [CAString]
118
- def pmax (other) ; as_text(super) ; end
118
+ def pmax (other) ; refuse_unordered_operands("pmax", other) ; as_text(super) ; end
119
119
  # The earlier String of each cell and `other`, cell by cell.
120
120
  # @return [CAString]
121
- def pmin (other) ; as_text(super) ; end
121
+ def pmin (other) ; refuse_unordered_operands("pmin", other) ; as_text(super) ; end
122
122
  # Another name of {#pmax} (a String has no NaN to let win).
123
123
  # @return [CAString]
124
- def maximum (other) ; as_text(super) ; end
124
+ def maximum (other) ; refuse_unordered_operands("maximum", other) ; as_text(super) ; end
125
125
  # Another name of {#pmin}.
126
126
  # @return [CAString]
127
- def minimum (other) ; as_text(super) ; end
127
+ def minimum (other) ; refuse_unordered_operands("minimum", other) ; as_text(super) ; end
128
128
  # Another name of {#pmax}.
129
129
  # @return [CAString]
130
- def fmax (other) ; as_text(super) ; end
130
+ def fmax (other) ; refuse_unordered_operands("fmax", other) ; as_text(super) ; end
131
131
  # Another name of {#pmin}.
132
132
  # @return [CAString]
133
- def fmin (other) ; as_text(super) ; end
133
+ def fmin (other) ; refuse_unordered_operands("fmin", other) ; as_text(super) ; end
134
134
 
135
135
  # The String a blank cell holds: the cell a fresh {#template} gives and
136
136
  # the one {#shift} fills by default. Frozen, because every blank cell
@@ -44,6 +44,25 @@ class CArray
44
44
  end
45
45
  private :string_map
46
46
 
47
+ # An ordering (pmax and its kin, clip) with a value that is not a String
48
+ # is refused as String#< refuses it. nil (no bound, for clip), UNDEF,
49
+ # an Array and a CArray are left to the operation.
50
+ def refuse_unordered_operands (verb, *operands)
51
+ operands.each do |v|
52
+ next if v.nil? || v.is_a?(String) || v.is_a?(Array) || v.is_a?(CArray) || UNDEF.equal?(v)
53
+ name = v.is_a?(Symbol) ? v.inspect : v.class
54
+ raise ArgumentError, "#{verb}: comparison of String with #{name} failed"
55
+ end
56
+ end
57
+ private :refuse_unordered_operands
58
+
59
+ # Clips the strings in byte order (see {CArray#clip}); a bound that is
60
+ # not a String is refused.
61
+ def clip (min, max = nil, *rest, **kw)
62
+ refuse_unordered_operands("clip", min, max)
63
+ super
64
+ end
65
+
47
66
  # ---- transforms (String result -> CAString) --------------------------
48
67
 
49
68
  # Per-cell `String#upcase`.
@@ -93,7 +112,7 @@ class CArray
93
112
  def encode (*a) ; string_map { |s| s.encode(*a) } ; end
94
113
  # Per-cell `String#force_encoding`.
95
114
  # @return [CAString] shape and mask of `self` preserved.
96
- def force_encoding (e) ; string_map { |s| s.force_encoding(e) } ; end
115
+ def force_encoding (e) ; string_map { |s| s.dup.force_encoding(e) } ; end
97
116
  # Per-cell `String#scrub`.
98
117
  # @return [CAString] shape and mask of `self` preserved.
99
118
  def scrub () ; string_map { |s| s.scrub } ; end
@@ -221,14 +240,16 @@ class CArray
221
240
  end
222
241
  entity = CArray.new(CA_FIXLEN, shape, :bytes => width)
223
242
  values.each_with_index { |s, i| entity[i] = s.nil? ? UNDEF : s }
224
- CAFixlenString.wrap(entity)
243
+ text = respond_to?(:encoding) ? encoding : CAFixlenString.__send__(:encoding_of_strings, values)
244
+ CAFixlenString.wrap(entity, encoding: text)
225
245
  end
226
246
 
227
247
  # Self (zero-copy) when already a {CAConstString} of the same encoding;
228
248
  # otherwise materialises a compact column.
229
- # @param encoding [Encoding] column encoding.
249
+ # @param encoding [Encoding] column encoding; defaults to the encoding
250
+ # the array carries, and to UTF-8 for one that carries none.
230
251
  # @return [CAConstString]
231
- def to_const_string (encoding: Encoding::UTF_8)
252
+ def to_const_string (encoding: respond_to?(:encoding) ? self.encoding : Encoding::UTF_8)
232
253
  if is_a?(CAConstString) && encoding == self.encoding
233
254
  return self
234
255
  end
@@ -699,7 +699,10 @@ class CAStruct::Builder # :nodoc:
699
699
  # Declares a bitfield member. Bits accumulate within the
700
700
  # current byte; when a byte member follows, the cursor rounds
701
701
  # up to the next byte boundary. Only supported in `:struct`
702
- # (not `:union`) and always requires a name.
702
+ # (not `:union`) and always requires a name. A member of more
703
+ # than one bit keeps the low `bits` bits of an Integer written to
704
+ # it (300 into 3 bits is 4), through an element or the member's
705
+ # column alike; a one-bit member is boolean.
703
706
  # @param name [Symbol, String] member name.
704
707
  # @param bits [Integer] bit width in `1..64`.
705
708
  # @return [void]