carray 3.0.5 → 3.0.6

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 (119) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +233 -0
  3. data/README.md +1 -1
  4. data/carray.gemspec +3 -2
  5. data/ext/ca_axis_dispatch.c +6 -0
  6. data/ext/ca_axis_group.c +1 -1
  7. data/ext/ca_categorical_iterator.c +7 -7
  8. data/ext/ca_kernel_iterator.c +116 -231
  9. data/ext/ca_kernel_iterator.h +410 -254
  10. data/ext/ca_obj_bincmp.c +9 -1
  11. data/ext/ca_obj_binop.c +10 -1
  12. data/ext/ca_obj_bitfield.c +2 -0
  13. data/ext/ca_obj_byte_swap.c +2 -0
  14. data/ext/ca_obj_face.c +2 -0
  15. data/ext/ca_obj_fake.c +3 -1
  16. data/ext/ca_obj_grid.c +167 -12
  17. data/ext/ca_obj_meld.c +69 -113
  18. data/ext/ca_obj_moncmp.c +9 -1
  19. data/ext/ca_obj_monop.c +40 -2
  20. data/ext/ca_obj_object.c +54 -20
  21. data/ext/ca_obj_reduce.c +15 -1
  22. data/ext/ca_obj_refer.c +65 -20
  23. data/ext/ca_obj_remap.c +69 -10
  24. data/ext/ca_obj_roll.c +1 -0
  25. data/ext/ca_obj_select.c +4 -1
  26. data/ext/ca_obj_select_axis.c +1 -0
  27. data/ext/ca_obj_shift.c +1 -0
  28. data/ext/ca_obj_stack.c +310 -111
  29. data/ext/ca_obj_stride.c +131 -45
  30. data/ext/ca_obj_string.c +18 -2
  31. data/ext/ca_obj_tile.c +1 -0
  32. data/ext/ca_obj_triop.c +9 -1
  33. data/ext/ca_obj_window.c +1 -0
  34. data/ext/ca_op_ipower.c +8 -8
  35. data/ext/caframe_csv_reader.c +1 -1
  36. data/ext/carray.h +2 -1
  37. data/ext/carray_access.c +27 -64
  38. data/ext/carray_address_basis.c +134 -0
  39. data/ext/carray_attribute.c +8 -0
  40. data/ext/carray_broadcast.c +3 -4
  41. data/ext/carray_build_flags.h +1 -1
  42. data/ext/carray_cast.c +8 -11
  43. data/ext/carray_conversion.c +9 -5
  44. data/ext/carray_core.c +811 -72
  45. data/ext/carray_element.c +13 -9
  46. data/ext/carray_histogram.c +25 -3
  47. data/ext/carray_internal.h +147 -12
  48. data/ext/carray_kernels_bincmp.c +2007 -3
  49. data/ext/carray_kernels_binop.c +25365 -5199
  50. data/ext/carray_kernels_init.c +8 -8
  51. data/ext/carray_kernels_monop.c +10696 -3678
  52. data/ext/carray_kernels_reduce_aggregate.c +4429 -702
  53. data/ext/carray_kernels_reduce_boolean.c +1533 -15
  54. data/ext/carray_kernels_reduce_cumulative.c +1808 -248
  55. data/ext/carray_kernels_reduce_extreme.c +4508 -511
  56. data/ext/carray_kernels_reduce_variance.c +2284 -152
  57. data/ext/carray_kernels_scan.c +240 -240
  58. data/ext/carray_kernels_search.c +4211 -264
  59. data/ext/carray_kernels_sort.c +367 -450
  60. data/ext/carray_lazy.c +47 -0
  61. data/ext/carray_loop.c +1 -1
  62. data/ext/carray_mask.c +130 -139
  63. data/ext/carray_median_percentile.c +4 -4
  64. data/ext/carray_memory_view.c +13 -7
  65. data/ext/carray_operator.c +147 -44
  66. data/ext/carray_order.c +3 -2
  67. data/ext/carray_random.c +4 -1
  68. data/ext/carray_slab.c +97 -21
  69. data/ext/carray_sort.c +1 -1
  70. data/ext/carray_test.c +74 -19
  71. data/ext/carray_time_text.c +2 -2
  72. data/ext/extconf.rb +11 -0
  73. data/ext/mkkernel.rb +346 -94
  74. data/ext/ruby_carray.c +4 -1
  75. data/ext/version.h +4 -4
  76. data/lib/carray/attribute.rb +25 -1
  77. data/lib/carray/autoload_carray.rb +3 -3
  78. data/lib/carray/autoload_method_extension.rb +5 -3
  79. data/lib/carray/axis_group.rb +13 -7
  80. data/lib/carray/basics.rb +1 -1
  81. data/lib/carray/block_iterator.rb +17 -3
  82. data/lib/carray/categorical.rb +6 -8
  83. data/lib/carray/categorical_iterator.rb +2 -2
  84. data/lib/carray/complex.rb +2 -2
  85. data/lib/carray/conditional.rb +3 -2
  86. data/lib/carray/frame/column_verbs.rb +45 -14
  87. data/lib/carray/frame/convert.rb +19 -0
  88. data/lib/carray/frame/csv_parser.rb +32 -32
  89. data/lib/carray/frame/describe.rb +4 -2
  90. data/lib/carray/frame/frame.rb +399 -78
  91. data/lib/carray/frame/group.rb +679 -53
  92. data/lib/carray/frame/io.rb +209 -71
  93. data/lib/carray/frame/join.rb +146 -37
  94. data/lib/carray/frame/records.rb +3 -3
  95. data/lib/carray/frame/reshape.rb +89 -11
  96. data/lib/carray/frame/sort.rb +57 -9
  97. data/lib/carray/frame/verbs.rb +384 -49
  98. data/lib/carray/frame/windows.rb +44 -6
  99. data/lib/carray/inspect.rb +7 -2
  100. data/lib/carray/iterator.rb +32 -0
  101. data/lib/carray/lazy.rb +17 -8
  102. data/lib/carray/mask_gap_fill.rb +37 -17
  103. data/lib/carray/meld_reduce.rb +1 -1
  104. data/lib/carray/methods/choose.rb +3 -2
  105. data/lib/carray/methods/meshgrid.rb +3 -1
  106. data/lib/carray/runtime.rb +47 -8
  107. data/lib/carray/segment_iterator.rb +23 -8
  108. data/lib/carray/serialize.rb +34 -17
  109. data/lib/carray/stack.rb +18 -6
  110. data/lib/carray/time.rb +5 -0
  111. data/lib/carray/window_iterator.rb +16 -4
  112. data/yard-stubs/ca_obj_array.rb +26 -1
  113. data/yard-stubs/carray_cast.rb +8 -5
  114. data/yard-stubs/carray_conversion.rb +8 -2
  115. data/yard-stubs/carray_mask.rb +20 -1
  116. data/yard-stubs/carray_math.rb +31 -16
  117. data/yard-stubs/carray_memory_view.rb +7 -0
  118. data/yard-stubs/carray_test.rb +7 -4
  119. metadata +18 -4
data/ext/ruby_carray.c CHANGED
@@ -14,6 +14,7 @@
14
14
  ---------------------------------------------------------------------------- */
15
15
 
16
16
  #include "carray.h"
17
+ #include "carray_internal.h"
17
18
  #include "carray_build_flags.h"
18
19
  #include "version.h"
19
20
 
@@ -313,7 +314,7 @@ Init_carray_ext (void)
313
314
  Init_carray_sort(); /* sort / sort_copy / sort_addr / axis2addr */
314
315
  Init_carray_count();
315
316
  Init_carray_kernels(); /* mkkernel-generated: reductions + maps */
316
- Init_carray_round(); /* round(ndigits); after the generated round */
317
+ Init_carray_round(); /* round(ndigits) / round!(ndigits) */
317
318
 
318
319
  Init_carray_utils();
319
320
 
@@ -394,5 +395,7 @@ Init_carray_ext (void)
394
395
  Init_carray_slab();
395
396
 
396
397
  Init_carray_address_basis();
398
+
399
+ ca_obj_num_builtin = ca_obj_num;
397
400
  }
398
401
 
data/ext/version.h CHANGED
@@ -1,6 +1,6 @@
1
- #define CA_VERSION "3.0.5"
2
- #define CA_VERSION_CODE 30005
1
+ #define CA_VERSION "3.0.6"
2
+ #define CA_VERSION_CODE 30006
3
3
  #define CA_VERSION_MAJOR 3
4
4
  #define CA_VERSION_MINOR 0
5
- #define CA_VERSION_TEENY 5
6
- #define CA_VERSION_DATE "2026/10/20"
5
+ #define CA_VERSION_TEENY 6
6
+ #define CA_VERSION_DATE "2026/10/28"
@@ -6,6 +6,7 @@
6
6
  # ca.set_attr(:unit, "m/s") # => setter; writes to self's @attr
7
7
  # ca.set_attrs(other.attrs) # => sets every key of a Hash at once
8
8
  # ca.attrs # => frozen shallow clone of the full Hash
9
+ # ca.attrs_symkey # => the same, with Symbol keys (nested too)
9
10
  # ca.has_attr? # => any attribute present?
10
11
  # ca.has_attr?(:unit) # => specific key present?
11
12
  #
@@ -162,6 +163,15 @@ class CArray
162
163
  (merged || {}).freeze
163
164
  end
164
165
 
166
+ # @overload attrs_symkey
167
+ # Returns {#attrs} with its keys as Symbols, frozen. The keys of
168
+ # Hashes nested in the values, inside Arrays too, become Symbols
169
+ # as well.
170
+ # @return [Hash{Symbol => Object}]
171
+ def attrs_symkey
172
+ return CArray.__send__(:attr_symbolize_keys, self.attrs)
173
+ end
174
+
165
175
  # @overload has_attr?
166
176
  # Returns whether `self` (or any parent view) has any attribute
167
177
  # set.
@@ -317,6 +327,20 @@ class CArray
317
327
  v
318
328
  end
319
329
  end
320
- private_class_method :attr_normalize_key, :attr_validate_value, :attr_coerce_value
330
+ # A stored value with the keys of every Hash in it as Symbols, frozen
331
+ # as the stored value is.
332
+ def self.attr_symbolize_keys (v)
333
+ case v
334
+ when Array then v.map { |e| attr_symbolize_keys(e) }.freeze
335
+ when Hash
336
+ v.each_with_object({}) do |(k, vv), h|
337
+ h[k.to_sym] = attr_symbolize_keys(vv)
338
+ end.freeze
339
+ else
340
+ v
341
+ end
342
+ end
343
+ private_class_method :attr_normalize_key, :attr_validate_value, :attr_coerce_value,
344
+ :attr_symbolize_keys
321
345
 
322
346
  end
@@ -32,12 +32,12 @@ class CArray
32
32
  autoload_method "marshal_load", serialize_rb
33
33
  end
34
34
 
35
- # A view, a wrap and a scalar are marshalled through _dump / self._load,
35
+ # A view, a mask, a wrap and a scalar are marshalled through _dump / self._load,
36
36
  # which return a new array: with marshal_dump Marshal records the class and
37
37
  # loads into an empty instance of it, which the plain array the payload
38
38
  # holds cannot become. A record array keeps marshal_dump, under which it
39
39
  # already round-trips with its class.
40
- [CAView, CAWrap, CScalar].each do |klass|
40
+ [CAView, CAWrap, CScalar, CArrayMask, CAObjectMask].each do |klass|
41
41
  klass.class_eval do
42
42
  undef_method :marshal_dump
43
43
  autoload_method "_dump", "carray/serialize"
@@ -137,7 +137,7 @@ autoload :CAUnion, "carray/struct"
137
137
  # ---- CAFrame (DataFrame over CArray columns) -------------------------------
138
138
  #
139
139
  # The frame is only reached through the CAFrame constant (CAFrame.new /
140
- # .from_csv / subclassing) or GroupedFrame (obtained from CAFrame#group_by,
140
+ # .read_csv / subclassing) or GroupedFrame (obtained from CAFrame#group_by,
141
141
  # by which point CAFrame is already loaded). Both constants live in the same
142
142
  # require bundle (carray/frame.rb), so autoloading either loads the whole
143
143
  # frame. Nothing in the eager core references them, so it loads on first use.
@@ -31,9 +31,11 @@ module AutoloadMethodExtension
31
31
 
32
32
  private
33
33
 
34
- def autoload_define (target, name, library, original_spec)
35
- library = Ractor.make_shareable(library.to_s.dup)
36
- original_spec = Ractor.make_shareable(original_spec.to_s.dup)
34
+ def autoload_define (target, name, library_given, spec_given)
35
+ # The stub reads these two, so they are never assigned again: Ruby 4.0
36
+ # refuses to make a Proc shareable when a variable it reads may be.
37
+ library = Ractor.make_shareable(library_given.to_s.dup)
38
+ original_spec = Ractor.make_shareable(spec_given.to_s.dup)
37
39
  # Shareable, so the stub answers in any Ractor. Only the main Ractor
38
40
  # loads the library: Ractors that require at the same time can deadlock
39
41
  # (Ruby 3.4), so another Ractor is told what to load there instead.
@@ -409,7 +409,7 @@ class CAGroupIterator
409
409
  # except for a band folded into the count, which has no route but the
410
410
  # engine.
411
411
  def core_counts? (kw)
412
- return false if CArray.__fast_paths_enabled__
412
+ return false if CArray.__fast_paths_enabled__(:axis_group_count_kernel)
413
413
  has_group, fused = AxisGroup.parse_axis(kw[:axis])
414
414
  has_group && fused.empty?
415
415
  end
@@ -494,8 +494,8 @@ class CAGroupIterator
494
494
  # Weighted mean = Sum(v*w) / Sum(w), both over the combined present-set. The
495
495
  # `value*0 + weights` denominator carries the same value|weight mask, so its
496
496
  # group-sum is Sum(w) over exactly the cells value*weight used. An empty group
497
- # (no present pair) is UNDEF (matching CArray#wmean); a present group whose
498
- # weights sum to zero yields NaN/Inf (core's 0/0).
497
+ # (no present pair) and a group whose weights sum to zero are UNDEF, as
498
+ # CArray#wmean leaves both.
499
499
  def wmean (weights, **kw)
500
500
  has_group, = AxisGroup.parse_axis(kw[:axis])
501
501
  return value.wmean(weights, **kw) unless has_group
@@ -508,7 +508,7 @@ class CAGroupIterator
508
508
  # No present pair is UNDEF (an empty group), set on the denominator so
509
509
  # the division never meets an empty group's 0 / 0.
510
510
  den = den.copy
511
- den[cnt.eq(0)] = UNDEF
511
+ den[cnt.eq(0).or(den.eq(0))] = UNDEF
512
512
  # min_count: counts the present pairs, as the core wmean does.
513
513
  pairs = regroup(prod, spec)
514
514
  pairs.finish_reduction(:wmean, kw, num / den)
@@ -863,7 +863,7 @@ class CAGroupIterator
863
863
  # pass. With the fast paths off it goes through the band loop instead,
864
864
  # which then has one position.
865
865
  def whole_block? (bslots)
866
- bslots.empty? && CArray.__fast_paths_enabled__
866
+ bslots.empty? && CArray.__fast_paths_enabled__(:axis_group_whole_block)
867
867
  end
868
868
 
869
869
  # Iterate the band positions of a band-preserving grouping. For each band
@@ -923,9 +923,15 @@ class CAGroupIterator
923
923
  # paths off (a development build run with CARRAY_GENERAL_PATH set) each
924
924
  # group is reduced by the core instead -- except a band folded into the
925
925
  # statistic (+fused+), which has no route but the kernel.
926
+ #
927
+ # With no band axis the core groups the whole value in one pass, which is
928
+ # faster than the kernel for every group size (1M cells on an M2 Max: 7.5
929
+ # to 12.5 ms against 15 ms), so the kernel is taken only with a band.
926
930
  def kernel_reads_value? (fused = [])
927
931
  return false if value.face? || ! (value.integer? || value.float?)
928
- CArray.__fast_paths_enabled__ || ! fused.empty?
932
+ return true unless fused.empty?
933
+ return false if spec.slot_meta.none? { |m| m[:kind] == :band }
934
+ CArray.__fast_paths_enabled__(:axis_group_reduce_kernel)
929
935
  end
930
936
 
931
937
  # Whether the scan kernel gives the core's answer: it reads an integer,
@@ -933,7 +939,7 @@ class CAGroupIterator
933
939
  # float64 (an object in object), cummax / cummin in the payload's data type
934
940
  # and cumcount in int64 -- not the core's answer for a boolean's running sum.
935
941
  def kernel_scans? (op)
936
- return false unless CArray.__fast_paths_enabled__
942
+ return false unless CArray.__fast_paths_enabled__(:axis_group_scan_kernel)
937
943
  return false if value.face? || value.complex?
938
944
  return true if value.object? || op == :cumcount
939
945
  core = CAIterator.core_probe(value).public_send(op).data_type
data/lib/carray/basics.rb CHANGED
@@ -122,7 +122,7 @@ class CArray
122
122
  # @return [CArray] view with reduced `ndim`.
123
123
  def drop_axis
124
124
  if ndim == 1
125
- return self[]
125
+ return scalar? ? self : self[]
126
126
  else
127
127
  newdim = shape.reject{|x| x == 1 }
128
128
  newdim = [1] if newdim.empty? # every axis has size 1: keep one
@@ -74,6 +74,14 @@ class CABlockIterator < CAIterator
74
74
  "blocks: expected #{source.ndim} tile sizes (one per axis), " \
75
75
  "got #{blocks.size}"
76
76
  end
77
+ # Each tile is read through a view with one axis per tile position and
78
+ # one per cell in the tile, twice the source's rank.
79
+ if 2 * source.ndim > CA_RANK_MAX
80
+ raise ArgumentError,
81
+ "blocks: a rank #{source.ndim} array would need #{2 * source.ndim} " \
82
+ "axes for its tiles, beyond the limit of #{CA_RANK_MAX}; " \
83
+ "blocks works up to rank #{CA_RANK_MAX / 2}"
84
+ end
77
85
 
78
86
  @sndim = source.ndim
79
87
  offsets = Array.new(@sndim, 0)
@@ -93,7 +101,9 @@ class CABlockIterator < CAIterator
93
101
  # Absorb a leading offset with a zero-copy pre-slice, so the tile geometry
94
102
  # below always starts at index 0.
95
103
  @source =
96
- if offsets.all?(&:zero?)
104
+ if source.scalar?
105
+ source.flatten # a CScalar takes no index; its one-cell view does
106
+ elsif offsets.all?(&:zero?)
97
107
  source
98
108
  else
99
109
  source[*offsets.map { |o| o..-1 }]
@@ -517,7 +527,7 @@ class CABlockIterator < CAIterator
517
527
  sview = saddr[*strip_ranges].block_view(*tiles)
518
528
  grid = (0...@sndim).map { |i| vview.shape[i] }
519
529
  cells = tiles.inject(1) { |p, t| p * t }
520
- order = vview.copy.reshape(*(grid + [cells])).sort_addr(axis: @sndim)
530
+ order = vview.copy.reshape(*(grid + [cells])).sort_index(axis: @sndim)
521
531
  src_sorted = sview.reshape(*(grid + [cells])).take_along_axis(order, axis: @sndim)
522
532
  out[*strip_ranges].block_view(*tiles)[] = src_sorted.reshape(*(grid + tiles))
523
533
  end
@@ -817,7 +827,7 @@ class CABlockIterator < CAIterator
817
827
  # A padded entity whose interior holds the source and whose partial-edge
818
828
  # margin is masked (UNDEF), sized to exactly cover the ceil tile grid. When
819
829
  # the source already divides evenly this is the source itself (no copy), so
820
- # the tiles alias it. Memoised; feeds the iterate escape hatches.
830
+ # the tiles alias it. Kept for one call; feeds the iterate escape hatches.
821
831
  def padded_source
822
832
  @padded_source ||= begin
823
833
  pshape = @sndim.times.map { |i| @shape[i] * @sizes[i] }
@@ -845,6 +855,8 @@ class CABlockIterator < CAIterator
845
855
  def lag (*args, **keywords) = @source.__send__(:carry_attrs, super)
846
856
  def strip_mask (**keywords) = @source.__send__(:carry_attrs, super)
847
857
  end)
858
+
859
+ read_source_per_call :@padded_source, :@tile_grid_view
848
860
  end
849
861
 
850
862
 
@@ -857,6 +869,8 @@ class CArray
857
869
  # first for "valid" tiling.
858
870
  # @param blocks [Array<Integer, Range>] per-axis tile sizes (or ranges).
859
871
  # @return [CABlockIterator]
872
+ # @raise [ArgumentError] when `self` has a rank above `CA_RANK_MAX / 2`
873
+ # (8): the tiles are read through a view of twice the rank.
860
874
  def blocks (*blocks)
861
875
  CABlockIterator.new(self, *blocks)
862
876
  end
@@ -415,8 +415,8 @@ class CACategorical < CAObject
415
415
  # Memoised: the codes are read-only (immutable storage), so the counts are a
416
416
  # pure function of the categorical and stay valid for its lifetime. The same
417
417
  # cached array backs {#reduceat_index} and {#group_addr}, so a wide aggregate
418
- # over the same categorical pays the count once. (Do not mutate the returned
419
- # array — it is shared; take `.copy` for a scratch buffer.) A derived-view /
418
+ # over the same categorical pays the count once. The returned array is shared,
419
+ # so it is frozen; take `.copy` for a scratch buffer. A derived-view /
420
420
  # composite categorical caches too: read-only rides from the codes as a flag,
421
421
  # not a Ruby freeze, so the object stays non-frozen and the memo ivar sticks.
422
422
  # @overload category_sizes
@@ -429,8 +429,7 @@ class CACategorical < CAObject
429
429
  bc = codes.bincount
430
430
  out = CArray.new(bc.data_type, [@labels.size], bytes: bc.bytes) # new zero-fills
431
431
  out[0...bc.elements] = bc if bc.elements > 0
432
- @category_sizes = out
433
- out
432
+ @category_sizes = out.freeze
434
433
  end
435
434
 
436
435
  # Back-compatible alias; {#category_sizes} is the canonical name (a histogram
@@ -635,7 +634,7 @@ class CACategorical < CAObject
635
634
  # Memoised (see the grouping-plan note above): the counting sort is the
636
635
  # dominant grouping cost, so it is computed once and shared by every payload
637
636
  # column and by CACategoricalIterator, and kept for the categorical's
638
- # lifetime (an int64 per cell). Do not mutate the returned array.
637
+ # lifetime (an int64 per cell). The returned array is shared, so it is frozen.
639
638
  # @overload group_addr
640
639
  # Returns a length-{#elements} integer CArray of flat storage
641
640
  # addresses that order the cells by category (excluded cells last).
@@ -645,8 +644,7 @@ class CACategorical < CAObject
645
644
  out = codes.reshape(elements).__send__(:addresses_by_category,
646
645
  reduceat_index.copy, @labels.size,
647
646
  category_sizes.sum)
648
- @group_addr = out
649
- out
647
+ @group_addr = out.freeze
650
648
  end
651
649
 
652
650
  # Segment start offsets into the category-contiguous layout produced by
@@ -663,7 +661,7 @@ class CACategorical < CAObject
663
661
  return @reduceat_index if @reduceat_index
664
662
  # Every boundary but the last; the copy makes it an entity the
665
663
  # reduceat kernels can read directly.
666
- @reduceat_index = CArray.segment_offsets(lengths: category_sizes)[0...-1].copy
664
+ @reduceat_index = CArray.segment_offsets(lengths: category_sizes)[0...-1].copy.freeze
667
665
  end
668
666
 
669
667
  # @overload categorize(labels: nil, sort_labels: false)
@@ -295,7 +295,7 @@ class CACategoricalIterator < CASegmentIterator
295
295
  # @overload wmean(weights, axis:)
296
296
  # Per-fiber per-category weighted mean along `axis`. Same weights-shape
297
297
  # contract as {#wsum} (weights.shape == source.shape). Empty cell → MASKED;
298
- # a present cell whose weights sum to zero → NaN (0/0 core contract).
298
+ # a present cell whose weights sum to zero → UNDEF, as CArray#wmean.
299
299
  # @param weights [CArray]
300
300
  # @param axis [Integer]
301
301
  # @return [CArray]
@@ -551,7 +551,7 @@ class CACategoricalIterator < CASegmentIterator
551
551
  codes = @cat.codes
552
552
  # Classified along the axis, a group is taken by index; the general way,
553
553
  # masking the rest of a copy, gives the same cells.
554
- if codes.shape == [h.shape[axis]] && CArray.__fast_paths_enabled__
554
+ if codes.shape == [h.shape[axis]] && CArray.__fast_paths_enabled__(:categorical_group_by_index)
555
555
  sel = none ? CArray.boolean(codes.elements) { false } : codes.eq(c)
556
556
  sel = sel.strip_mask(false) if sel.has_mask?
557
557
  idx = [nil] * h.ndim
@@ -74,7 +74,7 @@ class CArray
74
74
  when CA_CMPLX128 then field(0, CA_FLOAT64)
75
75
  end
76
76
  else
77
- self[]
77
+ scalar? ? self : self[]
78
78
  end
79
79
  frozen? ? view : (@__real__ = view)
80
80
  end
@@ -100,7 +100,7 @@ class CArray
100
100
  end
101
101
  if not complex?
102
102
  zero = template { 0 }
103
- zero[is_masked] = UNDEF if has_mask?
103
+ zero.inherit_mask(self) if has_mask?
104
104
  return zero
105
105
  end
106
106
  return @__imag__ if part_of_self?(@__imag__)
@@ -219,8 +219,9 @@ class CArray
219
219
  "conditional: cond must be a boolean CArray with same shape as self"
220
220
  end
221
221
 
222
- x_then = self[cond]
223
- x_else = self[cond.not]
222
+ src = scalar? ? flatten : self # a CScalar takes no index
223
+ x_then = src[cond]
224
+ x_else = src[cond.not]
224
225
  y_then = then_fn.call(x_then)
225
226
  y_else = else_fn.call(x_else)
226
227
  text = !data_type && CArray.send(:selection_text?, [y_then, y_else])
@@ -3,22 +3,32 @@
3
3
  class CAFrame
4
4
 
5
5
  # The one rule of a frame-wide verb over the frame's columns -- the group
6
- # reductions and transforms, the windowed reductions -- kept in one place
7
- # so that every verb answers in the same shape:
6
+ # reductions and transforms, the windowed reductions, and the frame's own
7
+ # reductions and running values -- kept in one place: which columns a verb
8
+ # computes, and what a named column without the operation does.
8
9
  #
9
- # - given names, the CArray (one) or the Array of them (several), as
10
- # +df[...]+ returns columns; a named column whose type has no such
11
- # operation is refused with CArray::DataTypeError, as the core refuses
12
- # it, since it was asked for;
10
+ # - given names, those columns, as the CArray (one) or a Hash of name to
11
+ # result (several); a named column whose type has no such operation is
12
+ # refused with CArray::DataTypeError, as the core refuses it, since it
13
+ # was asked for;
13
14
  # - given none, every column whose type has it, the key columns left out,
14
- # as a Hash of name to result that the caller makes a frame of.
15
+ # as a Hash of name to result.
16
+ #
17
+ # The caller decides the shape of the answer from these: a verb whose
18
+ # answers lie on the frame's rows (a transform, a window at the rows,
19
+ # +keep_rows:+) hands one named column back as it is, as +df[name]+
20
+ # returns it, and several as a frame on the frame's rows, while a verb
21
+ # with one row per group, per point or for the whole frame makes a frame
22
+ # of them, labelled by its rows.
15
23
  #
16
24
  # Whether a type has the operation is asked of the core on two copies of
17
25
  # the column's first row (two blank cells of its kind when it has no
18
26
  # rows), so the answer is about the type and not the data: a refusal
19
27
  # there means the operation does not apply, while an error in the verb
20
- # itself is left to raise. An N-D column (each row a profile) is asked,
21
- # and computed, along the rows.
28
+ # itself is left to raise. An object column is the exception: its type
29
+ # says nothing of what it holds, so one with no present value is left out
30
+ # of a verb run over every column. An N-D column (each row a profile) is
31
+ # asked, and computed, along the rows.
22
32
  module ColumnVerbs
23
33
 
24
34
  private
@@ -29,13 +39,17 @@ class CAFrame
29
39
  @frame.column_names.each do |name|
30
40
  next if key_names.include?(name)
31
41
  col = @frame[name]
42
+ next if valueless_object_column?(col, verb, args)
32
43
  next unless column_has?(col, verb, args, keywords)
33
44
  cols[name] = yield(col, name)
34
45
  end
35
46
  return cols
36
47
  end
37
- results = names.map do |name|
38
- key = @frame.__send__(:column_key, name, verb)
48
+ # A column named twice would be one result column twice over.
49
+ keys = CAFrame.__send__(:refuse_repeated_names,
50
+ names.map { |name| @frame.__send__(:column_key, name, verb) }, verb)
51
+ results = keys.map do |key|
52
+ name = key
39
53
  col = @frame[key]
40
54
  unless column_has?(col, verb, args, keywords)
41
55
  raise CArray::DataTypeError,
@@ -44,7 +58,17 @@ class CAFrame
44
58
  end
45
59
  yield(col, key)
46
60
  end
47
- results.size == 1 ? results.first : results
61
+ results.size == 1 ? results.first : keys.zip(results).to_h
62
+ end
63
+
64
+ # An object column with no present cell says nothing of what it holds,
65
+ # so a verb run over every column leaves it out, as select leaves it out
66
+ # of every kind; counting the present cells applies to any column.
67
+ def valueless_object_column? (col, verb, args)
68
+ return false if col.face? || col.data_type != CA_OBJECT
69
+ return false if %i[count_not_masked count_masked].include?(verb)
70
+ return false if verb == :count && args.empty?
71
+ col.count_not_masked == 0
48
72
  end
49
73
 
50
74
  # count with no value counts the present cells, which every type has;
@@ -54,10 +78,17 @@ class CAFrame
54
78
  verb = :count_not_masked if verb == :count && args.empty?
55
79
  # Two cells: one cell does not reach what combines two (a String's
56
80
  # median interpolates, its sum adds), and an empty column answers
57
- # with the identity without asking the type at all.
81
+ # with the identity without asking the type at all. The row is the
82
+ # first that holds a present cell, so a masked first row does not
83
+ # answer with the identity for a column whose values have none.
58
84
  tail = [nil] * (col.ndim - 1)
85
+ row = 0
86
+ if col.has_mask? && col.elements > 0
87
+ present = col.is_not_masked.flatten.where
88
+ row = present[0] / (col.elements / col.shape[0]) if present.elements > 0
89
+ end
59
90
  probe = if col.shape[0] > 0
60
- col[CA_INT64([0, 0]), *tail]
91
+ col[CA_INT64([row, row]), *tail]
61
92
  else
62
93
  col.resize(2, *col.shape[1..])
63
94
  end
@@ -26,6 +26,25 @@ class CAFrame
26
26
  each_row.map { |row| row.transform_values { |v| record_value(v) } }
27
27
  end
28
28
 
29
+ # The rows as an Array of Arrays, one value per column in column order --
30
+ # the table as plain Ruby data, for a library that takes rows (anything
31
+ # that calls +to_a+ on what it is given). A cell is what the column's own
32
+ # +to_a+ gives: a masked cell is +UNDEF+, a time column's cell its element,
33
+ # and an N-D column's cell an Array. The index is a row label, not a value,
34
+ # and is left out; +reset_index+ first to have it as the first column.
35
+ #
36
+ # Masked cells are not turned into anything: decide what they should be
37
+ # before handing the rows over, with +strip_mask+ or +drop_masked+.
38
+ #
39
+ # df.drop_masked.to_a
40
+ # df.strip_mask("temp" => Float::NAN).to_a
41
+ #
42
+ # @return [Array<Array>]
43
+ def to_a
44
+ return Array.new(@nrow) { [] } if @columns.empty?
45
+ return @columns.values.map(&:to_a).transpose
46
+ end
47
+
29
48
  # Hand the frame over as a CArray with the minimum work (memo §11.9): a
30
49
  # 2-D **view** of shape (nrow, ncol), one per column in column
31
50
  # order (§12-C). Nothing is materialised — the result is a +CAStack+ over