carray 2.0.0 → 3.0.0

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 (339) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +5 -25
  3. data/CHANGELOG.md +16 -0
  4. data/LICENSE +1 -1
  5. data/NEWS.md +3 -0
  6. data/README.md +128 -44
  7. data/carray.gemspec +22 -24
  8. data/ext/ca_array_pool.c +91 -0
  9. data/ext/ca_axis_descriptor.h +186 -0
  10. data/ext/ca_axis_dispatch.c +924 -0
  11. data/ext/ca_axis_group.c +1208 -0
  12. data/ext/ca_bincmp_dispatch.c +76 -0
  13. data/ext/ca_bincmp_dispatch.h +85 -0
  14. data/ext/ca_binop_dispatch.c +125 -0
  15. data/ext/ca_binop_dispatch.h +159 -0
  16. data/ext/ca_categorical_iterator.c +1375 -0
  17. data/ext/ca_compare.c +94 -0
  18. data/ext/ca_compare.h +26 -0
  19. data/ext/ca_composite_dispatch.c +414 -0
  20. data/ext/ca_composite_dispatch.h +116 -0
  21. data/ext/ca_for_buffer.h +96 -0
  22. data/ext/ca_for_each_element.h +241 -0
  23. data/ext/ca_group_iter.c +304 -0
  24. data/ext/ca_iter_substrate.h +325 -0
  25. data/ext/ca_kernel_iterator.c +4321 -0
  26. data/ext/ca_kernel_iterator.h +2603 -0
  27. data/ext/ca_moncmp_dispatch.c +37 -0
  28. data/ext/ca_moncmp_dispatch.h +62 -0
  29. data/ext/ca_monop_dispatch.c +200 -0
  30. data/ext/ca_monop_dispatch.h +235 -0
  31. data/ext/ca_obj_array.c +355 -359
  32. data/ext/ca_obj_bincmp.c +809 -0
  33. data/ext/ca_obj_binop.c +892 -0
  34. data/ext/ca_obj_bitarray.c +369 -164
  35. data/ext/ca_obj_bitfield.c +294 -234
  36. data/ext/ca_obj_block.c +189 -711
  37. data/ext/ca_obj_byte_swap.c +766 -0
  38. data/ext/ca_obj_const_string.c +965 -0
  39. data/ext/ca_obj_face.c +670 -0
  40. data/ext/ca_obj_face.h +247 -0
  41. data/ext/ca_obj_fake.c +228 -100
  42. data/ext/ca_obj_farray.c +54 -441
  43. data/ext/ca_obj_field.c +82 -529
  44. data/ext/ca_obj_fixlen_string.c +306 -0
  45. data/ext/ca_obj_grid.c +858 -440
  46. data/ext/ca_obj_meld.c +1034 -0
  47. data/ext/ca_obj_moncmp.c +569 -0
  48. data/ext/ca_obj_monop.c +1111 -0
  49. data/ext/ca_obj_object.c +774 -298
  50. data/ext/ca_obj_record.c +468 -0
  51. data/ext/ca_obj_reduce.c +97 -82
  52. data/ext/ca_obj_refer.c +569 -459
  53. data/ext/ca_obj_remap.c +475 -0
  54. data/ext/ca_obj_repeat.c +92 -477
  55. data/ext/ca_obj_roll.c +616 -0
  56. data/ext/ca_obj_select.c +344 -296
  57. data/ext/ca_obj_select_axis.c +1296 -0
  58. data/ext/ca_obj_shift.c +230 -792
  59. data/ext/ca_obj_source.c +78 -0
  60. data/ext/ca_obj_stack.c +1173 -0
  61. data/ext/ca_obj_stride.c +2501 -0
  62. data/ext/ca_obj_string.c +268 -0
  63. data/ext/ca_obj_tile.c +614 -0
  64. data/ext/ca_obj_time.c +546 -0
  65. data/ext/ca_obj_timedelta.c +435 -0
  66. data/ext/ca_obj_transpose.c +62 -516
  67. data/ext/ca_obj_triop.c +746 -0
  68. data/ext/ca_obj_unbound_repeat.c +208 -241
  69. data/ext/ca_obj_window.c +1131 -563
  70. data/ext/ca_op_byte_swap.c +175 -0
  71. data/ext/ca_op_ipower.c +319 -0
  72. data/ext/ca_op_powi.h +88 -0
  73. data/ext/ca_sort_kernels.h +132 -0
  74. data/ext/ca_sweep_engine.c +430 -0
  75. data/ext/ca_sweep_engine.h +157 -0
  76. data/ext/ca_transform_common.c +228 -0
  77. data/ext/ca_triop_dispatch.c +55 -0
  78. data/ext/ca_triop_dispatch.h +62 -0
  79. data/ext/carray.h +795 -402
  80. data/ext/carray_access.c +831 -711
  81. data/ext/carray_attribute.c +98 -330
  82. data/ext/carray_bincount.c +255 -0
  83. data/ext/carray_broadcast.c +283 -0
  84. data/ext/carray_call_cfunc.c +1360 -828
  85. data/ext/carray_call_cfunc.h +160 -0
  86. data/ext/carray_cast.c +1212 -301
  87. data/ext/carray_cast_func.rb +81 -40
  88. data/ext/carray_class.c +53 -63
  89. data/ext/carray_config.h +28 -0
  90. data/ext/carray_conversion.c +350 -346
  91. data/ext/carray_copy.c +156 -268
  92. data/ext/carray_core.c +1342 -199
  93. data/ext/carray_count.c +312 -0
  94. data/ext/carray_data_type.c +43 -19
  95. data/ext/carray_element.c +585 -213
  96. data/ext/carray_factorize.c +2542 -0
  97. data/ext/carray_generate.c +230 -559
  98. data/ext/carray_histogram.c +490 -0
  99. data/ext/carray_hold.c +228 -0
  100. data/ext/carray_index_classifier.c +1035 -0
  101. data/ext/carray_index_classifier.h +27 -0
  102. data/ext/carray_internal.h +120 -0
  103. data/ext/carray_kernels_bincmp.c +4445 -0
  104. data/ext/carray_kernels_binop.c +10979 -0
  105. data/ext/carray_kernels_init.c +36 -0
  106. data/ext/carray_kernels_map.c +3466 -0
  107. data/ext/carray_kernels_moncmp.c +2096 -0
  108. data/ext/carray_kernels_monop.c +18312 -0
  109. data/ext/carray_kernels_reduce_aggregate.c +25836 -0
  110. data/ext/carray_kernels_reduce_boolean.c +329 -0
  111. data/ext/carray_kernels_reduce_cumulative.c +14592 -0
  112. data/ext/carray_kernels_reduce_extreme.c +16947 -0
  113. data/ext/carray_kernels_reduce_variance.c +3909 -0
  114. data/ext/carray_kernels_scan.c +3692 -0
  115. data/ext/carray_kernels_search.c +32137 -0
  116. data/ext/carray_kernels_sort.c +10625 -0
  117. data/ext/carray_kernels_triop.c +1391 -0
  118. data/ext/carray_lazy.c +567 -0
  119. data/ext/carray_loop.c +88 -200
  120. data/ext/carray_mask.c +848 -154
  121. data/ext/carray_math_kernel.h +120 -0
  122. data/ext/carray_mathfunc.c +10 -241
  123. data/ext/carray_median_percentile.c +1257 -0
  124. data/ext/carray_memory_view.c +1625 -0
  125. data/ext/carray_operator.c +1526 -318
  126. data/ext/carray_order.c +664 -1394
  127. data/ext/carray_partition.c +416 -0
  128. data/ext/carray_random.c +518 -0
  129. data/ext/carray_scatter.c +357 -0
  130. data/ext/carray_slab.c +1219 -0
  131. data/ext/carray_slab.h +84 -0
  132. data/ext/carray_sort.c +829 -0
  133. data/ext/carray_sort_kernel.c +620 -0
  134. data/ext/carray_struct.c +695 -0
  135. data/ext/carray_test.c +343 -229
  136. data/ext/carray_undef.c +34 -17
  137. data/ext/carray_utils.c +175 -74
  138. data/ext/extconf.rb +216 -55
  139. data/ext/mk_call_cfunc.rb +480 -0
  140. data/ext/mkkernel.rb +8842 -0
  141. data/ext/ruby_carray.c +202 -101
  142. data/ext/version.h +4 -14
  143. data/ext/version.rb +5 -13
  144. data/lib/carray/arrow_tensor.rb +401 -0
  145. data/lib/carray/attribute.rb +166 -0
  146. data/lib/carray/autoload_carray.rb +220 -0
  147. data/lib/carray/autoload_method_extension.rb +44 -0
  148. data/lib/carray/axis_group.rb +711 -0
  149. data/lib/carray/basics.rb +481 -0
  150. data/lib/carray/bincount_nd.rb +358 -0
  151. data/lib/carray/block_iterator.rb +604 -0
  152. data/lib/carray/boolean_reduce.rb +109 -0
  153. data/lib/carray/categorical.rb +561 -0
  154. data/lib/carray/categorical_iterator.rb +1062 -0
  155. data/lib/carray/complex.rb +150 -0
  156. data/lib/carray/conditional.rb +216 -0
  157. data/lib/carray/const_string.rb +228 -0
  158. data/lib/carray/construct.rb +139 -328
  159. data/lib/carray/core_extensions.rb +240 -0
  160. data/lib/carray/data_type_extension.rb +233 -0
  161. data/lib/carray/fixlen_string.rb +95 -0
  162. data/lib/carray/frame/concat.rb +132 -0
  163. data/lib/carray/frame/convert.rb +95 -0
  164. data/lib/carray/frame/csv_parser.rb +211 -0
  165. data/lib/carray/frame/frame.rb +649 -0
  166. data/lib/carray/frame/group.rb +186 -0
  167. data/lib/carray/frame/io.rb +164 -0
  168. data/lib/carray/frame/join.rb +248 -0
  169. data/lib/carray/frame/records.rb +99 -0
  170. data/lib/carray/frame/sort.rb +113 -0
  171. data/lib/carray/frame/verbs.rb +299 -0
  172. data/lib/carray/frame.rb +16 -0
  173. data/lib/carray/histogram.rb +512 -0
  174. data/lib/carray/inspect.rb +37 -20
  175. data/lib/carray/iterator.rb +57 -349
  176. data/lib/carray/lazy.rb +889 -0
  177. data/lib/carray/mask_gap_fill.rb +200 -0
  178. data/lib/carray/math.rb +78 -342
  179. data/lib/carray/meld_reduce.rb +289 -0
  180. data/lib/carray/methods/align_addr.rb +116 -0
  181. data/lib/carray/methods/bin.rb +128 -0
  182. data/lib/carray/methods/bincount.rb +87 -0
  183. data/lib/carray/methods/bit_string.rb +92 -0
  184. data/lib/carray/methods/broadcast.rb +63 -0
  185. data/lib/carray/methods/choose.rb +39 -0
  186. data/lib/carray/methods/composition.rb +280 -0
  187. data/lib/carray/methods/gather_nd.rb +206 -0
  188. data/lib/carray/methods/index.rb +39 -0
  189. data/lib/carray/methods/insert_block.rb +99 -0
  190. data/lib/carray/methods/is_in.rb +141 -0
  191. data/lib/carray/methods/join.rb +90 -0
  192. data/lib/carray/methods/locate_addr.rb +47 -0
  193. data/lib/carray/methods/mask_duplicates.rb +41 -0
  194. data/lib/carray/methods/meshgrid.rb +91 -0
  195. data/lib/carray/methods/mode.rb +126 -0
  196. data/lib/carray/methods/nunique.rb +46 -0
  197. data/lib/carray/methods/resize.rb +56 -0
  198. data/lib/carray/methods/snap.rb +156 -0
  199. data/lib/carray/methods/string_format.rb +57 -0
  200. data/lib/carray/methods/unique.rb +47 -0
  201. data/lib/carray/methods/value_counts.rb +71 -0
  202. data/lib/carray/mkmf.rb +124 -101
  203. data/lib/carray/runtime.rb +108 -0
  204. data/lib/carray/serialize.rb +478 -167
  205. data/lib/carray/slab_iterator.rb +292 -0
  206. data/lib/carray/stack.rb +291 -0
  207. data/lib/carray/string.rb +56 -180
  208. data/lib/carray/string_operation_extension.rb +289 -0
  209. data/lib/carray/struct.rb +335 -323
  210. data/lib/carray/struct_builder.rb +697 -0
  211. data/lib/carray/table.rb +41 -2
  212. data/lib/carray/time.rb +2255 -38
  213. data/lib/carray/window_iterator.rb +655 -0
  214. data/lib/carray.rb +55 -57
  215. metadata +163 -130
  216. data/Rakefile +0 -51
  217. data/TODO.md +0 -18
  218. data/ext/ca_iter_block.c +0 -257
  219. data/ext/ca_iter_dimension.c +0 -299
  220. data/ext/ca_iter_window.c +0 -214
  221. data/ext/ca_obj_mapping.c +0 -644
  222. data/ext/carray_iterator.c +0 -641
  223. data/ext/carray_math.rb +0 -850
  224. data/ext/carray_numeric.c +0 -259
  225. data/ext/carray_sort_addr.c +0 -254
  226. data/ext/carray_stat.c +0 -2100
  227. data/ext/carray_stat_proc.rb +0 -1999
  228. data/ext/mkmath.rb +0 -741
  229. data/ext/ruby_ccomplex.c +0 -509
  230. data/ext/ruby_float_func.c +0 -86
  231. data/lib/carray/array.rb +0 -8
  232. data/lib/carray/autoload/autoload_base.rb +0 -19
  233. data/lib/carray/autoload/autoload_gem_cairo.rb +0 -9
  234. data/lib/carray/autoload/autoload_gem_ffi.rb +0 -9
  235. data/lib/carray/autoload/autoload_gem_gnuplot.rb +0 -2
  236. data/lib/carray/autoload/autoload_gem_io_csv.rb +0 -14
  237. data/lib/carray/autoload/autoload_gem_io_pg.rb +0 -6
  238. data/lib/carray/autoload/autoload_gem_io_sqlite3.rb +0 -12
  239. data/lib/carray/autoload/autoload_gem_narray.rb +0 -10
  240. data/lib/carray/autoload/autoload_gem_numo_narray.rb +0 -15
  241. data/lib/carray/autoload/autoload_gem_opencv.rb +0 -16
  242. data/lib/carray/autoload/autoload_gem_random.rb +0 -8
  243. data/lib/carray/autoload/autoload_gem_rmagick.rb +0 -23
  244. data/lib/carray/autoload/autoload_gem_zimg.rb +0 -3
  245. data/lib/carray/autoload/autoload_io_imagemagick.rb +0 -6
  246. data/lib/carray/autoload/autoload_math_histogram.rb +0 -5
  247. data/lib/carray/autoload/autoload_math_recurrence.rb +0 -6
  248. data/lib/carray/autoload/autoload_object_iterator.rb +0 -1
  249. data/lib/carray/autoload/autoload_object_link.rb +0 -1
  250. data/lib/carray/autoload/autoload_object_pack.rb +0 -2
  251. data/lib/carray/autoload.rb +0 -141
  252. data/lib/carray/basic.rb +0 -191
  253. data/lib/carray/broadcast.rb +0 -101
  254. data/lib/carray/compose.rb +0 -315
  255. data/lib/carray/convert.rb +0 -115
  256. data/lib/carray/info.rb +0 -110
  257. data/lib/carray/io/imagemagick.rb +0 -235
  258. data/lib/carray/mask.rb +0 -102
  259. data/lib/carray/math/histogram.rb +0 -177
  260. data/lib/carray/math/recurrence.rb +0 -93
  261. data/lib/carray/object/ca_obj_iterator.rb +0 -50
  262. data/lib/carray/object/ca_obj_link.rb +0 -50
  263. data/lib/carray/object/ca_obj_pack.rb +0 -99
  264. data/lib/carray/obsolete.rb +0 -256
  265. data/lib/carray/ordering.rb +0 -181
  266. data/lib/carray/testing.rb +0 -51
  267. data/lib/carray/transform.rb +0 -109
  268. data/misc/Methods.ja.md +0 -182
  269. data/misc/NOTE +0 -51
  270. data/spec/Classes/CABitfield_spec.rb +0 -58
  271. data/spec/Classes/CABlockIterator_spec.rb +0 -114
  272. data/spec/Classes/CABlock_spec.rb +0 -205
  273. data/spec/Classes/CAField_spec.rb +0 -39
  274. data/spec/Classes/CAGrid_spec.rb +0 -75
  275. data/spec/Classes/CAMap_spec.rb +0 -0
  276. data/spec/Classes/CAMapping_spec.rb +0 -105
  277. data/spec/Classes/CAObject_attribute_spec.rb +0 -33
  278. data/spec/Classes/CAObject_spec.rb +0 -33
  279. data/spec/Classes/CARefer_spec.rb +0 -93
  280. data/spec/Classes/CARepeat_spec.rb +0 -65
  281. data/spec/Classes/CASelect_spec.rb +0 -22
  282. data/spec/Classes/CAShift_spec.rb +0 -16
  283. data/spec/Classes/CAStruct_spec.rb +0 -71
  284. data/spec/Classes/CATranspose_spec.rb +0 -60
  285. data/spec/Classes/CAUnboudRepeat_spec.rb +0 -102
  286. data/spec/Classes/CAWindow_spec.rb +0 -54
  287. data/spec/Classes/CAWrap_spec.rb +0 -8
  288. data/spec/Classes/CArray_spec.rb +0 -184
  289. data/spec/Classes/CScalar_spec.rb +0 -55
  290. data/spec/Classes/ex1.rb +0 -46
  291. data/spec/Features/feature_130_spec.rb +0 -19
  292. data/spec/Features/feature_attributes_spec.rb +0 -280
  293. data/spec/Features/feature_boolean_spec.rb +0 -98
  294. data/spec/Features/feature_broadcast.rb +0 -116
  295. data/spec/Features/feature_cast_function.rb +0 -19
  296. data/spec/Features/feature_cast_spec.rb +0 -33
  297. data/spec/Features/feature_class_spec.rb +0 -84
  298. data/spec/Features/feature_complex_spec.rb +0 -42
  299. data/spec/Features/feature_composite_spec.rb +0 -124
  300. data/spec/Features/feature_convert_spec.rb +0 -46
  301. data/spec/Features/feature_copy_spec.rb +0 -123
  302. data/spec/Features/feature_creation_spec.rb +0 -84
  303. data/spec/Features/feature_element_spec.rb +0 -144
  304. data/spec/Features/feature_extream_spec.rb +0 -54
  305. data/spec/Features/feature_generate_spec.rb +0 -74
  306. data/spec/Features/feature_index_spec.rb +0 -69
  307. data/spec/Features/feature_mask_spec.rb +0 -580
  308. data/spec/Features/feature_math_spec.rb +0 -97
  309. data/spec/Features/feature_order_spec.rb +0 -146
  310. data/spec/Features/feature_ref_store_spec.rb +0 -209
  311. data/spec/Features/feature_serialization_spec.rb +0 -125
  312. data/spec/Features/feature_stat_spec.rb +0 -397
  313. data/spec/Features/feature_virtual_spec.rb +0 -48
  314. data/spec/Features/method_eq_spec.rb +0 -81
  315. data/spec/Features/method_is_nan_spec.rb +0 -12
  316. data/spec/Features/method_map_spec.rb +0 -54
  317. data/spec/Features/method_max_with.rb +0 -20
  318. data/spec/Features/method_min_with.rb +0 -19
  319. data/spec/Features/method_ne_spec.rb +0 -18
  320. data/spec/Features/method_project_spec.rb +0 -188
  321. data/spec/Features/method_ref_spec.rb +0 -27
  322. data/spec/Features/method_round_spec.rb +0 -11
  323. data/spec/Features/method_s_linspace_spec.rb +0 -48
  324. data/spec/Features/method_s_span_spec.rb +0 -14
  325. data/spec/Features/method_seq_spec.rb +0 -47
  326. data/spec/Features/method_sort_with.rb +0 -43
  327. data/spec/Features/method_sorted_with.rb +0 -29
  328. data/spec/Features/method_span_spec.rb +0 -42
  329. data/spec/Features/method_wrap_readonly_spec.rb +0 -43
  330. data/spec/UnitTest/test_CAVirtual.rb +0 -214
  331. data/spec/spec_all.rb +0 -10
  332. data/utils/ca_ase.rb +0 -21
  333. data/utils/ca_methods.rb +0 -15
  334. data/utils/cast_checker.rb +0 -30
  335. data/utils/convert_test.rb +0 -73
  336. data/utils/extract_yard.rb +0 -22
  337. data/utils/guess_shape.rb +0 -76
  338. data/utils/monkey_patch_methods.rb +0 -62
  339. data/utils/remove_resource_fork.sh +0 -5
@@ -0,0 +1,186 @@
1
+ # CAFrame group_by (memo §5 spine, §11.6 surface).
2
+ #
3
+ # Grouping is always on the row axis (axis 0). The key is any length-N
4
+ # thing: a column name, several column names (composite key), or an
5
+ # external length-N CArray. Everything routes through categorize -> codes
6
+ # -> group_by_category, so the frame layer only builds the key and hands
7
+ # the group iterator back (exposure, §4.3).
8
+
9
+ class CAFrame
10
+ # Group rows by one or more keys. Each key is a column name (String) or an
11
+ # external length-N CArray. Returns a GroupedFrame.
12
+ def group_by(*keys)
13
+ raise ArgumentError, "group_by needs at least one key" if keys.empty?
14
+ cat = grouping_categorical(keys)
15
+ axis = keys.size == 1 && keys.first.is_a?(String) ? keys.first : "group"
16
+ GroupedFrame.new(self, cat, axis)
17
+ end
18
+
19
+ # Number of rows currently selected — used by group per-group view-frames
20
+ # and elsewhere; already provided by the core (attr_reader :nrow).
21
+
22
+ private def grouping_categorical(keys)
23
+ cols = keys.map { |k| key_column(k) }
24
+ if cols.size == 1
25
+ cols.first.categorize
26
+ else
27
+ # Composite key: one object cell per row holding the tuple of key
28
+ # values, categorized by content (the codes are composed from the
29
+ # per-column keys).
30
+ n = nrow
31
+ CArray.object(n) { |i| cols.map { |c| c[i] } }.categorize
32
+ end
33
+ end
34
+
35
+ private def key_column(key)
36
+ case key
37
+ when String
38
+ self[key]
39
+ when CArray
40
+ unless key.shape[0] == nrow
41
+ raise ArgumentError,
42
+ "external group key length #{key.shape[0]} != nrow #{nrow}"
43
+ end
44
+ key
45
+ else
46
+ raise ArgumentError, "group key must be a column name or CArray (got #{key.class})"
47
+ end
48
+ end
49
+ end
50
+
51
+ # GroupedFrame — the result of +CAFrame#group_by+ (memo §11.6, §16).
52
+ #
53
+ # Holds the grouping categorical once (grouping is per-key, computed once
54
+ # and shared across every column). Three surfaces:
55
+ # grp["col"] -> the CArray group iterator (raw exposure)
56
+ # aggregate -> declarative per-column reductions into a new frame
57
+ # table { |g| }-> cross-column Ruby escape, g is a per-group view-frame
58
+ class GroupedFrame
59
+ def initialize(frame, cat, axis_name)
60
+ @frame = frame
61
+ @cat = cat
62
+ @axis_name = axis_name
63
+ @labels = cat.labels # group values, in code order
64
+ end
65
+
66
+ # Number of groups.
67
+ def ngroup
68
+ @labels.size
69
+ end
70
+
71
+ # Group key values (one per group), as an Array.
72
+ def labels
73
+ @labels.dup
74
+ end
75
+
76
+ # Raw exposure: the CArray group iterator for one column (memo §11.6).
77
+ def [](name)
78
+ @frame[name].group_by_category(@cat)
79
+ end
80
+
81
+ # Declarative aggregation (memo §11.6). Spec maps an output column name to
82
+ # +[input_column, reduction]+, where reduction is a Symbol (vectorized,
83
+ # applied through the group iterator) or a Proc (per-group custom, called
84
+ # with the group's column slice).
85
+ def aggregate(spec)
86
+ cols = {}
87
+ spec.each do |out_name, (in_name, reduction)|
88
+ out = out_name.to_s
89
+ cols[out] =
90
+ case reduction
91
+ when Symbol
92
+ self[in_name].public_send(reduction)
93
+ when Proc
94
+ per_group_column(in_name, reduction)
95
+ else
96
+ raise ArgumentError,
97
+ "reduction must be a Symbol or Proc (got #{reduction.class})"
98
+ end
99
+ end
100
+ CAFrame.new(cols, axis_name: @axis_name, index: label_index)
101
+ end
102
+
103
+ # Cross-column Ruby escape (memo §11.6). The block receives a per-group
104
+ # view-frame and returns a Hash of output-name => value; the values are
105
+ # collected column-wise across groups into a new frame.
106
+ def table
107
+ collected = {}
108
+ order = nil
109
+ each_group_frame do |g|
110
+ out = yield(g)
111
+ unless out.is_a?(Hash)
112
+ raise ArgumentError, "table block must return a Hash (got #{out.class})"
113
+ end
114
+ order ||= out.keys.map(&:to_s)
115
+ out.each { |k, v| (collected[k.to_s] ||= []) << v }
116
+ end
117
+ cols = {}
118
+ (order || []).each do |name|
119
+ vals = collected[name]
120
+ cols[name] = CArray.object(vals.size) { |i| vals[i] }
121
+ end
122
+ CAFrame.new(cols, axis_name: @axis_name, index: label_index)
123
+ end
124
+
125
+ # Convenience reductions over every numeric scalar column (memo §6-4
126
+ # "grp.mean"). Non-numeric / N-D columns are skipped.
127
+ [:sum, :mean, :min, :max].each do |red|
128
+ define_method(red) { reduce_numeric(red) }
129
+ end
130
+
131
+ # @return [String]
132
+ def inspect
133
+ "#<GroupedFrame ngroup=#{ngroup} by=#{@axis_name.inspect}>"
134
+ end
135
+
136
+ private def label_index
137
+ CArray.object(@labels.size) { |i| @labels[i] }
138
+ end
139
+
140
+ NON_NUMERIC = [:object, :boolean, :fixlen].freeze
141
+ private_constant :NON_NUMERIC
142
+
143
+ private def reduce_numeric(reduction)
144
+ cols = {}
145
+ @frame.variable_names.each do |name|
146
+ col = @frame[name]
147
+ next unless col.ndim == 1 && !NON_NUMERIC.include?(col.data_type)
148
+ cols[name] = col.group_by_category(@cat).public_send(reduction)
149
+ end
150
+ CAFrame.new(cols, axis_name: @axis_name, index: label_index)
151
+ end
152
+
153
+ private def per_group_column(in_name, proc)
154
+ results = []
155
+ each_group_slice(in_name) { |slice| results << proc.call(slice) }
156
+ CArray.object(results.size) { |i| results[i] }
157
+ end
158
+
159
+ private def each_group_slice(in_name)
160
+ col = @frame[in_name]
161
+ ngroup.times do |k|
162
+ yield col[group_address(k), *([nil] * (col.ndim - 1))]
163
+ end
164
+ end
165
+
166
+ private def each_group_frame
167
+ ngroup.times { |k| yield @frame[group_address(k)] }
168
+ end
169
+
170
+ private def group_address(k)
171
+ start = group_offsets[k]
172
+ group_perm[start...(start + group_sizes[k])]
173
+ end
174
+
175
+ private def group_perm
176
+ @group_perm ||= @cat.sort_addr
177
+ end
178
+
179
+ private def group_offsets
180
+ @group_offsets ||= @cat.reduceat_index
181
+ end
182
+
183
+ private def group_sizes
184
+ @group_sizes ||= @cat.category_sizes
185
+ end
186
+ end
@@ -0,0 +1,164 @@
1
+ # CAFrame CSV input/output (memo §11.2, §6-2).
2
+
3
+ require "carray/frame/csv_parser"
4
+
5
+ class CAFrame
6
+ # Read a CSV into a frame. The header row supplies column names (Strings,
7
+ # §3.7); every column is built raw as an object CArray of the cell strings
8
+ # (§4.2 -- read and arrange only, no type-inference engine). Casting is a
9
+ # separate step: pass +types:+ ({ "temp" => :float64 }, or the array-key /
10
+ # reverse forms of +cast+) to cast named columns on load, or call +cast+
11
+ # later. Broken cells fail to_type and become UNDEF automatically
12
+ # (parse-mask, §6-2).
13
+ #
14
+ # Parsing uses the built-in fast tokenizer (CSVParser). Options:
15
+ # sep: field separator (default ",")
16
+ # quote: quote character (default '"')
17
+ # strip: trim spaces from unquoted fields (default false, RFC spacing)
18
+ # +encoding+: IO open-mode encoding (default "bom|utf-8", strips a BOM)
19
+ # parser: a callable path -> [headers, rows] to inject another parser
20
+ # (e.g. the stdlib +csv+, or a typed-table source); when given,
21
+ # sep/quote/strip/encoding and any block are that parser's concern.
22
+ #
23
+ # A block gives reading control for files with preamble lines, a units row,
24
+ # or no header (memo §11.2), using +skip+ / +header+ / +column_names+ /
25
+ # +body+ (see CSVReader). Without a block the default is +header+ then +body+.
26
+ #
27
+ # CAFrame.from_csv("obs.csv") do
28
+ # skip 2; header; skip 1; body
29
+ # end
30
+ #
31
+ # Columns are handed to the frame as CABlock views over one backing object
32
+ # array (§3.6 view-by-default); casting a column materializes it, and +copy+
33
+ # gives an independent frame.
34
+ def self.from_csv(path, types: nil,
35
+ sep: ",", quote: '"', strip: false,
36
+ encoding: "bom|utf-8", parser: nil, &block)
37
+ names, rows =
38
+ if parser
39
+ parser.call(path)
40
+ else
41
+ File.open(path, "r:#{encoding}") do |io|
42
+ reader = CSVReader.new(io, sep: sep, quote: quote, strip: strip)
43
+ if block
44
+ block.arity == 1 ? block.call(reader) : reader.instance_exec(&block)
45
+ else
46
+ reader.header
47
+ reader.body
48
+ end
49
+ reader.result
50
+ end
51
+ end
52
+
53
+ frame = build_frame(names, rows)
54
+ frame.cast(types) if types
55
+ frame
56
+ end
57
+
58
+ # Build a frame from parsed [names, rows]. When names is nil (headerless and
59
+ # no column_names) positional names "c0".."cN" are generated from the widest
60
+ # row. Rows are squared off to the column count (short rows padded with nil,
61
+ # over-long rows raise), one 2-D object array is bulk-filled, and each column
62
+ # is a view into it (§3.6).
63
+ # Write the frame as CSV. CSV is a flat table of scalar cells, so this is the
64
+ # text form of the same all-scalar subset +to_ca+ requires (§11.9): every
65
+ # column must be 1-D. Unlike +to_ca+ it does not promote to a common dtype --
66
+ # each column is formatted to text independently, so mixed dtypes (numbers,
67
+ # strings, datetime / categorical Faces) sit side by side. An N-D column has
68
+ # no flat CSV cell and raises; export it per column, or use +to_records+ +
69
+ # JSON for the structured shape (memo §11.9, the N-D escape).
70
+ #
71
+ # With +path+, writes the file and returns self; without it, returns the CSV
72
+ # String. The index (if any) is written as the first column under +axis_name+
73
+ # unless +index: false+. A masked cell (UNDEF) becomes an empty field, which
74
+ # +from_csv+ reads back as UNDEF (parse-mask, §6-2) -- so mask round-trips. A
75
+ # genuine empty string is written quoted (+""+) to stay distinct from missing,
76
+ # matching the tokenizer's own unquoted-empty vs quoted-empty distinction.
77
+ #
78
+ # df.to_csv("out.csv") # write file
79
+ # csv = df.to_csv # get a String
80
+ #
81
+ # Options: +sep+ / +quote+ mirror +from_csv+; +header+ writes the name row
82
+ # (default true); +index+ writes the index column (default true).
83
+ def to_csv(path = nil, sep: ",", quote: '"', header: true, index: true)
84
+ nd = @columns.find { |_, c| c.ndim != 1 }
85
+ if nd
86
+ raise ArgumentError,
87
+ "to_csv needs all-scalar (1-D) columns; #{nd.first.inspect} is " \
88
+ "#{nd.last.ndim}-D — export it per column or via to_records + JSON"
89
+ end
90
+
91
+ names = []
92
+ formatted = []
93
+ if index && @index
94
+ names << @axis_name
95
+ formatted << format_csv_column(@index)
96
+ end
97
+ @columns.each do |name, col|
98
+ names << name
99
+ formatted << format_csv_column(col)
100
+ end
101
+
102
+ out = +""
103
+ if header
104
+ out << names.map { |t| quote_csv_field(t, sep, quote) }.join(sep) << "\n"
105
+ end
106
+ @nrow.times do |i|
107
+ out << formatted.map { |fcol| quote_csv_field(fcol[i], sep, quote) }.join(sep) << "\n"
108
+ end
109
+
110
+ if path
111
+ File.write(path, out)
112
+ self
113
+ else
114
+ out
115
+ end
116
+ end
117
+
118
+ private def format_csv_column(col)
119
+ col.to_a.map do |e|
120
+ if UNDEF.equal?(e) || e.nil?
121
+ nil
122
+ elsif e.is_a?(String)
123
+ e
124
+ elsif e.respond_to?(:iso8601)
125
+ e.iso8601
126
+ else
127
+ e.to_s
128
+ end
129
+ end
130
+ end
131
+
132
+ private def quote_csv_field(text, sep, quote)
133
+ return "" if text.nil?
134
+ if text.empty? || text.include?(sep) || text.include?(quote) ||
135
+ text.include?("\n") || text.include?("\r")
136
+ quote + text.gsub(quote, quote * 2) + quote
137
+ else
138
+ text
139
+ end
140
+ end
141
+
142
+ def self.build_frame(names, rows)
143
+ ncol = names ? names.size : (rows.map(&:size).max || 0)
144
+ names ||= Array.new(ncol) { |j| "c#{j}" }
145
+
146
+ cols = {}
147
+ if rows.empty?
148
+ names.each { |name| cols[name] = CArray.object(0) }
149
+ return new(cols)
150
+ end
151
+ rows.each_with_index do |r, i|
152
+ if r.size < ncol
153
+ r.concat(Array.new(ncol - r.size))
154
+ elsif r.size > ncol
155
+ raise ArgumentError,
156
+ "row #{i + 1} has #{r.size} fields, expected #{ncol}"
157
+ end
158
+ end
159
+ table = CArray.object(rows.size, ncol) { rows }
160
+ names.each_with_index { |name, j| cols[name] = table[nil, j] }
161
+ new(cols)
162
+ end
163
+ private_class_method :build_frame
164
+ end
@@ -0,0 +1,248 @@
1
+ # CAFrame join (memo §5, §11.7, §13.3).
2
+ #
3
+ # Join is a thin delegation to landed addressing primitives: the join key
4
+ # yields an address array (locate_addr for a row-preserving left lookup,
5
+ # align_addr for symmetric set alignment), and each column is gathered by
6
+ # +project+ (length-preserving, miss -> UNDEF, Face-lift, read-only safe).
7
+ # The frame layer just distributes that address to every column.
8
+
9
+ class CAFrame
10
+ # Default disambiguation for non-key columns present on both sides (§12-C):
11
+ # left gets "_left", right gets "_right". +_left+/+_right+ read clearer than
12
+ # pandas' +_x+/+_y+ for the target audience.
13
+ DEFAULT_JOIN_SUFFIXES = ["_left", "_right"].freeze
14
+ private_constant :DEFAULT_JOIN_SUFFIXES
15
+
16
+ # Join +other+ on a shared key column.
17
+ #
18
+ # how: :left (default) — keep all left rows; right columns gathered per
19
+ # left row (locate_addr), misses become UNDEF.
20
+ # how: :inner/:outer/:right — set-align both key sets (align_addr) and
21
+ # gather both sides; the aligned key values form
22
+ # the +on+ column.
23
+ #
24
+ # A non-key column present on both sides collides. The key column (+on+) is
25
+ # kept once, never suffixed. By default the collision is resolved by suffixing
26
+ # both sides (+suffixes: ["_left", "_right"]+); pass a 2-element array to pick
27
+ # meaningful names up front (+["_obs", "_fcst"]+), or +suffixes: false+ to
28
+ # raise instead. Rename afterward with +rename+ if needed (§12-C).
29
+ def join(other, on:, how: :left, suffixes: DEFAULT_JOIN_SUFFIXES)
30
+ on = on.to_s
31
+ lkey = self[on]
32
+ rkey = other[on]
33
+ plan = join_name_plan(other, on, suffixes)
34
+
35
+ case how
36
+ when :left
37
+ join_left(other, on, lkey, rkey, plan)
38
+ when :inner, :outer, :right
39
+ join_align(other, on, lkey, rkey, how, plan)
40
+ else
41
+ raise ArgumentError, "unknown join mode #{how.inspect}"
42
+ end
43
+ end
44
+
45
+ # As-of (nearest-key) left join for irregular series (memo §11.7). Each
46
+ # left row is matched to the nearest +other+ row by +on+ via
47
+ # locate_nearest_addr (the per-row, row-preserving counterpart of
48
+ # align_nearest_addr); +direction:+ follows CArray (:floor = most recent
49
+ # at-or-before, :ceil = next, :round = nearest), and rows with no match
50
+ # in range or beyond +tolerance:+ come back UNDEF. Same wiring as the left
51
+ # join, only the address primitive differs.
52
+ def join_asof(other, on:, direction: :floor, tolerance: nil, suffixes: DEFAULT_JOIN_SUFFIXES)
53
+ on = on.to_s
54
+ plan = join_name_plan(other, on, suffixes)
55
+ addr = self[on].locate_nearest_addr(other[on], direction: direction, tolerance: tolerance)
56
+ join_by_addr(other, on, addr, plan)
57
+ end
58
+
59
+ # Conform every variable to an externally supplied reference key set (the
60
+ # asymmetric sibling of +join+; pandas +reindex+). Given a +reference+ array
61
+ # of key values, each column is gathered onto it by exact key match
62
+ # (+locate_addr+): the aligned key column (or index) becomes the reference
63
+ # itself, and a reference key absent from the source comes back UNDEF in
64
+ # every other column.
65
+ #
66
+ # CAFrame measures no interval and generates nothing -- the caller owns the
67
+ # reference. This is the primitive for reindexing to a caller-built axis
68
+ # (e.g. a complete 10-minute time grid): supply that axis as +reference+ and
69
+ # the gaps fill with UNDEF rows carrying only the reference key.
70
+ #
71
+ # df.align("time", reftime) # reftime = a CArray of reference key values
72
+ #
73
+ # +key+ may be a column name or the index axis name. +reference+ is a CArray
74
+ # (or Array); its keys are matched against the source key by value, so their
75
+ # data types must be comparable (a DateTime object key matches by eql?/hash).
76
+ def align(key, reference)
77
+ key = key.to_s
78
+ ref = reference.is_a?(CArray) ? reference : reference.to_ca
79
+ on_index = !@columns.key?(key) && @axis_name == key && @index
80
+ addr = ref.locate_addr(on_index ? @index : self[key])
81
+
82
+ cols = {}
83
+ variable_names.each { |name| cols[name] = project_rows(self[name], addr) }
84
+ if on_index
85
+ CAFrame.new(cols, axis_name: key, index: ref)
86
+ else
87
+ cols[key] = ref
88
+ CAFrame.new(cols, axis_name: @axis_name,
89
+ index: @index && project_rows(@index, addr))
90
+ end
91
+ end
92
+
93
+ # Paste +other+'s variables beside this frame's, matched by **row position**
94
+ # (memo §12-C) — a keyless column merge (the column-direction counterpart of
95
+ # the row-stacking +concat+, and the positional counterpart of the key-aligned
96
+ # +join+; named after the UNIX +paste+). Both frames must have the same
97
+ # +nrow+; rows are assumed to already correspond (no key alignment, consistent
98
+ # with the no-implicit-align stance). Colliding column names are disambiguated
99
+ # by the same policy as +join+ (default suffix +_left+/+_right+, +suffixes:+ to
100
+ # override, +suffixes: false+ to raise). This frame's index is kept; +other+'s
101
+ # index, if any, is not carried (only its columns are pasted). Returns a new
102
+ # frame.
103
+ #
104
+ # obs.paste(fcst) # side by side, same rows
105
+ # obs.paste(fcst, suffixes: ["_obs", "_fcst"])
106
+ def paste(other, suffixes: DEFAULT_JOIN_SUFFIXES)
107
+ CAFrame.paste(self, other, suffixes: suffixes)
108
+ end
109
+
110
+ # Paste N frames side by side by row position (symmetric N-ary sibling of
111
+ # the instance +#paste+; see the instance verb's docstring for the pair
112
+ # semantics). All frames must have the same +nrow+. The result's
113
+ # +axis_name+ and +index+ come from the first frame (peers, but a base is
114
+ # needed for the index). Column names that appear in more than one frame
115
+ # are colliding — pass +suffixes:+ as an Array of exactly K strings (one
116
+ # per input frame) to disambiguate them; otherwise the collision raises.
117
+ #
118
+ # CAFrame.paste(obs, fcst)
119
+ # CAFrame.paste(obs, ecmwf, gfs, suffixes: ["_obs", "_ecmwf", "_gfs"])
120
+ # CAFrame.paste([obs, fcst]) # Array is accepted too
121
+ def self.paste(*frames, suffixes: DEFAULT_JOIN_SUFFIXES)
122
+ frames = frames.flatten
123
+ raise ArgumentError, "paste requires at least one frame" if frames.empty?
124
+ unless frames.all? { |f| f.is_a?(CAFrame) }
125
+ raise ArgumentError, "paste expects CAFrame arguments"
126
+ end
127
+ first = frames.first
128
+ nrow = first.nrow
129
+ frames.each_with_index do |f, i|
130
+ next if i.zero?
131
+ unless f.nrow == nrow
132
+ raise ArgumentError,
133
+ "paste: row-count mismatch (frame #{i}: #{f.nrow} vs #{nrow}); " \
134
+ "use join for key alignment"
135
+ end
136
+ end
137
+
138
+ # Detect columns that appear in 2+ frames (collisions across the K inputs).
139
+ name_count = Hash.new(0)
140
+ frames.each { |f| f.variable_names.each { |name| name_count[name] += 1 } }
141
+ collisions = name_count.select { |_, n| n > 1 }.keys
142
+
143
+ if collisions.any?
144
+ if suffixes == false
145
+ raise ArgumentError,
146
+ "paste column name collision: #{collisions.inspect} " \
147
+ "(pass suffixes: [\"_a\", \"_b\", ...] with one entry per frame, " \
148
+ "or rename first)"
149
+ end
150
+ # For the 2-frame case DEFAULT_JOIN_SUFFIXES ("_left", "_right") works
151
+ # as-is; for K > 2 the caller must pass a K-length Array (or use the
152
+ # pair-only default indirectly via the instance method).
153
+ unless suffixes.is_a?(Array) && suffixes.size == frames.size &&
154
+ suffixes.all? { |s| s.is_a?(String) }
155
+ raise ArgumentError,
156
+ "paste: suffixes must be an Array of #{frames.size} Strings " \
157
+ "(one per frame), got #{suffixes.inspect}"
158
+ end
159
+ end
160
+
161
+ cols = {}
162
+ frames.each_with_index do |f, i|
163
+ f.variable_names.each do |name|
164
+ key = collisions.include?(name) ? "#{name}#{suffixes[i]}" : name
165
+ if cols.key?(key)
166
+ raise ArgumentError,
167
+ "paste: column name #{key.inspect} produced twice " \
168
+ "(collision after suffix from frame #{i})"
169
+ end
170
+ cols[key] = f[name]
171
+ end
172
+ end
173
+ new(cols, axis_name: first.axis_name, index: first.index)
174
+ end
175
+
176
+ private def join_name_plan(other, on, suffixes)
177
+ collisions = (variable_names & other.variable_names) - [on]
178
+ return { collisions: [], lsuf: nil, rsuf: nil } if collisions.empty?
179
+
180
+ if suffixes == false
181
+ raise ArgumentError,
182
+ "join column name collision: #{collisions.inspect} " \
183
+ "(pass suffixes: [\"_l\", \"_r\"] to disambiguate, or rename first)"
184
+ end
185
+ unless suffixes.is_a?(Array) && suffixes.size == 2 && suffixes.all? { |s| s.is_a?(String) }
186
+ raise ArgumentError,
187
+ "suffixes must be false or a 2-element array of Strings, got #{suffixes.inspect}"
188
+ end
189
+ { collisions: collisions, lsuf: suffixes[0], rsuf: suffixes[1] }
190
+ end
191
+
192
+ private def join_name(name, suffix, plan)
193
+ plan[:collisions].include?(name) ? "#{name}#{suffix}" : name
194
+ end
195
+
196
+ private def put_join_column(cols, name, col)
197
+ if cols.key?(name)
198
+ raise ArgumentError,
199
+ "join produces a duplicate column #{name.inspect} after suffixing; " \
200
+ "rename a column first or pass different suffixes"
201
+ end
202
+ cols[name] = col
203
+ end
204
+
205
+ private def join_left(other, on, lkey, rkey, plan)
206
+ join_by_addr(other, on, lkey.locate_addr(rkey), plan)
207
+ end
208
+
209
+ private def join_by_addr(other, on, addr, plan)
210
+ cols = {}
211
+ variable_names.each { |name| put_join_column(cols, join_name(name, plan[:lsuf], plan), self[name]) }
212
+ other.variable_names.each do |name|
213
+ next if name == on
214
+ put_join_column(cols, join_name(name, plan[:rsuf], plan), project_rows(other[name], addr))
215
+ end
216
+ CAFrame.new(cols, axis_name: @axis_name, index: @index)
217
+ end
218
+
219
+ private def join_align(other, on, lkey, rkey, how, plan)
220
+ common, a_idx, b_idx = CArray.align_addr(lkey, rkey, join: how)
221
+ cols = {}
222
+ variable_names.each do |name|
223
+ if name == on
224
+ put_join_column(cols, on, common)
225
+ else
226
+ put_join_column(cols, join_name(name, plan[:lsuf], plan), project_rows(self[name], a_idx))
227
+ end
228
+ end
229
+ other.variable_names.each do |name|
230
+ next if name == on
231
+ put_join_column(cols, join_name(name, plan[:rsuf], plan), project_rows(other[name], b_idx))
232
+ end
233
+ # Carry the left index the same way its columns are gathered (by a_idx),
234
+ # so it stays consistent with the left join; rows with no left match
235
+ # (outer/right) become UNDEF.
236
+ new_index = @index && project_rows(@index, a_idx)
237
+ CAFrame.new(cols, axis_name: @axis_name, index: new_index)
238
+ end
239
+
240
+ private def project_rows(col, addr)
241
+ return col.project(addr) if col.ndim == 1
242
+ trailing = col.shape[1..]
243
+ t = trailing.inject(1, :*)
244
+ n = addr.shape[0]
245
+ flat = addr.reshape(n, 1) * t + CArray.int64(t).seq.reshape(1, t)
246
+ col.project(flat).reshape(n, *trailing)
247
+ end
248
+ end
@@ -0,0 +1,99 @@
1
+ # CAFrame record (row-oriented) input (memo §11.2, §11.5).
2
+ #
3
+ # from_records takes an Array of row Hashes -- the shape JSON.parse yields for
4
+ # a JSON array of objects -- and arranges it into columns. Unlike from_csv,
5
+ # the cell values are already typed Ruby objects (Float / Integer / DateTime /
6
+ # String), so a homogeneous column is built at its native leaf type rather than
7
+ # left as strings ("arrange by the value's own type", not string inference --
8
+ # §4.2 stays intact: no date-like string is parsed, mixed columns stay object).
9
+ #
10
+ # An Array-valued cell that is the same length across every record becomes an
11
+ # N-D column (§11.5): { "temp" => [min, mean, max] } over N records is one
12
+ # (N, 3) column. Ragged or non-numeric arrays fall back to an object column.
13
+ #
14
+ # Missing keys and explicit nils become UNDEF for numeric columns (mask, not a
15
+ # float promotion): an int column with a hole stays int + UNDEF.
16
+
17
+ class CAFrame
18
+ # Build a frame from an Array of row Hashes. Column set is the union of keys
19
+ # in first-appearance order; keys are stringified. +types:+ casts named
20
+ # columns afterward (same map / array-key forms as +cast+).
21
+ def self.from_records(records, types: nil)
22
+ unless records.is_a?(Array) && records.all? { |r| r.is_a?(Hash) }
23
+ raise ArgumentError, "from_records expects an Array of Hashes"
24
+ end
25
+ return new if records.empty?
26
+
27
+ keys = record_key_union(records)
28
+ n = records.size
29
+ cols = {}
30
+ keys.each do |key|
31
+ values = records.map { |r| r[key] }
32
+ cols[key.to_s] = build_record_column(values, n)
33
+ end
34
+
35
+ frame = new(cols)
36
+ frame.cast(types) if types
37
+ frame
38
+ end
39
+
40
+ # Union of record keys in first-appearance order (original key objects, so a
41
+ # String- or Symbol-keyed set both work; the column name stringifies later).
42
+ def self.record_key_union(records)
43
+ seen = {}
44
+ keys = []
45
+ records.each do |r|
46
+ r.each_key do |k|
47
+ unless seen.key?(k)
48
+ seen[k] = true
49
+ keys << k
50
+ end
51
+ end
52
+ end
53
+ keys
54
+ end
55
+ private_class_method :record_key_union
56
+
57
+ # Arrange one column's values (Ruby objects, nil for missing) into a CArray.
58
+ # All-Array cells of equal length -> N-D native column; numeric scalars ->
59
+ # native scalar column (nil -> UNDEF); anything else -> object column.
60
+ def self.build_record_column(values, n)
61
+ present = values.reject(&:nil?)
62
+ return CArray.object(n) { values } if present.empty?
63
+
64
+ if present.all? { |v| v.is_a?(Array) || v.is_a?(CArray) }
65
+ build_nd_column(values, present, n)
66
+ else
67
+ type = numeric_leaf_type(present)
68
+ col = CArray.object(n) { values }
69
+ type ? col.to_type(type) : col
70
+ end
71
+ end
72
+ private_class_method :build_record_column
73
+
74
+ # Stack equal-length array cells into an (N, L) column via an object 2-D fill
75
+ # + to_type (nil rows -> UNDEF, int/float by leaf). Ragged lengths or
76
+ # non-numeric leaves fall back to a 1-D object column of the raw cells.
77
+ def self.build_nd_column(values, present, n)
78
+ lengths = present.map { |v| v.is_a?(CArray) ? v.shape[0] : v.size }
79
+ len = lengths.first
80
+ return CArray.object(n) { values } unless lengths.all? { |x| x == len }
81
+
82
+ nested = values.map { |v| v.nil? ? Array.new(len) : (v.is_a?(CArray) ? v.to_a : v) }
83
+ type = numeric_leaf_type(nested.flatten.compact)
84
+ table = CArray.object(n, len) { nested }
85
+ type ? table.to_type(type) : table
86
+ end
87
+ private_class_method :build_nd_column
88
+
89
+ # :int64 if every value is an Integer, :float64 if all are Numeric (int/float
90
+ # mix), otherwise nil (strings / DateTime / booleans / mixed -> keep object).
91
+ def self.numeric_leaf_type(values)
92
+ if values.all? { |v| v.is_a?(Integer) }
93
+ :int64
94
+ elsif values.all? { |v| v.is_a?(Numeric) }
95
+ :float64
96
+ end
97
+ end
98
+ private_class_method :numeric_leaf_type
99
+ end