carray 3.0.2 → 3.0.3

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 (218) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +304 -0
  3. data/README.md +3 -4
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_dispatch.c +225 -0
  6. data/ext/ca_axis_group.c +177 -99
  7. data/ext/ca_binop_dispatch.c +54 -32
  8. data/ext/ca_binop_dispatch.h +1 -0
  9. data/ext/ca_categorical_iterator.c +66 -71
  10. data/ext/ca_compare.c +8 -2
  11. data/ext/ca_for_buffer.h +21 -68
  12. data/ext/ca_group_iter.c +27 -11
  13. data/ext/ca_kernel_iterator.c +663 -214
  14. data/ext/ca_kernel_iterator.h +231 -167
  15. data/ext/ca_monop_dispatch.c +4 -4
  16. data/ext/ca_monop_dispatch.h +4 -0
  17. data/ext/ca_obj_array.c +132 -22
  18. data/ext/ca_obj_bincmp.c +152 -121
  19. data/ext/ca_obj_binop.c +107 -210
  20. data/ext/ca_obj_bitarray.c +37 -24
  21. data/ext/ca_obj_bitfield.c +95 -62
  22. data/ext/ca_obj_block.c +1 -0
  23. data/ext/ca_obj_byte_swap.c +40 -40
  24. data/ext/ca_obj_const_string.c +96 -25
  25. data/ext/ca_obj_face.c +406 -64
  26. data/ext/ca_obj_face.h +92 -47
  27. data/ext/ca_obj_fake.c +21 -50
  28. data/ext/ca_obj_farray.c +1 -0
  29. data/ext/ca_obj_field.c +16 -3
  30. data/ext/ca_obj_fixlen_string.c +21 -21
  31. data/ext/ca_obj_grid.c +181 -124
  32. data/ext/ca_obj_meld.c +189 -107
  33. data/ext/ca_obj_moncmp.c +61 -61
  34. data/ext/ca_obj_monop.c +125 -107
  35. data/ext/ca_obj_object.c +543 -233
  36. data/ext/ca_obj_record.c +51 -16
  37. data/ext/ca_obj_reduce.c +5 -8
  38. data/ext/ca_obj_refer.c +117 -33
  39. data/ext/ca_obj_remap.c +57 -17
  40. data/ext/ca_obj_repeat.c +21 -5
  41. data/ext/ca_obj_roll.c +115 -11
  42. data/ext/ca_obj_select.c +47 -49
  43. data/ext/ca_obj_select_axis.c +58 -60
  44. data/ext/ca_obj_shift.c +22 -102
  45. data/ext/ca_obj_stack.c +223 -118
  46. data/ext/ca_obj_stride.c +195 -34
  47. data/ext/ca_obj_string.c +17 -18
  48. data/ext/ca_obj_tile.c +32 -16
  49. data/ext/ca_obj_time.c +23 -25
  50. data/ext/ca_obj_timedelta.c +21 -23
  51. data/ext/ca_obj_transpose.c +12 -3
  52. data/ext/ca_obj_triop.c +110 -117
  53. data/ext/ca_obj_window.c +149 -27
  54. data/ext/ca_op_ipower.c +24 -11
  55. data/ext/ca_op_powi.h +57 -30
  56. data/ext/ca_pow5_table.h +656 -0
  57. data/ext/ca_sort_kernels.h +70 -0
  58. data/ext/ca_sweep_engine.c +102 -79
  59. data/ext/ca_sweep_engine.h +7 -20
  60. data/ext/ca_triop_dispatch.c +4 -1
  61. data/ext/ca_triop_dispatch.h +5 -2
  62. data/ext/caframe_csv_reader.c +463 -0
  63. data/ext/carray.h +70 -27
  64. data/ext/carray_access.c +455 -211
  65. data/ext/carray_address_basis.c +177 -36
  66. data/ext/carray_attribute.c +45 -3
  67. data/ext/carray_bincount.c +32 -2
  68. data/ext/carray_broadcast.c +46 -14
  69. data/ext/carray_call_cfunc.c +0 -28
  70. data/ext/carray_cast.c +1444 -209
  71. data/ext/carray_cast_func.rb +34 -21
  72. data/ext/carray_conversion.c +197 -73
  73. data/ext/carray_copy.c +32 -11
  74. data/ext/carray_core.c +514 -122
  75. data/ext/carray_count.c +140 -87
  76. data/ext/carray_element.c +18 -27
  77. data/ext/carray_factorize.c +569 -277
  78. data/ext/carray_generate.c +76 -40
  79. data/ext/carray_histogram.c +152 -94
  80. data/ext/carray_hold.c +2 -1
  81. data/ext/carray_index_classifier.c +63 -96
  82. data/ext/carray_internal.h +385 -3
  83. data/ext/carray_lazy.c +834 -79
  84. data/ext/carray_loop.c +81 -65
  85. data/ext/carray_mask.c +682 -316
  86. data/ext/carray_math_kernel.h +1 -0
  87. data/ext/carray_median_percentile.c +221 -76
  88. data/ext/carray_memory_view.c +254 -248
  89. data/ext/carray_operator.c +1089 -359
  90. data/ext/carray_order.c +133 -69
  91. data/ext/carray_partition.c +102 -56
  92. data/ext/carray_random.c +273 -239
  93. data/ext/carray_scatter.c +308 -65
  94. data/ext/carray_segment.c +257 -0
  95. data/ext/carray_slab.c +170 -70
  96. data/ext/carray_sort.c +215 -218
  97. data/ext/carray_sort_kernel.c +8 -1
  98. data/ext/carray_struct.c +99 -21
  99. data/ext/carray_test.c +167 -64
  100. data/ext/carray_time_text.c +1182 -0
  101. data/ext/carray_utils.c +363 -98
  102. data/ext/extconf.rb +27 -2
  103. data/ext/mk_call_cfunc.rb +0 -2
  104. data/ext/mk_pow5_table.rb +35 -0
  105. data/ext/mkkernel.rb +1076 -846
  106. data/ext/ruby_carray.c +7 -1
  107. data/ext/version.h +4 -4
  108. data/ext/version.rb +4 -2
  109. data/lib/carray/attribute.rb +39 -0
  110. data/lib/carray/autoload_carray.rb +19 -0
  111. data/lib/carray/autoload_method_extension.rb +6 -0
  112. data/lib/carray/axis_group.rb +174 -26
  113. data/lib/carray/basics.rb +91 -25
  114. data/lib/carray/bincount_nd.rb +6 -0
  115. data/lib/carray/block_iterator.rb +118 -48
  116. data/lib/carray/boolean_reduce.rb +21 -29
  117. data/lib/carray/categorical.rb +94 -26
  118. data/lib/carray/categorical_iterator.rb +333 -826
  119. data/lib/carray/complex.rb +36 -29
  120. data/lib/carray/conditional.rb +94 -7
  121. data/lib/carray/const_string.rb +46 -0
  122. data/lib/carray/frame/concat.rb +157 -6
  123. data/lib/carray/frame/convert.rb +13 -7
  124. data/lib/carray/frame/csv_parser.rb +589 -82
  125. data/lib/carray/frame/describe.rb +79 -0
  126. data/lib/carray/frame/frame.rb +124 -25
  127. data/lib/carray/frame/group.rb +189 -63
  128. data/lib/carray/frame/io.rb +412 -72
  129. data/lib/carray/frame/join.rb +134 -46
  130. data/lib/carray/frame/records.rb +37 -16
  131. data/lib/carray/frame/reshape.rb +448 -0
  132. data/lib/carray/frame/sort.rb +26 -2
  133. data/lib/carray/frame/verbs.rb +715 -48
  134. data/lib/carray/frame.rb +2 -0
  135. data/lib/carray/fuse_source.rb +111 -2
  136. data/lib/carray/fusion.rb +229 -38
  137. data/lib/carray/inspect.rb +29 -11
  138. data/lib/carray/iterator.rb +14 -2
  139. data/lib/carray/lazy.rb +54 -30
  140. data/lib/carray/mask_gap_fill.rb +66 -52
  141. data/lib/carray/math.rb +32 -19
  142. data/lib/carray/meld_reduce.rb +40 -7
  143. data/lib/carray/methods/align_addr.rb +8 -4
  144. data/lib/carray/methods/bincount.rb +2 -2
  145. data/lib/carray/methods/bit_string.rb +14 -3
  146. data/lib/carray/methods/composition.rb +16 -10
  147. data/lib/carray/methods/factorize.rb +2 -2
  148. data/lib/carray/methods/gather_nd.rb +13 -2
  149. data/lib/carray/methods/insert_block.rb +9 -14
  150. data/lib/carray/methods/is_in.rb +117 -4
  151. data/lib/carray/methods/join.rb +1 -1
  152. data/lib/carray/methods/locate_addr.rb +177 -36
  153. data/lib/carray/methods/mode.rb +18 -1
  154. data/lib/carray/methods/repeat.rb +1 -20
  155. data/lib/carray/methods/resize.rb +21 -23
  156. data/lib/carray/methods/snap.rb +34 -12
  157. data/lib/carray/methods/unique.rb +11 -2
  158. data/lib/carray/record_order.rb +208 -0
  159. data/lib/carray/runtime.rb +14 -11
  160. data/lib/carray/segment_iterator.rb +990 -0
  161. data/lib/carray/serialize.rb +197 -27
  162. data/lib/carray/slab_iterator.rb +51 -27
  163. data/lib/carray/stack.rb +10 -4
  164. data/lib/carray/string_operation_extension.rb +4 -3
  165. data/lib/carray/struct.rb +142 -51
  166. data/lib/carray/struct_builder.rb +110 -14
  167. data/lib/carray/table.rb +2 -2
  168. data/lib/carray/time.rb +517 -103
  169. data/lib/carray/window_iterator.rb +127 -72
  170. data/lib/carray.rb +2 -2
  171. data/yard-stubs/ca_obj_array.rb +4 -4
  172. data/yard-stubs/ca_obj_bitarray.rb +7 -0
  173. data/yard-stubs/ca_obj_bitfield.rb +23 -12
  174. data/yard-stubs/ca_obj_fake.rb +1 -1
  175. data/yard-stubs/ca_obj_field.rb +1 -1
  176. data/yard-stubs/ca_obj_refer.rb +3 -3
  177. data/yard-stubs/ca_obj_roll.rb +0 -1
  178. data/yard-stubs/ca_obj_stride.rb +10 -7
  179. data/yard-stubs/ca_obj_transpose.rb +1 -1
  180. data/yard-stubs/ca_obj_window.rb +1 -2
  181. data/yard-stubs/carray_access.rb +5 -4
  182. data/yard-stubs/carray_broadcast.rb +1 -1
  183. data/yard-stubs/carray_cast.rb +78 -43
  184. data/yard-stubs/carray_conversion.rb +13 -1
  185. data/yard-stubs/carray_copy.rb +8 -3
  186. data/yard-stubs/carray_core.rb +3 -3
  187. data/yard-stubs/carray_count.rb +9 -0
  188. data/yard-stubs/carray_lazy.rb +5 -1
  189. data/yard-stubs/carray_loop.rb +4 -1
  190. data/yard-stubs/carray_mask.rb +34 -10
  191. data/yard-stubs/carray_math.rb +25 -13
  192. data/yard-stubs/carray_median_percentile.rb +10 -6
  193. data/yard-stubs/carray_order.rb +34 -13
  194. data/yard-stubs/carray_scatter.rb +10 -3
  195. data/yard-stubs/carray_segment.rb +72 -0
  196. data/yard-stubs/carray_slab.rb +2 -1
  197. data/yard-stubs/carray_sort.rb +43 -3
  198. data/yard-stubs/carray_test.rb +10 -0
  199. data/yard-stubs/carray_utils.rb +0 -26
  200. metadata +11 -19
  201. data/ext/ca_for_each_element.h +0 -239
  202. data/ext/carray_build_flags.h +0 -3
  203. data/ext/carray_config.h +0 -28
  204. data/ext/carray_kernels_bincmp.c +0 -4446
  205. data/ext/carray_kernels_binop.c +0 -11001
  206. data/ext/carray_kernels_init.c +0 -1131
  207. data/ext/carray_kernels_map.c +0 -3467
  208. data/ext/carray_kernels_moncmp.c +0 -2097
  209. data/ext/carray_kernels_monop.c +0 -18313
  210. data/ext/carray_kernels_reduce_aggregate.c +0 -26005
  211. data/ext/carray_kernels_reduce_boolean.c +0 -330
  212. data/ext/carray_kernels_reduce_cumulative.c +0 -14862
  213. data/ext/carray_kernels_reduce_extreme.c +0 -17494
  214. data/ext/carray_kernels_reduce_variance.c +0 -3910
  215. data/ext/carray_kernels_scan.c +0 -3693
  216. data/ext/carray_kernels_search.c +0 -32218
  217. data/ext/carray_kernels_sort.c +0 -10626
  218. data/ext/carray_kernels_triop.c +0 -1392
@@ -52,6 +52,14 @@ class CArray
52
52
  # framework's read-only chain does not replace the CAField mutable view).
53
53
  # ---------------------------------------------------------------------------
54
54
 
55
+ # Whether a view kept by {#real} / {#imag} was made for this array.
56
+ # `dup` and `clone` copy instance variables, so a copy arrives holding the
57
+ # original's view, whose parent is the original; the copy makes its own.
58
+ def part_of_self? (view)
59
+ view && view.parent.equal?(self)
60
+ end
61
+ private :part_of_self?
62
+
55
63
  # @overload real
56
64
  # Returns the real part of `self` as a zero-copy view. For a
57
65
  # complex array the view is a mutable {CAField} into the
@@ -59,19 +67,16 @@ class CArray
59
67
  # over `self`. Writing to the view updates `self` in place.
60
68
  # @return [CArray]
61
69
  def real
62
- if not @__real__
63
- if complex?
64
- @__real__ = case data_type
65
- when CA_CMPLX64
66
- field(0, CA_FLOAT32)
67
- when CA_CMPLX128
68
- field(0, CA_FLOAT64)
69
- end
70
- else
71
- @__real__ = self[]
72
- end
73
- end
74
- @__real__
70
+ return @__real__ if part_of_self?(@__real__)
71
+ view = if complex?
72
+ case data_type
73
+ when CA_CMPLX64 then field(0, CA_FLOAT32)
74
+ when CA_CMPLX128 then field(0, CA_FLOAT64)
75
+ end
76
+ else
77
+ self[]
78
+ end
79
+ frozen? ? view : (@__real__ = view)
75
80
  end
76
81
 
77
82
  # @overload real=(val)
@@ -85,35 +90,37 @@ class CArray
85
90
  # @overload imag
86
91
  # Returns the imaginary part of `self`. For a complex array the
87
92
  # result is a mutable {CAField} view of the imaginary slot;
88
- # writing to it updates `self` in place. For a real array the
89
- # result is a fresh independent CArray filled with 0.
93
+ # writing to it updates `self` in place. For an object array each
94
+ # cell's `imaginary`; for a real array a fresh independent CArray
95
+ # filled with 0. Masked cells stay masked.
90
96
  # @return [CArray]
91
97
  def imag
92
- if not @__imag__
93
- if complex?
94
- @__imag__ = case data_type
95
- when CA_CMPLX64
96
- field(4, CA_FLOAT32)
97
- when CA_CMPLX128
98
- field(8, CA_FLOAT64)
99
- end
100
- else
101
- @__imag__ = self.template { 0 }
102
- end
98
+ if object?
99
+ return imag_i
100
+ end
101
+ if not complex?
102
+ zero = template { 0 }
103
+ zero[is_masked] = UNDEF if has_mask?
104
+ return zero
103
105
  end
104
- return @__imag__
106
+ return @__imag__ if part_of_self?(@__imag__)
107
+ view = case data_type
108
+ when CA_CMPLX64 then field(4, CA_FLOAT32)
109
+ when CA_CMPLX128 then field(8, CA_FLOAT64)
110
+ end
111
+ frozen? ? view : (@__imag__ = view)
105
112
  end
106
113
 
107
114
  # @overload imag=(val)
108
115
  # Sets the imaginary-part slot to `val` (complex arrays only).
109
116
  # @param val [CArray, Numeric] value to broadcast.
110
117
  # @return [Object] `val`.
111
- # @raise [RuntimeError] when `self` is not a complex array.
118
+ # @raise [CArray::DataTypeError] when `self` is not a complex array.
112
119
  def imag= (val)
113
120
  if complex?
114
121
  imag[] = val
115
122
  else
116
- raise "not a complex array"
123
+ raise CArray::DataTypeError, "not a complex array"
117
124
  end
118
125
  end
119
126
 
@@ -6,7 +6,7 @@
6
6
  # same "pick a value per cell based on a boolean condition" contract but
7
7
  # differ in where the condition lives and how the branches are supplied:
8
8
  #
9
- # cond.then_else(x, y) # boolean receiver, 2 values (eager)
9
+ # cond.then_else(x, y) # boolean receiver, 2 values (lazy if an operand is)
10
10
  # self.replace_where(cond, b) # value receiver, 1 replacement (eager)
11
11
  # self.conditional(cond, f_then, f_else) # value receiver, 2 callables (per-region)
12
12
  #
@@ -25,10 +25,20 @@ class CArray
25
25
  # reading as "if `self` then `x` else `y`". The boolean 2-way
26
26
  # case of {#choose} that additionally propagates `self`'s mask
27
27
  # (`UNDEF` in `self` produces `UNDEF` in the result).
28
+ # The result's data type is the common type of the two branches; a
29
+ # branch given as `UNDEF` masks its cells and leaves the type to the
30
+ # other branch (`cond.then_else(a, UNDEF)` has `a`'s data type).
31
+ # When `self` or a branch is a lazy view, the result is a lazy view
32
+ # (read-only) that joins the expression around it; `copy` makes an
33
+ # array of it. An object result is always computed at once.
28
34
  # @param x [CArray, Numeric, Object] true-branch value(s).
29
35
  # @param y [CArray, Numeric, Object] false-branch value(s).
30
- # @return [CArray] new array with the same shape as `self`.
31
- # @raise [ArgumentError] when `self` is not a boolean CArray.
36
+ # @return [CArray] new array with the same shape as `self`, or a
37
+ # lazy view (CATriOp) when an operand is lazy.
38
+ # @raise [ArgumentError] when `self` is not a boolean CArray, or when
39
+ # an array branch's shape does not pair with `self`'s as an
40
+ # operator's operands would (equal, or size-1 axes of the same ndim;
41
+ # an object or fixlen result needs `self`'s shape exactly).
32
42
  def then_else (x, y)
33
43
  # Guard: self must be boolean. Integer / float receiver would be
34
44
  # silently reinterpreted by the indexer setter (`result[self] = ...`)
@@ -38,10 +48,27 @@ class CArray
38
48
  raise ArgumentError,
39
49
  "then_else: receiver must be a boolean CArray (data_type == CA_BOOLEAN), got #{self.data_type}"
40
50
  end
41
- # Promote data_type from both branches via CArray.result_type
42
- # (a CScalar contributes its own data_type, so CA_INT32(0) keeps int32
43
- # where a bare Ruby Integer would widen to int64).
44
- dt = CArray.result_type(x, y)
51
+ reshaped = [x, y].any? { |v| v.is_a?(CArray) && !v.scalar? && v.shape != shape }
52
+ face = [x, y].find { |v| v.is_a?(CArray) && !v.scalar? && v.face? }
53
+ if face
54
+ refuse_branch_shape(x, y) if reshaped
55
+ return face_then_else(x, y, face)
56
+ end
57
+ # Promote data_type from both branches via CArray.result_type, which
58
+ # lets a scalar branch take the type of an array branch as an operator
59
+ # does (cond.then_else(0, int32_array) is int32). UNDEF is a missing
60
+ # value, not a value of some type: a branch that is UNDEF leaves the
61
+ # type to the other.
62
+ typed = [x, y].reject { |v| v.equal?(UNDEF) }
63
+ dt = typed.empty? ? CA_OBJECT : CArray.result_type(*typed)
64
+ return lazy_then_else(x, y, dt) if lazy_then_else?(x, y, dt)
65
+ # A branch shaped unlike the condition goes through the lazy select,
66
+ # which pairs the shapes by the operator rule (equal, or size-1 axes
67
+ # with the same ndim) and refuses the rest.
68
+ if reshaped
69
+ refuse_branch_shape(x, y) if scalar? || dt == CA_OBJECT || dt == CA_FIXLEN
70
+ return lazy_then_else(x, y, dt).copy
71
+ end
45
72
  # A CScalar (scalar? CArray) is treated as a scalar value, not as a
46
73
  # self-shaped operand: full CArray -> gather/copy, scalar -> broadcast.
47
74
  y_full = y.is_a?(CArray) && !y.scalar?
@@ -60,6 +87,66 @@ class CArray
60
87
  result
61
88
  end
62
89
 
90
+ # A lazy receiver or branch makes the selection a lazy node (CATriOp
91
+ # select), so it joins the expression around it rather than computing
92
+ # it here. The branches carry the result type: a scalar branch becomes
93
+ # a CScalar of it, UNDEF a masked one.
94
+ private def lazy_then_else? (x, y, dt)
95
+ return false if scalar? || dt == CA_OBJECT || dt == CA_FIXLEN
96
+ [self, x, y].any? { |v| v.is_a?(CArray) && v.__lazy_view__? }
97
+ end
98
+
99
+ # The types the lazy select cannot carry keep the strict form: a full
100
+ # branch must have the condition's shape.
101
+ private def refuse_branch_shape (x, y)
102
+ bad = [x, y].find { |v| v.is_a?(CArray) && !v.scalar? && v.shape != shape }
103
+ raise ArgumentError,
104
+ "then_else: branch shape #{bad.shape.inspect} does not match the condition's #{shape.inspect}"
105
+ end
106
+
107
+ private def lazy_then_else (x, y, dt)
108
+ branch = lambda do |v|
109
+ next v if v.is_a?(CArray) && !v.scalar?
110
+ cell = CScalar.new(dt)
111
+ cell[0] = v.is_a?(CArray) ? v[0] : v
112
+ cell
113
+ end
114
+ CATriOp.__build__(self, branch.(x), branch.(y), CATriOp::OP_SELECT)
115
+ end
116
+
117
+ private def face_then_else (x, y, face)
118
+ # A Face branch decides the result: the other branch is written into a
119
+ # copy of it through the Face's own store, so an instant stays an instant.
120
+ # Promoting the two branches to a common data type would compare a Face's
121
+ # surface type with its storage's. A CAConstString cannot be written, so
122
+ # it selects among its strings and packs them again.
123
+ if face.is_a?(CAConstString)
124
+ sx = x.is_a?(CAConstString) ? x.to_string : x
125
+ sy = y.is_a?(CAConstString) ? y.to_string : y
126
+ return CArray.const_string(then_else(sx, sy), encoding: face.encoding)
127
+ end
128
+ x_full = x.is_a?(CArray) && !x.scalar?
129
+ y_full = y.is_a?(CArray) && !y.scalar?
130
+ base = y_full ? y : x
131
+ if base.read_only?
132
+ # No store to write through (a CACategorical's codes): select among
133
+ # the values themselves.
134
+ result = CArray.object(*shape)
135
+ result[] = y_full ? y.to_type(CA_OBJECT) : y
136
+ result[self] = x_full ? x.to_type(CA_OBJECT)[self] : x
137
+ result[self.is_masked] = UNDEF if self.has_mask?
138
+ return result
139
+ end
140
+ result = base.copy
141
+ if y_full
142
+ result[self] = x_full ? x[self] : x
143
+ else
144
+ result[self.not] = y.is_a?(CArray) ? y[0] : y
145
+ end
146
+ result[self.is_masked] = UNDEF if self.has_mask?
147
+ result
148
+ end
149
+
63
150
  # @overload replace_where(cond, b)
64
151
  # Returns a copy of `self` with cells where `cond` is true
65
152
  # replaced by `b`. Functional sibling of the destructive
@@ -182,6 +182,13 @@ class CAConstString
182
182
  to_string.minmax(*args, **kw).map { |r| const_string_lift(r) }
183
183
  end
184
184
 
185
+ # @overload clip(min, max = nil, fill_value = nil, lfill: nil, ufill: nil)
186
+ # Clips the strings in byte order (see {CArray#clip}).
187
+ # @return [CAConstString]
188
+ def clip (*args, **kw)
189
+ const_string_lift(to_string.clip(*args, **kw))
190
+ end
191
+
185
192
  # @overload min_index(axis: nil)
186
193
  # @return [Integer, CArray] where the byte-smallest string sits.
187
194
  def min_index (*args, **kw)
@@ -315,6 +322,32 @@ class CAConstString
315
322
  to_string.categorize(labels: labels, sort_labels: sort_labels)
316
323
  end
317
324
 
325
+ # ---- comparison operators ---------------------------------------------
326
+ #
327
+ # The core operators compare storage, and a storage cell is a byte range:
328
+ # `x < "dd"` compared where each string sits in the buffer and answered
329
+ # true everywhere. They read the strings here instead. `eq` has a native
330
+ # byte scan and stays; `<=>` composes from `>` and `<`, so it follows.
331
+
332
+ # @!method ne(other)
333
+ # @return [CArray] boolean, true where the strings differ.
334
+ # @!method lt(other)
335
+ # @return [CArray] boolean, true where self sorts before `other`.
336
+ # @!method le(other)
337
+ # @return [CArray] boolean, true where self sorts before or equals `other`.
338
+ # @!method gt(other)
339
+ # @return [CArray] boolean, true where self sorts after `other`.
340
+ # @!method ge(other)
341
+ # @return [CArray] boolean, true where self sorts after or equals `other`.
342
+ [:ne, :lt, :le, :gt, :ge].each do |op|
343
+ define_method(op) { |other| to_string.public_send(op, string_operand(other)) }
344
+ end
345
+
346
+ alias < lt
347
+ alias <= le
348
+ alias > gt
349
+ alias >= ge
350
+
318
351
  # A CAConstString operand has to be decoded too; anything else (a CAString,
319
352
  # an object array, an Array) already compares as strings.
320
353
  private def string_operand (other)
@@ -330,3 +363,16 @@ class CAConstString
330
363
  end
331
364
 
332
365
  end
366
+
367
+ class CAConstString
368
+ # The decimal readers of CAFrame#infer_types / #cast, read from the buffer
369
+ # without a String per cell.
370
+ def __infer_number_type_of_text__
371
+ CArray.__const_string_infer_number_type_of_text__(self)
372
+ end
373
+
374
+ def __read_text_as_number__(type, report = nil)
375
+ out = CArray.__const_string_read_text_as_number__(self, type, report)
376
+ out && ndim > 1 ? out.reshape(*shape) : out
377
+ end
378
+ end
@@ -9,6 +9,9 @@
9
9
  #
10
10
  # Both are class methods (symmetric N-ary; no frame is privileged), like
11
11
  # +CArray.meld+ / +CArray.concatenate+, not instance verbs.
12
+ #
13
+ # +CAFrame.stack+ and +CAFrame#unstack+ add and take away a layer axis
14
+ # instead of rows.
12
15
 
13
16
  class CAFrame
14
17
  # Weld frames along the row axis, view-style. Each output column is
@@ -28,6 +31,13 @@ class CAFrame
28
31
  #
29
32
  # The index is welded too when every frame has one (their +axis_name+
30
33
  # must agree); if none do, the result has no index; a mix raises.
34
+ #
35
+ # A frame with no rows adds no values, so it takes no part in the result
36
+ # past these checks: its column set, its N-D columns' trailing dimensions
37
+ # and its index still have to agree, but the
38
+ # data types are the other frames' (a header-only CSV reads as object
39
+ # columns, which would otherwise refuse or demote the rest). When every
40
+ # frame is empty, the first one stands for them.
31
41
  # Column-set mismatch raises — a union-with-UNDEF mode is a possible
32
42
  # future opt-in, kept out here to stay explicit (memo §4.2).
33
43
  #
@@ -37,8 +47,11 @@ class CAFrame
37
47
  frames = frames.flatten
38
48
  check_concat_inputs(frames, verb: "meld")
39
49
  first = frames.first
40
- names = first.variable_names
50
+ names = first.column_names
41
51
  check_column_sets(frames, names, verb: "meld")
52
+ index_pieces(frames, verb: "meld")
53
+ check_trailing_shapes(frames, names, verb: "meld")
54
+ frames = frames_with_rows(frames)
42
55
  cols = {}
43
56
  names.each do |name|
44
57
  cols[name] = CArray.meld(frames.map { |f| f[name] }, axis: 0)
@@ -54,8 +67,9 @@ class CAFrame
54
67
  # For a view frame that shares storage with the inputs (strict same data type
55
68
  # per column, chain composability preserved) use {CAFrame.meld}.
56
69
  #
57
- # Column matching, index handling, and column-set / index-mix rules
58
- # match {CAFrame.meld}.
70
+ # Column matching, index handling, column-set / index-mix rules and the
71
+ # treatment of a frame with no rows match {CAFrame.meld}: an empty frame's
72
+ # data types take no part in the common type.
59
73
  #
60
74
  # CAFrame.concatenate(jan, feb, mar) # eager, independent result
61
75
  # CAFrame.concatenate([jan, feb, mar]) # an Array is accepted too
@@ -63,8 +77,11 @@ class CAFrame
63
77
  frames = frames.flatten
64
78
  check_concat_inputs(frames, verb: "concatenate")
65
79
  first = frames.first
66
- names = first.variable_names
80
+ names = first.column_names
67
81
  check_column_sets(frames, names, verb: "concatenate")
82
+ index_pieces(frames, verb: "concatenate")
83
+ check_trailing_shapes(frames, names, verb: "concatenate")
84
+ frames = frames_with_rows(frames)
68
85
  cols = {}
69
86
  names.each do |name|
70
87
  cols[name] = CArray.concatenate(frames.map { |f| f[name] })
@@ -72,6 +89,108 @@ class CAFrame
72
89
  new(cols, axis_name: first.axis_name, index: concatenate_index(frames))
73
90
  end
74
91
 
92
+ # Stack frames of the same shape as layers. Each output column is
93
+ # +CArray.stack+ of that column across the frames along +axis+, counted in
94
+ # the column's own axes (0 is the row axis, so +axis+ starts at 1): a
95
+ # scalar column of K frames becomes a column of shape (nrow, K). The rows
96
+ # stay as they are, so row verbs work on the stack unchanged and a layer is
97
+ # a column axis -- +s["temp"][nil, k]+ is frame k's column, and
98
+ # +s["temp"].mean(axis: 1)+ reduces across the layers.
99
+ #
100
+ # The result is a view frame: its columns are CAStack views over the
101
+ # frames' columns, and writes reach them. Call +copy+ for an independent
102
+ # frame. The layers carry no labels; keep them alongside (the dates of the
103
+ # files, say) as an array of your own.
104
+ #
105
+ # Every frame must have the same columns, the same number of rows and the
106
+ # same index (by value) under the same axis name, or none; the result
107
+ # takes the first frame's. +unstack+ is the inverse:
108
+ #
109
+ # s = CAFrame.stack(jan1, jan2, jan3) # temp:float64 -> temp:float64[3]
110
+ # CAFrame.stack(*s.unstack(axis: 1)) # the same frame again
111
+ def self.stack(*frames, axis: 1)
112
+ frames = frames.flatten
113
+ check_concat_inputs(frames, verb: "stack")
114
+ check_layer_axis(axis, "stack")
115
+ first = frames.first
116
+ names = first.column_names
117
+ check_column_sets(frames, names, verb: "stack")
118
+ frames.each_with_index do |f, i|
119
+ next if i.zero?
120
+ unless f.nrow == first.nrow
121
+ raise ArgumentError,
122
+ "stack: frame #{i} has #{f.nrow} rows, frame 0 has #{first.nrow}"
123
+ end
124
+ end
125
+ indexes, = index_pieces(frames, verb: "stack")
126
+ if indexes && ! indexes.all? { |x| same_index_values?(x, indexes.first) }
127
+ raise ArgumentError, "stack: the frames' indexes differ; stacked rows have to be the same rows"
128
+ end
129
+ cols = {}
130
+ names.each do |name|
131
+ parts = frames.map { |f| f[name] }
132
+ if axis > parts.first.ndim
133
+ raise ArgumentError,
134
+ "stack: axis #{axis} out of range for column #{name.inspect} " \
135
+ "(a #{parts.first.ndim}-D column takes 1 to #{parts.first.ndim})"
136
+ end
137
+ cols[name] = CArray.stack(parts, axis: axis)
138
+ end
139
+ new(cols, axis_name: first.axis_name, index: first.index)
140
+ end
141
+
142
+ # Split the frame along a column axis into an Array of frames, one per
143
+ # position on that axis -- the inverse of {CAFrame.stack}. +axis+ counts
144
+ # the columns' own axes (0 is the row axis, so it starts at 1). Every
145
+ # column must have that axis, and the same length along it: a column
146
+ # without it has no layer to give each frame. Each frame's columns are
147
+ # views of this frame's, and each has this frame's index.
148
+ #
149
+ # s.unstack(axis: 1) # => [frame of layer 0, frame of layer 1, ...]
150
+ def unstack(axis:)
151
+ self.class.send(:check_layer_axis, axis, "unstack")
152
+ if @columns.empty?
153
+ raise ArgumentError, "unstack: the frame has no columns to unstack"
154
+ end
155
+ length = nil
156
+ @columns.each do |name, col|
157
+ unless axis < col.ndim
158
+ raise ArgumentError,
159
+ "unstack: column #{name.inspect} is #{col.ndim}-D and has no axis #{axis}"
160
+ end
161
+ length ||= col.shape[axis]
162
+ unless col.shape[axis] == length
163
+ raise ArgumentError,
164
+ "unstack: column #{name.inspect} has #{col.shape[axis]} along axis #{axis}, " \
165
+ "the others #{length}"
166
+ end
167
+ end
168
+ (0...length).map do |k|
169
+ cols = @columns.to_h do |name, col|
170
+ idx = [nil] * col.ndim
171
+ idx[axis] = k
172
+ [name, col[*idx]]
173
+ end
174
+ CAFrame.new(cols, axis_name: @axis_name, index: @index)
175
+ end
176
+ end
177
+
178
+ def self.check_layer_axis(axis, verb)
179
+ unless axis.is_a?(Integer)
180
+ raise TypeError, "#{verb}: axis must be an Integer (got #{axis.inspect})"
181
+ end
182
+ if axis == 0
183
+ raise ArgumentError,
184
+ "#{verb}: axis 0 is the row axis; rows are joined by meld or concatenate"
185
+ end
186
+ if axis < 0
187
+ raise ArgumentError,
188
+ "#{verb}: axis counts a column's axes from 1 (got #{axis}); " \
189
+ "a negative axis would mean a different axis in columns of different ndim"
190
+ end
191
+ end
192
+ private_class_method :check_layer_axis
193
+
75
194
  # ---- shared validators -------------------------------------------------
76
195
 
77
196
  def self.check_concat_inputs(frames, verb:)
@@ -86,15 +205,47 @@ class CAFrame
86
205
  expected = names.sort
87
206
  frames.each_with_index do |f, i|
88
207
  next if i.zero?
89
- if f.variable_names.sort != expected
208
+ if f.column_names.sort != expected
90
209
  raise ArgumentError,
91
- "#{verb}: frame #{i} has columns #{f.variable_names.inspect}, " \
210
+ "#{verb}: frame #{i} has columns #{f.column_names.inspect}, " \
92
211
  "expected the same set as #{names.inspect}"
93
212
  end
94
213
  end
95
214
  end
96
215
  private_class_method :check_column_sets
97
216
 
217
+ # A column's trailing dimensions are its shape, not a type the other
218
+ # frames can settle, so a frame with no rows has to agree on them too.
219
+ def self.check_trailing_shapes(frames, names, verb:)
220
+ names.each do |name|
221
+ trailing = frames.first[name].shape[1..]
222
+ frames.each_with_index do |f, i|
223
+ next if i.zero? || f[name].shape[1..] == trailing
224
+ raise ArgumentError,
225
+ "#{verb}: column #{name.inspect} has trailing dimensions " \
226
+ "#{f[name].shape[1..].inspect} in frame #{i}, #{trailing.inspect} in frame 0"
227
+ end
228
+ end
229
+ end
230
+ private_class_method :check_trailing_shapes
231
+
232
+ # Two indexes hold the same rows when their values agree cell by cell; a
233
+ # NaN label matches a NaN label, and a masked cell a masked cell.
234
+ def self.same_index_values?(a, b)
235
+ return false unless a.shape == b.shape
236
+ a.to_a.zip(b.to_a).all? do |x, y|
237
+ x.equal?(y) || x == y || (x.is_a?(Float) && y.is_a?(Float) && x.nan? && y.nan?)
238
+ end
239
+ end
240
+ private_class_method :same_index_values?
241
+
242
+ # The frames that add rows, or the first frame when none does.
243
+ def self.frames_with_rows(frames)
244
+ with_rows = frames.reject { |f| f.nrow.zero? }
245
+ with_rows.empty? ? [frames.first] : with_rows
246
+ end
247
+ private_class_method :frames_with_rows
248
+
98
249
  # ---- index helpers -----------------------------------------------------
99
250
 
100
251
  # Index welded via CArray.meld (view). Every frame must agree: all
@@ -16,21 +16,25 @@ class CAFrame
16
16
  # column name => value; a scalar cell is a Ruby value, an N-D cell is a Ruby
17
17
  # Array, and a masked cell (UNDEF) is +nil+. Normalizing UNDEF -> nil and
18
18
  # CArray -> Array (unlike +each_row+, which yields the raw view with UNDEF /
19
- # CArray slices) is what makes it round-trip: +from_records(df.to_records)+
20
- # rebuilds the same typing, and the result is JSON-serializable.
19
+ # CArray slices) makes the result JSON-serializable and lets
20
+ # +from_records(df.to_records)+ give back the column names, the values and
21
+ # the mask. The rest is not in a record: +from_records+ rebuilds each type
22
+ # from the values (an int32 comes back int64, a time column as an object
23
+ # column of its elements), the index comes back as an ordinary first
24
+ # column, and a frame with no rows gives +[]+, which has no column names.
21
25
  def to_records
22
26
  each_row.map { |row| row.transform_values { |v| record_value(v) } }
23
27
  end
24
28
 
25
29
  # Hand the frame over as a CArray with the minimum work (memo §11.9): a
26
- # 2-D **view** of shape (nrow, nvar), one column per variable in column
30
+ # 2-D **view** of shape (nrow, ncol), one per column in column
27
31
  # order (§12-C). Nothing is materialised — the result is a +CAStack+ over
28
32
  # the stored columns, so reads gather from them and writes flow back
29
33
  # (§3.6). Call +copy+ on it for an independent, owned matrix; +CArray.tabulate+
30
34
  # is the eager sibling that builds one directly.
31
35
  #
32
36
  # Only same-shape scalar (1-D) columns qualify. An N-D column has no single
33
- # matrix form and raises — escape it per column with +df["name"]+. A mixed
37
+ # matrix form and raises — take it per column with +df["name"]+. A mixed
34
38
  # data type set is promoted to a common type (+result_type+, §12-F) through
35
39
  # lazy cast lanes, so the promotion costs no buffer either.
36
40
  #
@@ -48,12 +52,12 @@ class CAFrame
48
52
  if nd
49
53
  raise ArgumentError,
50
54
  "to_ca needs all-scalar (1-D) columns; #{nd.first.inspect} is " \
51
- "#{nd.last.ndim}-D — escape per column with df[name]"
55
+ "#{nd.last.ndim}-D — take it per column with df[name]"
52
56
  end
53
57
  refuse_unshared_columns(cols) if writable
54
58
  begin
55
59
  CArray.stack(cols, axis: 1)
56
- rescue ArgumentError, RuntimeError => e
60
+ rescue ArgumentError, TypeError, CArray::DataTypeError => e
57
61
  # The columns have no common type (a text or Face column beside a
58
62
  # numeric one). Point at the frame-level verb that gives them one.
59
63
  raise e.class,
@@ -87,7 +91,9 @@ class CAFrame
87
91
  if UNDEF.equal?(v)
88
92
  nil
89
93
  elsif v.is_a?(CArray)
90
- v.to_a.map { |e| UNDEF.equal?(e) ? nil : e }
94
+ record_value(v.to_a)
95
+ elsif v.is_a?(Array)
96
+ v.map { |e| record_value(e) }
91
97
  else
92
98
  v
93
99
  end