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
@@ -158,7 +158,7 @@ class CAFrame
158
158
  g_of = nb > 0 ? occupied / nb : occupied
159
159
  b_of = nb > 0 ? occupied % nb : occupied
160
160
  # Each group's by values, from its first row.
161
- order = gcat.sort_addr
161
+ order = gcat.group_addr
162
162
  first = order[CArray.segment_offsets(lengths: gcat.category_sizes)[0...ng]]
163
163
  columns = {}
164
164
  by_names.zip(by_cols).each do |n, col|
@@ -239,10 +239,11 @@ end
239
239
  # GroupedFrame — the result of +CAFrame#group_by+ (memo §11.6, §16).
240
240
  #
241
241
  # Holds the grouping categorical once (grouping is per-key, computed once
242
- # and shared across every column). Three surfaces:
243
- # grp["col"] -> the CArray group iterator (raw exposure)
244
- # aggregate -> declarative per-column reductions into a new frame
245
- # table { |g| }-> cross-column Ruby escape, g is a per-group view-frame
242
+ # and shared across every column). The surfaces:
243
+ # grp["col"], grp[array] -> the CArray group iterator of a column or array
244
+ # aggregate(spec) -> declarative per-column reductions into a new frame
245
+ # aggregate { |grp| } -> columns built from iterators, run once
246
+ # table { |g| } -> Ruby escape, g is a per-group view-frame
246
247
  class GroupedFrame
247
248
  include CAFrame::ColumnVerbs
248
249
  # +key_names+ are the frame columns the grouping was keyed on. They become
@@ -285,10 +286,40 @@ class GroupedFrame
285
286
  @labels.dup
286
287
  end
287
288
 
288
- # Raw exposure: the CArray group iterator for one column (memo §11.6).
289
- def [](name)
289
+ # The CArray group iterator of a column, or of any CArray with one entry
290
+ # per row of the frame: a value computed from columns, or a column with
291
+ # some cells masked to leave them out. Its reductions answer one value
292
+ # per group, and take the arguments a CArray's reductions take.
293
+ #
294
+ # grp["temp"].percentile(90)
295
+ # grp[df["temp"] * df["w"]].sum / grp["w"].sum
296
+ # grp[df["temp"].mask_where(df["qc"].ne(0))].mean # without the flagged rows
297
+ #
298
+ # The array is matched to the rows by position, as a CArray key of
299
+ # +group_by+ is. An N-D one is grouped along its first axis; pass
300
+ # +axis: 0+ to its reductions to keep the trailing shape.
301
+ #
302
+ # @param source [String, CArray] a column name, or a CArray whose first
303
+ # axis has one entry per row.
304
+ # @return [CACategoricalIterator]
305
+ # @raise [KeyError] for a name that is no column.
306
+ # @raise [ArgumentError] when the CArray's first axis is not the rows.
307
+ def [](source)
290
308
  refuse_changed_rows("[]")
291
- @frame[name].group_by_category(@cat)
309
+ values =
310
+ case source
311
+ when CArray
312
+ rows = source.ndim == 0 ? 0 : source.shape[0]
313
+ unless source.ndim >= 1 && rows == @cat.elements
314
+ raise ArgumentError,
315
+ "[]: the array has #{source.ndim == 0 ? "no axis" : "#{rows} rows"} " \
316
+ "for the frame's #{@cat.elements}"
317
+ end
318
+ source
319
+ else
320
+ @frame[@frame.__send__(:column_key, source, "[]")]
321
+ end
322
+ values.group_by_category(@cat)
292
323
  end
293
324
 
294
325
  # Declarative aggregation (memo §11.6). Spec maps an output column name to
@@ -304,8 +335,34 @@ class GroupedFrame
304
335
  # keyword works for +table+ and the reductions below.
305
336
  #
306
337
  # df.group_by("station").aggregate("temp_mean" => ["temp", :mean], keep_rows: true)
307
- def aggregate(spec = nil, keep_rows: false, **named)
338
+ #
339
+ # Given a block instead of a spec, aggregate yields the grouped frame once
340
+ # and the block returns a Hash of output name => a CArray with one entry
341
+ # per group. The block computes the columns with the grouped frame's
342
+ # reductions, +grp[...]+ iterators and CArray arithmetic, so nothing runs
343
+ # per group in Ruby:
344
+ #
345
+ # df.group_by("station").aggregate do |g|
346
+ # {
347
+ # "temp" => g["temp"].mean,
348
+ # "range" => g["temp"].max - g["temp"].min,
349
+ # "wmean" => g[df["temp"] * df["w"]].sum / g["w"].sum,
350
+ # "n" => g["temp"].group_sizes,
351
+ # }
352
+ # end
353
+ #
354
+ # +keep_rows: true+ lays each column over the rows as for a spec.
355
+ #
356
+ # @raise [ArgumentError] when both a spec and a block are given, or the
357
+ # block returns something other than a Hash of per-group CArrays.
358
+ def aggregate(spec = nil, keep_rows: false, **named, &block)
308
359
  refuse_changed_rows("aggregate")
360
+ if block
361
+ unless spec.nil? && named.empty?
362
+ raise ArgumentError, "aggregate: give a spec or a block, not both"
363
+ end
364
+ return result_frame(block_columns(yield(self)), keep_rows)
365
+ end
309
366
  spec = (spec || {}).merge(named)
310
367
  cols = {}
311
368
  spec.each do |out_name, (in_name, reduction, *args)|
@@ -364,7 +421,9 @@ class GroupedFrame
364
421
  # its trailing shape: a (N, L) column gives (groups, L). Each
365
422
  # takes +min_count:+ and +fill_value:+ as a core reduction does: a group
366
423
  # with fewer than min_count present values is UNDEF, and fill_value fills
367
- # the UNDEF cells. +keep_rows: true+ gives each row its group's value
424
+ # the UNDEF cells. One fill_value goes to every column and has to suit each
425
+ # (a number into a string column raises); a Hash of column name => value
426
+ # fills those columns only. +keep_rows: true+ gives each row its group's value
368
427
  # instead of one row per group, as for +aggregate+.
369
428
  #
370
429
  # Given column names, they reduce those columns only and return them as
@@ -644,8 +703,18 @@ class GroupedFrame
644
703
  # +keep_rows+.
645
704
  private def reduce_columns(reduction, names, keep_rows, **keywords)
646
705
  keywords = keywords.compact
647
- out = over_named_columns(reduction, names, @key_names) do |col|
648
- col = group_reduce(col, reduction, **keywords)
706
+ fills = keywords.delete(:fill_value)
707
+ if fills.is_a?(Hash)
708
+ unknown = fills.keys - @frame.column_names
709
+ unless unknown.empty?
710
+ raise ArgumentError,
711
+ "#{reduction}: fill_value names no column #{unknown.map(&:inspect).join(', ')}"
712
+ end
713
+ end
714
+ out = over_named_columns(reduction, names, @key_names) do |col, name|
715
+ fill = fills.is_a?(Hash) ? fills[name] : fills
716
+ kw = fill.nil? ? keywords : keywords.merge(fill_value: fill)
717
+ col = group_reduce(col, reduction, **kw)
649
718
  keep_rows && !names.empty? ? spread_to_rows(col) : col
650
719
  end
651
720
  return out unless names.empty?
@@ -690,6 +759,24 @@ class GroupedFrame
690
759
  end
691
760
  end
692
761
 
762
+ # The columns an aggregate block returned: a Hash of names to CArrays
763
+ # with one entry per group along their first axis.
764
+ private def block_columns(out)
765
+ unless out.is_a?(Hash)
766
+ raise ArgumentError, "aggregate block must return a Hash (got #{out.class})"
767
+ end
768
+ out.each_with_object({}) do |(name, col), cols|
769
+ name = @frame.__send__(:column_name_arg, name, "aggregate")
770
+ unless col.is_a?(CArray) && col.ndim >= 1 && col.shape[0] == ngroup
771
+ got = col.is_a?(CArray) ? "shape #{col.shape.inspect}" : col.class.to_s
772
+ raise ArgumentError,
773
+ "aggregate: #{name.inspect} must be a CArray with one value per group " \
774
+ "(#{ngroup}), got #{got}"
775
+ end
776
+ cols[name] = col
777
+ end
778
+ end
779
+
693
780
  private def per_group_column(in_name, proc)
694
781
  results = []
695
782
  each_group_slice(in_name) { |slice| results << proc.call(slice) }
@@ -751,7 +838,7 @@ class GroupedFrame
751
838
  end
752
839
 
753
840
  private def group_perm
754
- @group_perm ||= @cat.sort_addr
841
+ @group_perm ||= @cat.group_addr
755
842
  end
756
843
 
757
844
  private def group_bounds
@@ -66,7 +66,9 @@ class CAFrame
66
66
  # attrs: attribute keys for the lines right below the header, one
67
67
  # key per line: field j of a line becomes that attribute of
68
68
  # column j (an empty field gives none). The data then starts
69
- # below them. What to_csv(attrs:) writes, it reads back
69
+ # below them. What to_csv(attrs:) writes, it reads back. The
70
+ # fields are read as the data fields are, so strip: trims them
71
+ # too
70
72
  #
71
73
  # CAFrame.from_csv("obs.csv", header: 2) # a title above
72
74
  # CAFrame.from_csv("obs.csv", header: 0, data: 3) # units on lines 1-2
@@ -160,9 +160,9 @@ class CArray
160
160
 
161
161
  def self.extract (block)
162
162
  sequence = RubyVM::InstructionSequence.of(block) rescue nil
163
- location = sequence && sequence.to_a[4][:code_location]
164
163
  path = sequence && (sequence.absolute_path || sequence.path)
165
- unless location && path && File.readable?(path)
164
+ location = path && File.readable?(path) && code_location(sequence, path)
165
+ unless location
166
166
  raise ArgumentError,
167
167
  "CArray.fuse cannot read this block's source (defined in irb, " \
168
168
  "eval, or a file that is no longer there). Write `.lazy` on " \
@@ -181,6 +181,35 @@ class CArray
181
181
  end
182
182
  end
183
183
 
184
+ # [first_line, first_column, last_line, last_column] of the block. The
185
+ # block that starts on the sequence's first line is found in the file's
186
+ # syntax tree. Only when two start on that line is the sequence itself
187
+ # dumped: InstructionSequence#to_a reads the instructions while another
188
+ # Ractor may be running them, and that can crash the process.
189
+ def self.code_location (sequence, path)
190
+ line = sequence.first_lineno
191
+ found = []
192
+ pending = [Prism.parse_file(path).value]
193
+ until pending.empty?
194
+ node = pending.pop
195
+ next unless node
196
+ # A lambda's code starts after the arrow, at its parameters or brace.
197
+ start = case node
198
+ when Prism::BlockNode then node.location
199
+ when Prism::LambdaNode then (node.parameters || node).location
200
+ end
201
+ start = node.opening_loc if start && node.is_a?(Prism::LambdaNode) && !node.parameters
202
+ found << [start, node.location] if start && start.start_line == line
203
+ pending.concat(node.compact_child_nodes)
204
+ end
205
+ if found.size == 1
206
+ start, whole = found.first
207
+ [start.start_line, start.start_column, whole.end_line, whole.end_column]
208
+ else
209
+ sequence.to_a[4][:code_location]
210
+ end
211
+ end
212
+
184
213
  # -- the rewrite -------------------------------------------------------
185
214
 
186
215
  # The leaves are the names being read. Everything else keeps its shape:
data/lib/carray/lazy.rb CHANGED
@@ -330,9 +330,28 @@ class CArray
330
330
  # branch uses C99 `remainder`, round-half-to-even).
331
331
  }.freeze
332
332
 
333
+ # A Face answers an operator by its own values (a label, an instant, a
334
+ # String) and refuses what it can't answer; the lazy kernels read the
335
+ # storage and see neither. With a Face among the operands the operator is
336
+ # the eager one, on the lazy operands computed.
337
+ module LazyDispatch
338
+ private
339
+
340
+ def face_among_operands? (*operands)
341
+ operands.any? { |o| o.is_a?(CArray) && o.face? }
342
+ end
343
+
344
+ def computed_operand (operand)
345
+ ( operand.is_a?(CArray) && operand.lazy_view? ) ? operand.copy : operand
346
+ end
347
+ end
348
+
333
349
  CArray.send(:define_named_methods, LazyDispatch, LAZY_BINOP_OP_IDS, template: <<~'RUBY')
334
350
  def __NAME__(other)
335
351
  if lazy_view? || (other.is_a?(CArray) && other.lazy_view?)
352
+ if face_among_operands?(self, other)
353
+ return computed_operand(self).__send__(__method__, computed_operand(other))
354
+ end
336
355
  CABinOp.__build__(self, other, __VALUE__)
337
356
  else
338
357
  super(other)
@@ -403,6 +422,10 @@ class CArray
403
422
  (op2.is_a?(CArray) && op2.lazy_view?) ||
404
423
  (op3.is_a?(CArray) && op3.lazy_view?)
405
424
  if lazy_receiver
425
+ if face_among_operands?(self, op2, op3)
426
+ return computed_operand(self).__send__(__method__, computed_operand(op2),
427
+ computed_operand(op3))
428
+ end
406
429
  CATriOp.__build__(self, op2, op3, __VALUE__)
407
430
  else
408
431
  super(op2, op3)
@@ -435,6 +458,10 @@ class CArray
435
458
  # is already fast. The lazy-substrate gap was Float/Complex only.
436
459
  module LazyDispatch
437
460
  def **(other)
461
+ if ( lazy_view? || (other.is_a?(CArray) && other.lazy_view?) ) &&
462
+ face_among_operands?(self, other)
463
+ return computed_operand(self) ** computed_operand(other)
464
+ end
438
465
  if other.is_a?(Integer) && (float? || complex?) &&
439
466
  (lazy_view? || (other.is_a?(CArray) && other.lazy_view?))
440
467
  CABinOp.__build__(self, CScalar.int64.tap { |s| s[0] = other },
@@ -492,6 +519,9 @@ class CArray
492
519
  CArray.send(:define_named_methods, LazyDispatch, LAZY_BINCMP_OP_IDS, template: <<~'RUBY')
493
520
  def __NAME__(other)
494
521
  if lazy_view? || (other.is_a?(CArray) && other.lazy_view?)
522
+ if face_among_operands?(self, other)
523
+ return computed_operand(self).__send__(__method__, computed_operand(other))
524
+ end
495
525
  CABinCmp.__build__(self, other, __VALUE__)
496
526
  else
497
527
  super(other)
@@ -511,6 +541,9 @@ class CArray
511
541
  CArray.send(:define_named_methods, LazyDispatch, LAZY_BINCMP_TOL_OP_IDS, template: <<~'RUBY')
512
542
  def __NAME__(other, tol)
513
543
  if lazy_view? || (other.is_a?(CArray) && other.lazy_view?)
544
+ if face_among_operands?(self, other)
545
+ return computed_operand(self).__send__(__method__, computed_operand(other), tol)
546
+ end
514
547
  CABinCmp.__build__(self, other, __VALUE__, tol.to_f.abs)
515
548
  else
516
549
  super(other, tol)
@@ -598,6 +631,8 @@ class CAMonOp
598
631
  op_id = __op_id__
599
632
  if op_id >= CAMonOp::CAST_BASE
600
633
  "cast_#{CArray.data_type_name(op_id - CAMonOp::CAST_BASE)}"
634
+ elsif op_id == CAMonOp::OP_BYTE_SWAP
635
+ "swap_bytes" # a view of its own, not a lazy operation by name
601
636
  else
602
637
  (OP_NAMES[op_id] || "op_#{op_id}").to_s
603
638
  end
@@ -226,8 +226,15 @@ class CArray
226
226
  # set for not matching self. Same rule the scalar operand of a
227
227
  # comparison follows: a String standing in for a cell is padded to the
228
228
  # cell's width.
229
+ # A String longer than the cell keeps its length: self is read at its
230
+ # width instead (NUL-padded), so "abX" is in no two-byte cell.
231
+ w = elems.map { |v| v.is_a?(String) ? v.bytesize : 0 }.max.to_i
232
+ me = self
233
+ if w > bytes
234
+ me = is_a?(CAFixlenString) ? to_fixlen_string(bytes: w) : to_type(CA_FIXLEN, bytes: w)
235
+ end
229
236
  # coerce: a fixlen or object set holds values of self's kind (see above)
230
- [self, elems.to_ca.to_type(data_type, bytes: bytes)]
237
+ [me, elems.to_ca.to_type(data_type, bytes: me.bytes)]
231
238
  elsif data_type == CA_OBJECT
232
239
  # coerce: an object set holds values of self's kind
233
240
  [self, elems.to_ca.to_type(data_type)]
@@ -252,12 +252,20 @@ class CASegmentIterator < CAIterator
252
252
  return axis_by_masked_copy(axis, :variance) if axis
253
253
  m = moments
254
254
  return per_segment(core_reduce_kind(:variance)) { |s| s.variance } unless m
255
+ centred_variance(m, 1)
256
+ end
257
+
258
+ # The kernel divides the centred sum of squares by count - ddof once, as
259
+ # the core reduction does; scaling the sample variance by (n-1)/n instead
260
+ # rounds twice and lands an ulp away.
261
+ def centred_variance (m, ddof)
255
262
  cnt = m[:count]
256
- means = m[:sum] / cnt.float64 # per-segment mean (garbage where count 0/1,
257
- out = CArray.float64(@k) # ignored by the kernel's n<2 guards)
258
- grouped.send(:__reduceat_variance__, @offsets, means, cnt, out)
263
+ means = m[:sum] / cnt.float64 # per-segment mean (garbage where the count
264
+ out = CArray.float64(@k) # is too small; the kernel masks those)
265
+ grouped.send(:__reduceat_variance__, @offsets, means, cnt, out, ddof)
259
266
  out
260
267
  end
268
+ private :centred_variance
261
269
 
262
270
  # @overload stddev
263
271
  # Returns per-segment SAMPLE standard deviation (ddof=1) as float64.
@@ -375,11 +383,7 @@ class CASegmentIterator < CAIterator
375
383
  return axis_by_masked_copy(axis, :variancep) if axis
376
384
  m = moments
377
385
  return per_segment(core_reduce_kind(:variancep)) { |s| s.variancep } unless m
378
- cnt = m[:count]
379
- vp = variance * (cnt - 1).float64 / cnt.float64
380
- one = cnt.eq(1) # one value: defined, unlike variance --
381
- vp[one] = (m[:sum] - m[:sum])[one] # 0.0, or NaN for a NaN or an infinity
382
- vp
386
+ centred_variance(m, 0) # one value: 0.0, or NaN for a NaN or an infinity
383
387
  end
384
388
 
385
389
  # @overload stddevp
@@ -141,7 +141,15 @@ class CArray::Serializer # :nodoc:
141
141
  "keep its values with Marshal.dump(ca.to_type(:object))"
142
142
  end
143
143
 
144
- file_endian = endian || CArray.endian
144
+ file_endian = case endian
145
+ when nil, :native then CArray.endian
146
+ when :big, CA_BIG_ENDIAN then CA_BIG_ENDIAN
147
+ when :little, CA_LITTLE_ENDIAN then CA_LITTLE_ENDIAN
148
+ else
149
+ raise ArgumentError,
150
+ "endian: must be :big, :little, :native, " \
151
+ "CA_BIG_ENDIAN or CA_LITTLE_ENDIAN (got #{endian.inspect})"
152
+ end
145
153
  swap = (file_endian != CArray.endian)
146
154
 
147
155
  elements = ca.elements
@@ -383,6 +391,7 @@ class CArray::Serializer # :nodoc:
383
391
  "members" => members,
384
392
  }
385
393
  schema["name"] = klass.name if klass.name
394
+ schema["order_by"] = klass.order_by.map(&:to_s) if klass.order_by
386
395
  schema
387
396
  end
388
397
 
@@ -439,7 +448,15 @@ class CArray::Serializer # :nodoc:
439
448
  members = schema["members"]
440
449
  record_bytes = schema["record_bytes"]
441
450
  builder = (schema["kind"] == "union") ? CArray.method(:union) : CArray.method(:struct)
442
- builder.call(:pack => 1, :size => record_bytes) do
451
+ order_by = schema["order_by"]
452
+ names = members.map { |m| m["name"].to_s }
453
+ if order_by && !(order_by.is_a?(Array) && order_by.all? { |n| names.include?(n.to_s) })
454
+ raise ArgumentError, "corrupt CArray binary data (order_by #{order_by.inspect} " \
455
+ "names a member the data_class does not have)"
456
+ end
457
+ opts = { :pack => 1, :size => record_bytes }
458
+ opts[:order_by] = order_by.map(&:to_sym) if order_by
459
+ builder.call(**opts) do
443
460
  members.each do |m|
444
461
  sym = PEP_TO_SYMBOL[m["type"]]
445
462
  unless sym
data/lib/carray/stack.rb CHANGED
@@ -69,27 +69,28 @@ class CArray
69
69
  # No data is copied; reads gather from parents on demand and writes flow
70
70
  # back to them (chain composability preserved).
71
71
  #
72
- # Pieces must agree on `ndim`, `data_type`, byte width, and every axis
73
- # length except `axis` (the "meld axis"). Mismatched `data_type` raises:
74
- # cast the pieces yourself (`.to_type(:float64)`) or use
75
- # {CArray.concatenate} (eager, auto-casts).
72
+ # Pieces must agree on `ndim` and every axis length except `axis` (the
73
+ # "meld axis"). Pieces of different data types are brought to a common
74
+ # one as {CArray.stack} brings them (`promote_list`), through read-only
75
+ # views of the pieces that need it.
76
76
  #
77
77
  # "meld" = melt + weld — pieces dissolve their boundaries along the named
78
78
  # axis and are regarded as one.
79
79
  #
80
- # @overload meld(*arrays, axis: 0)
81
- # @overload meld(list, axis: 0)
80
+ # @overload meld(*arrays, axis: 0, data_type: nil)
81
+ # @overload meld(list, axis: 0, data_type: nil)
82
82
  # Convenience form: a single Array argument is treated as the list.
83
83
  # @param arrays [Array<CArray>] pieces to weld. A single Array
84
84
  # argument is accepted for compatibility with older callers.
85
85
  # @param axis [Integer, nil] existing axis to extend (normalises
86
86
  # negative values against the reference ndim); `nil` is the same
87
87
  # as `0`.
88
+ # @param data_type [Symbol, Integer, nil] result `data_type`;
89
+ # inferred when `nil`.
88
90
  # @return [CAMeld] view over the welded pieces.
89
91
  # @raise [ArgumentError] when the list is empty, ndim mismatch,
90
- # data_type mismatch, or non-axis dim mismatch across pieces
91
- # (surfaced by CAMeld.new).
92
- def self.meld (*arrays, axis: 0)
92
+ # or non-axis dim mismatch across pieces (surfaced by CAMeld.new).
93
+ def self.meld (*arrays, axis: 0, data_type: nil)
93
94
  if arrays.length == 1 && arrays[0].is_a?(Array)
94
95
  arrays = arrays[0]
95
96
  end
@@ -100,6 +101,7 @@ class CArray
100
101
  raise ArgumentError, "meld: entries must be CArray (got #{first.class})"
101
102
  end
102
103
  axis_norm = CArray.normalize_axis(axis, first.ndim, "meld")
104
+ arrays = CArray.promote_list(arrays, data_type: data_type)
103
105
  # Flatten nested CAMeld inputs that share our meld axis: they already
104
106
  # describe a segment sequence, so absorbing their parents keeps chain
105
107
  # depth at 1 (avoids 2-level xfer_all / reduce chains through the
@@ -119,14 +121,15 @@ class CArray
119
121
  # Instance form of {CArray.meld}; non-destructive, see the class method
120
122
  # for full semantics.
121
123
  #
122
- # @overload meld(*others, axis: 0)
124
+ # @overload meld(*others, axis: 0, data_type: nil)
123
125
  # @param others [Array<CArray>] additional pieces.
124
126
  # @param axis [Integer] existing axis to extend.
127
+ # @param data_type [Symbol, Integer, nil] result `data_type`.
125
128
  # @return [CAMeld]
126
129
  # @raise [ArgumentError] when no `others` are given.
127
- def meld (*others, axis: 0)
130
+ def meld (*others, axis: 0, data_type: nil)
128
131
  raise ArgumentError, "meld: at least one other array required" if others.empty?
129
- CArray.meld(self, *others, axis: axis)
132
+ CArray.meld(self, *others, axis: axis, data_type: data_type)
130
133
  end
131
134
 
132
135
  # Arrange `list` of uniform-shape pieces in a `tdim`-shape grid that
data/lib/carray/string.rb CHANGED
@@ -57,6 +57,14 @@ class CArray
57
57
  else
58
58
  values = Array.new(n) { |i| block.call(i) }
59
59
  end
60
+ elsif arg.is_a?(Integer)
61
+ raise ArgumentError,
62
+ "CArray.string: a count is given with a block that makes each " \
63
+ "String (CArray.string(n) { |i| ... })"
64
+ elsif arg.is_a?(String) || ! arg.respond_to?(:to_a)
65
+ raise TypeError,
66
+ "CArray.string: expected an Array of Strings, an object array or " \
67
+ "a String array (got #{arg.class})"
60
68
  else
61
69
  values = arg.to_a
62
70
  end
@@ -31,11 +31,14 @@ class CArray
31
31
  # (integer / boolean results). Shape and mask are carried.
32
32
  def string_map (data_type = nil) # :nodoc:
33
33
  out = data_type ? template(data_type) : CArray.object(*shape)
34
+ # A typed result has no nil: a cell the String method answers nil for
35
+ # (index finds nothing) is UNDEF.
36
+ cell = data_type ? ->(s) { v = yield(s); v.nil? ? UNDEF : v } : ->(s) { yield(s) }
34
37
  if has_mask?
35
38
  m = is_masked
36
- elements.times { |i| out[i] = (m[i] ? UNDEF : yield(self[i])) }
39
+ elements.times { |i| out[i] = (m[i] ? UNDEF : cell.(self[i])) }
37
40
  else
38
- elements.times { |i| out[i] = yield(self[i]) }
41
+ elements.times { |i| out[i] = cell.(self[i]) }
39
42
  end
40
43
  data_type ? out : CAString.wrap(out)
41
44
  end
@@ -124,10 +127,12 @@ class CArray
124
127
  # @return [CArray] `:int`, shape and mask of `self` preserved.
125
128
  def bytesize () ; string_map(:int) { |s| s.bytesize } ; end
126
129
  # Per-cell `String#index`.
127
- # @return [CArray] `:int`, shape and mask of `self` preserved.
130
+ # @return [CArray] `:int`, shape and mask of `self` preserved; UNDEF
131
+ # where the cell does not hold the substring.
128
132
  def str_index (*a) ; string_map(:int) { |s| s.index(*a) } ; end
129
133
  # Per-cell `String#rindex`.
130
- # @return [CArray] `:int`, shape and mask of `self` preserved.
134
+ # @return [CArray] `:int`, shape and mask of `self` preserved; UNDEF
135
+ # where the cell does not hold the substring.
131
136
  def rindex (*a) ; string_map(:int) { |s| s.rindex(*a) } ; end
132
137
 
133
138
  # ---- numeric parse (Integer / Float result) --------------------------
@@ -191,6 +196,9 @@ class CArray
191
196
  unless [:error, :silent].include?(truncate)
192
197
  raise ArgumentError, "truncate: must be :error or :silent (got #{truncate.inspect})"
193
198
  end
199
+ unless bytes.nil? || (bytes.is_a?(Integer) && bytes >= 1)
200
+ raise ArgumentError, "to_fixlen_string: invalid bytes: #{bytes.inspect} (a cell needs bytes: of 1 or more)"
201
+ end
194
202
  return self if is_a?(CAFixlenString) && (bytes.nil? || bytes == self.bytes)
195
203
 
196
204
  if has_mask?
@@ -200,7 +208,7 @@ class CArray
200
208
  values = Array.new(elements) { |i| self[i].to_s }
201
209
  end
202
210
  width = bytes || values.compact.map(&:bytesize).max || 1
203
- width = 1 if width < 1
211
+ width = 1 if width < 1 # every string empty: the narrowest cell
204
212
  if truncate == :error
205
213
  values.each_with_index do |s, i|
206
214
  next if s.nil?
@@ -224,11 +224,13 @@ class CAStruct::Builder # :nodoc:
224
224
  end
225
225
  vtype = {1 => :uint8, 2 => :uint16, 4 => :uint32, 8 => :uint64}[view_bytes]
226
226
  range = bit_in_byte..(bit_in_byte + bits - 1)
227
+ # The word is read little-endian on every host: bits are numbered
228
+ # byte by byte from the record's start, as the layout places them.
227
229
  reader = ->(data) {
228
- data.field(start_byte, vtype).bitfield(range)[0]
230
+ data.field(start_byte, vtype).endian(:little).bitfield(range)[0]
229
231
  }
230
232
  writer = ->(data, val) {
231
- data.field(start_byte, vtype).bitfield(range)[0] = val
233
+ data.field(start_byte, vtype).endian(:little).bitfield(range)[0] = val
232
234
  }
233
235
  [reader, writer].freeze
234
236
  end
@@ -241,7 +243,7 @@ class CAStruct::Builder # :nodoc:
241
243
  def self.build_nested_struct_dispatcher (offset, nested_class)
242
244
  reader = ->(data) {
243
245
  element = nested_class.allocate
244
- element.instance_variable_set(:@data, data.field(offset, nested_class).parent)
246
+ element.instance_variable_set(:@data, data.field(offset, nested_class).send(:face_storage))
245
247
  element.freeze if data.frozen?
246
248
  element
247
249
  }