carray 2.0.1 → 3.0.1

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 (386) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +6 -25
  3. data/CHANGELOG.md +338 -0
  4. data/{NEWS.md → CHANGELOG.v1.md} +3 -0
  5. data/LICENSE +1 -1
  6. data/README.md +120 -36
  7. data/carray.gemspec +32 -30
  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 +1244 -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 +124 -0
  15. data/ext/ca_binop_dispatch.h +152 -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 +239 -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 +4367 -0
  26. data/ext/ca_kernel_iterator.h +2596 -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 +839 -0
  33. data/ext/ca_obj_binop.c +948 -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 +967 -0
  39. data/ext/ca_obj_face.c +750 -0
  40. data/ext/ca_obj_face.h +279 -0
  41. data/ext/ca_obj_fake.c +239 -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 +308 -0
  45. data/ext/ca_obj_grid.c +866 -440
  46. data/ext/ca_obj_meld.c +1039 -0
  47. data/ext/ca_obj_moncmp.c +588 -0
  48. data/ext/ca_obj_monop.c +1123 -0
  49. data/ext/ca_obj_object.c +866 -296
  50. data/ext/ca_obj_record.c +470 -0
  51. data/ext/ca_obj_reduce.c +97 -82
  52. data/ext/ca_obj_refer.c +593 -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 +624 -0
  56. data/ext/ca_obj_select.c +344 -296
  57. data/ext/ca_obj_select_axis.c +1306 -0
  58. data/ext/ca_obj_shift.c +231 -793
  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 +2584 -0
  62. data/ext/ca_obj_string.c +270 -0
  63. data/ext/ca_obj_tile.c +622 -0
  64. data/ext/ca_obj_time.c +548 -0
  65. data/ext/ca_obj_timedelta.c +437 -0
  66. data/ext/ca_obj_transpose.c +62 -516
  67. data/ext/ca_obj_triop.c +785 -0
  68. data/ext/ca_obj_window.c +1202 -565
  69. data/ext/ca_op_byte_swap.c +175 -0
  70. data/ext/ca_op_cmplx64.h +123 -0
  71. data/ext/ca_op_ipower.c +316 -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 +473 -0
  75. data/ext/ca_sweep_engine.h +166 -0
  76. data/ext/ca_transform_common.c +235 -0
  77. data/ext/ca_triop_dispatch.c +55 -0
  78. data/ext/ca_triop_dispatch.h +62 -0
  79. data/ext/carray.h +810 -420
  80. data/ext/carray_access.c +873 -731
  81. data/ext/carray_attribute.c +98 -329
  82. data/ext/carray_bincount.c +255 -0
  83. data/ext/carray_broadcast.c +376 -0
  84. data/ext/carray_build_flags.h +3 -0
  85. data/ext/carray_call_cfunc.c +2897 -874
  86. data/ext/carray_call_cfunc.h +313 -0
  87. data/ext/carray_cast.c +1264 -315
  88. data/ext/carray_cast_func.rb +81 -40
  89. data/ext/carray_class.c +53 -63
  90. data/ext/carray_config.h +28 -0
  91. data/ext/carray_conversion.c +350 -346
  92. data/ext/carray_copy.c +168 -270
  93. data/ext/carray_core.c +1396 -206
  94. data/ext/carray_count.c +312 -0
  95. data/ext/carray_data_type.c +43 -19
  96. data/ext/carray_element.c +585 -213
  97. data/ext/carray_factorize.c +2542 -0
  98. data/ext/carray_generate.c +230 -559
  99. data/ext/carray_histogram.c +490 -0
  100. data/ext/carray_hold.c +228 -0
  101. data/ext/carray_index_classifier.c +1021 -0
  102. data/ext/carray_index_classifier.h +27 -0
  103. data/ext/carray_internal.h +136 -0
  104. data/ext/carray_kernels_bincmp.c +4446 -0
  105. data/ext/carray_kernels_binop.c +11001 -0
  106. data/ext/carray_kernels_init.c +1131 -0
  107. data/ext/carray_kernels_map.c +3467 -0
  108. data/ext/carray_kernels_moncmp.c +2097 -0
  109. data/ext/carray_kernels_monop.c +18313 -0
  110. data/ext/carray_kernels_reduce_aggregate.c +25837 -0
  111. data/ext/carray_kernels_reduce_boolean.c +330 -0
  112. data/ext/carray_kernels_reduce_cumulative.c +14593 -0
  113. data/ext/carray_kernels_reduce_extreme.c +16948 -0
  114. data/ext/carray_kernels_reduce_variance.c +3910 -0
  115. data/ext/carray_kernels_scan.c +3693 -0
  116. data/ext/carray_kernels_search.c +32138 -0
  117. data/ext/carray_kernels_sort.c +10626 -0
  118. data/ext/carray_kernels_triop.c +1392 -0
  119. data/ext/carray_lazy.c +737 -0
  120. data/ext/carray_loop.c +88 -200
  121. data/ext/carray_mask.c +853 -158
  122. data/ext/carray_math_kernel.h +120 -0
  123. data/ext/carray_mathfunc.c +10 -241
  124. data/ext/carray_median_percentile.c +1257 -0
  125. data/ext/carray_memory_view.c +1650 -0
  126. data/ext/carray_operator.c +1525 -320
  127. data/ext/carray_order.c +664 -1394
  128. data/ext/carray_partition.c +416 -0
  129. data/ext/carray_random.c +518 -0
  130. data/ext/carray_scatter.c +357 -0
  131. data/ext/carray_slab.c +1219 -0
  132. data/ext/carray_slab.h +84 -0
  133. data/ext/carray_sort.c +829 -0
  134. data/ext/carray_sort_kernel.c +620 -0
  135. data/ext/carray_struct.c +695 -0
  136. data/ext/carray_test.c +343 -229
  137. data/ext/carray_undef.c +34 -17
  138. data/ext/carray_utils.c +175 -74
  139. data/ext/extconf.rb +234 -55
  140. data/ext/mk_call_cfunc.rb +671 -0
  141. data/ext/mkkernel.rb +9096 -0
  142. data/ext/ruby_carray.c +211 -108
  143. data/ext/version.h +4 -14
  144. data/ext/version.rb +5 -13
  145. data/lib/carray/arrow_tensor.rb +401 -0
  146. data/lib/carray/attribute.rb +166 -0
  147. data/lib/carray/autoload_carray.rb +239 -0
  148. data/lib/carray/autoload_method_extension.rb +45 -0
  149. data/lib/carray/axis_group.rb +711 -0
  150. data/lib/carray/basics.rb +481 -0
  151. data/lib/carray/bincount_nd.rb +358 -0
  152. data/lib/carray/block_iterator.rb +614 -0
  153. data/lib/carray/boolean_reduce.rb +109 -0
  154. data/lib/carray/categorical.rb +561 -0
  155. data/lib/carray/categorical_iterator.rb +1084 -0
  156. data/lib/carray/complex.rb +150 -0
  157. data/lib/carray/conditional.rb +216 -0
  158. data/lib/carray/const_string.rb +228 -0
  159. data/lib/carray/construct.rb +160 -328
  160. data/lib/carray/core_extensions.rb +297 -0
  161. data/lib/carray/data_type_extension.rb +250 -0
  162. data/lib/carray/fixlen_string.rb +95 -0
  163. data/lib/carray/frame/concat.rb +132 -0
  164. data/lib/carray/frame/convert.rb +95 -0
  165. data/lib/carray/frame/csv_parser.rb +211 -0
  166. data/lib/carray/frame/frame.rb +642 -0
  167. data/lib/carray/frame/group.rb +186 -0
  168. data/lib/carray/frame/io.rb +321 -0
  169. data/lib/carray/frame/join.rb +248 -0
  170. data/lib/carray/frame/records.rb +99 -0
  171. data/lib/carray/frame/sort.rb +113 -0
  172. data/lib/carray/frame/verbs.rb +316 -0
  173. data/lib/carray/frame.rb +16 -0
  174. data/lib/carray/fuse_source.rb +123 -0
  175. data/lib/carray/fusion.rb +218 -0
  176. data/lib/carray/histogram.rb +512 -0
  177. data/lib/carray/inspect.rb +37 -26
  178. data/lib/carray/iterator.rb +58 -349
  179. data/lib/carray/lazy.rb +941 -0
  180. data/lib/carray/mask_gap_fill.rb +200 -0
  181. data/lib/carray/math.rb +78 -342
  182. data/lib/carray/meld_reduce.rb +289 -0
  183. data/lib/carray/methods/align_addr.rb +116 -0
  184. data/lib/carray/methods/bin.rb +128 -0
  185. data/lib/carray/methods/bincount.rb +87 -0
  186. data/lib/carray/methods/bit_string.rb +92 -0
  187. data/lib/carray/methods/broadcast.rb +63 -0
  188. data/lib/carray/methods/choose.rb +39 -0
  189. data/lib/carray/methods/composition.rb +280 -0
  190. data/lib/carray/methods/gather_nd.rb +206 -0
  191. data/lib/carray/methods/index.rb +39 -0
  192. data/lib/carray/methods/insert_block.rb +99 -0
  193. data/lib/carray/methods/is_in.rb +141 -0
  194. data/lib/carray/methods/join.rb +90 -0
  195. data/lib/carray/methods/locate_addr.rb +52 -0
  196. data/lib/carray/methods/mask_duplicates.rb +41 -0
  197. data/lib/carray/methods/meshgrid.rb +90 -0
  198. data/lib/carray/methods/mode.rb +126 -0
  199. data/lib/carray/methods/nunique.rb +46 -0
  200. data/lib/carray/methods/resize.rb +56 -0
  201. data/lib/carray/methods/snap.rb +161 -0
  202. data/lib/carray/methods/string_format.rb +57 -0
  203. data/lib/carray/methods/unique.rb +47 -0
  204. data/lib/carray/methods/value_counts.rb +71 -0
  205. data/lib/carray/mkmf.rb +124 -101
  206. data/lib/carray/runtime.rb +89 -0
  207. data/lib/carray/serialize.rb +478 -167
  208. data/lib/carray/slab_iterator.rb +305 -0
  209. data/lib/carray/stack.rb +291 -0
  210. data/lib/carray/string.rb +56 -180
  211. data/lib/carray/string_operation_extension.rb +289 -0
  212. data/lib/carray/struct.rb +335 -323
  213. data/lib/carray/struct_builder.rb +697 -0
  214. data/lib/carray/table.rb +41 -2
  215. data/lib/carray/time.rb +2654 -38
  216. data/lib/carray/window_iterator.rb +927 -0
  217. data/lib/carray.rb +55 -57
  218. data/yard-stubs/ca_obj_array.rb +385 -0
  219. data/yard-stubs/ca_obj_bitarray.rb +38 -0
  220. data/yard-stubs/ca_obj_bitfield.rb +43 -0
  221. data/yard-stubs/ca_obj_block.rb +73 -0
  222. data/yard-stubs/ca_obj_byte_swap.rb +56 -0
  223. data/yard-stubs/ca_obj_fake.rb +31 -0
  224. data/yard-stubs/ca_obj_farray.rb +32 -0
  225. data/yard-stubs/ca_obj_field.rb +45 -0
  226. data/yard-stubs/ca_obj_grid.rb +35 -0
  227. data/yard-stubs/ca_obj_refer.rb +72 -0
  228. data/yard-stubs/ca_obj_roll.rb +45 -0
  229. data/yard-stubs/ca_obj_shift.rb +43 -0
  230. data/yard-stubs/ca_obj_stride.rb +181 -0
  231. data/yard-stubs/ca_obj_tile.rb +29 -0
  232. data/yard-stubs/ca_obj_transpose.rb +40 -0
  233. data/yard-stubs/ca_obj_window.rb +49 -0
  234. data/yard-stubs/carray_access.rb +131 -0
  235. data/yard-stubs/carray_attribute.rb +246 -0
  236. data/yard-stubs/carray_broadcast.rb +37 -0
  237. data/yard-stubs/carray_cast.rb +489 -0
  238. data/yard-stubs/carray_class.rb +65 -0
  239. data/yard-stubs/carray_conversion.rb +76 -0
  240. data/yard-stubs/carray_copy.rb +79 -0
  241. data/yard-stubs/carray_core.rb +114 -0
  242. data/yard-stubs/carray_count.rb +79 -0
  243. data/yard-stubs/carray_element.rb +108 -0
  244. data/yard-stubs/carray_generate.rb +66 -0
  245. data/yard-stubs/carray_lazy.rb +23 -0
  246. data/yard-stubs/carray_loop.rb +140 -0
  247. data/yard-stubs/carray_mask.rb +259 -0
  248. data/yard-stubs/carray_math.rb +132 -0
  249. data/yard-stubs/carray_mathfunc.rb +45 -0
  250. data/yard-stubs/carray_median_percentile.rb +89 -0
  251. data/yard-stubs/carray_memory_view.rb +163 -0
  252. data/yard-stubs/carray_order.rb +312 -0
  253. data/yard-stubs/carray_random.rb +89 -0
  254. data/yard-stubs/carray_scatter.rb +106 -0
  255. data/yard-stubs/carray_slab.rb +57 -0
  256. data/yard-stubs/carray_sort.rb +163 -0
  257. data/yard-stubs/carray_test.rb +85 -0
  258. data/yard-stubs/carray_undef.rb +64 -0
  259. data/yard-stubs/carray_utils.rb +97 -0
  260. data/yard-stubs/ruby_carray.rb +193 -0
  261. metadata +220 -138
  262. data/Rakefile +0 -51
  263. data/TODO.md +0 -18
  264. data/ext/ca_iter_block.c +0 -257
  265. data/ext/ca_iter_dimension.c +0 -299
  266. data/ext/ca_iter_window.c +0 -214
  267. data/ext/ca_obj_mapping.c +0 -644
  268. data/ext/ca_obj_unbound_repeat.c +0 -529
  269. data/ext/carray_iterator.c +0 -641
  270. data/ext/carray_math.rb +0 -850
  271. data/ext/carray_numeric.c +0 -259
  272. data/ext/carray_sort_addr.c +0 -254
  273. data/ext/carray_stat.c +0 -2100
  274. data/ext/carray_stat_proc.rb +0 -1999
  275. data/ext/mkmath.rb +0 -741
  276. data/ext/ruby_ccomplex.c +0 -509
  277. data/ext/ruby_float_func.c +0 -86
  278. data/lib/carray/array.rb +0 -8
  279. data/lib/carray/autoload/autoload_base.rb +0 -19
  280. data/lib/carray/autoload/autoload_gem_cairo.rb +0 -9
  281. data/lib/carray/autoload/autoload_gem_ffi.rb +0 -9
  282. data/lib/carray/autoload/autoload_gem_gnuplot.rb +0 -2
  283. data/lib/carray/autoload/autoload_gem_io_csv.rb +0 -14
  284. data/lib/carray/autoload/autoload_gem_io_pg.rb +0 -6
  285. data/lib/carray/autoload/autoload_gem_io_sqlite3.rb +0 -12
  286. data/lib/carray/autoload/autoload_gem_narray.rb +0 -10
  287. data/lib/carray/autoload/autoload_gem_numo_narray.rb +0 -15
  288. data/lib/carray/autoload/autoload_gem_opencv.rb +0 -16
  289. data/lib/carray/autoload/autoload_gem_random.rb +0 -8
  290. data/lib/carray/autoload/autoload_gem_rmagick.rb +0 -23
  291. data/lib/carray/autoload/autoload_gem_zimg.rb +0 -3
  292. data/lib/carray/autoload/autoload_io_imagemagick.rb +0 -6
  293. data/lib/carray/autoload/autoload_math_histogram.rb +0 -5
  294. data/lib/carray/autoload/autoload_math_recurrence.rb +0 -6
  295. data/lib/carray/autoload/autoload_object_iterator.rb +0 -1
  296. data/lib/carray/autoload/autoload_object_link.rb +0 -1
  297. data/lib/carray/autoload/autoload_object_pack.rb +0 -2
  298. data/lib/carray/autoload.rb +0 -141
  299. data/lib/carray/basic.rb +0 -191
  300. data/lib/carray/broadcast.rb +0 -101
  301. data/lib/carray/compose.rb +0 -315
  302. data/lib/carray/convert.rb +0 -115
  303. data/lib/carray/info.rb +0 -110
  304. data/lib/carray/io/imagemagick.rb +0 -235
  305. data/lib/carray/mask.rb +0 -102
  306. data/lib/carray/math/histogram.rb +0 -177
  307. data/lib/carray/math/recurrence.rb +0 -93
  308. data/lib/carray/object/ca_obj_iterator.rb +0 -50
  309. data/lib/carray/object/ca_obj_link.rb +0 -50
  310. data/lib/carray/object/ca_obj_pack.rb +0 -99
  311. data/lib/carray/obsolete.rb +0 -256
  312. data/lib/carray/ordering.rb +0 -181
  313. data/lib/carray/testing.rb +0 -51
  314. data/lib/carray/transform.rb +0 -109
  315. data/mailmap +0 -1
  316. data/misc/Methods.ja.md +0 -182
  317. data/misc/NOTE +0 -51
  318. data/spec/Classes/CABitfield_spec.rb +0 -58
  319. data/spec/Classes/CABlockIterator_spec.rb +0 -114
  320. data/spec/Classes/CABlock_spec.rb +0 -205
  321. data/spec/Classes/CAField_spec.rb +0 -39
  322. data/spec/Classes/CAGrid_spec.rb +0 -75
  323. data/spec/Classes/CAMap_spec.rb +0 -0
  324. data/spec/Classes/CAMapping_spec.rb +0 -105
  325. data/spec/Classes/CAObject_attribute_spec.rb +0 -33
  326. data/spec/Classes/CAObject_spec.rb +0 -33
  327. data/spec/Classes/CARefer_spec.rb +0 -93
  328. data/spec/Classes/CARepeat_spec.rb +0 -65
  329. data/spec/Classes/CASelect_spec.rb +0 -22
  330. data/spec/Classes/CAShift_spec.rb +0 -16
  331. data/spec/Classes/CAStruct_spec.rb +0 -71
  332. data/spec/Classes/CATranspose_spec.rb +0 -60
  333. data/spec/Classes/CAUnboudRepeat_spec.rb +0 -102
  334. data/spec/Classes/CAWindow_spec.rb +0 -54
  335. data/spec/Classes/CAWrap_spec.rb +0 -8
  336. data/spec/Classes/CArray_spec.rb +0 -184
  337. data/spec/Classes/CScalar_spec.rb +0 -55
  338. data/spec/Features/feature_130_spec.rb +0 -19
  339. data/spec/Features/feature_attributes_spec.rb +0 -280
  340. data/spec/Features/feature_boolean_spec.rb +0 -98
  341. data/spec/Features/feature_broadcast.rb +0 -116
  342. data/spec/Features/feature_cast_function.rb +0 -19
  343. data/spec/Features/feature_cast_spec.rb +0 -33
  344. data/spec/Features/feature_class_spec.rb +0 -84
  345. data/spec/Features/feature_complex_spec.rb +0 -42
  346. data/spec/Features/feature_composite_spec.rb +0 -124
  347. data/spec/Features/feature_convert_spec.rb +0 -46
  348. data/spec/Features/feature_copy_spec.rb +0 -123
  349. data/spec/Features/feature_creation_spec.rb +0 -84
  350. data/spec/Features/feature_element_spec.rb +0 -144
  351. data/spec/Features/feature_extream_spec.rb +0 -54
  352. data/spec/Features/feature_generate_spec.rb +0 -74
  353. data/spec/Features/feature_index_spec.rb +0 -69
  354. data/spec/Features/feature_mask_spec.rb +0 -580
  355. data/spec/Features/feature_math_spec.rb +0 -97
  356. data/spec/Features/feature_order_spec.rb +0 -146
  357. data/spec/Features/feature_ref_store_spec.rb +0 -209
  358. data/spec/Features/feature_serialization_spec.rb +0 -125
  359. data/spec/Features/feature_stat_spec.rb +0 -397
  360. data/spec/Features/feature_virtual_spec.rb +0 -48
  361. data/spec/Features/method_eq_spec.rb +0 -81
  362. data/spec/Features/method_is_nan_spec.rb +0 -12
  363. data/spec/Features/method_map_spec.rb +0 -54
  364. data/spec/Features/method_max_with.rb +0 -20
  365. data/spec/Features/method_min_with.rb +0 -19
  366. data/spec/Features/method_ne_spec.rb +0 -18
  367. data/spec/Features/method_project_spec.rb +0 -188
  368. data/spec/Features/method_ref_spec.rb +0 -27
  369. data/spec/Features/method_round_spec.rb +0 -11
  370. data/spec/Features/method_s_linspace_spec.rb +0 -48
  371. data/spec/Features/method_s_span_spec.rb +0 -14
  372. data/spec/Features/method_seq_spec.rb +0 -47
  373. data/spec/Features/method_sort_with.rb +0 -43
  374. data/spec/Features/method_sorted_with.rb +0 -29
  375. data/spec/Features/method_span_spec.rb +0 -42
  376. data/spec/Features/method_wrap_readonly_spec.rb +0 -43
  377. data/spec/UnitTest/test_CAVirtual.rb +0 -214
  378. data/spec/spec_all.rb +0 -10
  379. data/utils/ca_ase.rb +0 -21
  380. data/utils/ca_methods.rb +0 -15
  381. data/utils/cast_checker.rb +0 -30
  382. data/utils/convert_test.rb +0 -73
  383. data/utils/extract_yard.rb +0 -22
  384. data/utils/guess_shape.rb +0 -76
  385. data/utils/monkey_patch_methods.rb +0 -62
  386. 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,321 @@
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 data type --
66
+ # each column is formatted to text independently, so mixed data types (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
+ # Render the frame as an aligned text table for reading:
119
+ #
120
+ # puts df.to_table
121
+ #
122
+ # time temp station
123
+ # ---------- ---- -------
124
+ # 2026-01-01 1.5 Tokyo
125
+ # 2026-01-02 _ Osaka
126
+ #
127
+ # This is the display counterpart of +to_csv+ and shares none of its
128
+ # constraints: it is text meant to be looked at, not read back. Numeric
129
+ # columns are right-aligned, everything else left-aligned; a masked cell
130
+ # shows as +_+, the same marker CArray's own inspect uses. An N-D column
131
+ # (which +to_csv+ rejects, having no flat cell) shows each row's slice as
132
+ # an Array literal.
133
+ #
134
+ # Float cells are rounded to +precision+ decimal places for display only
135
+ # (default 6); +precision: nil+ prints them at full precision, which is
136
+ # faithful but lets one long value set the column width.
137
+ #
138
+ # Long frames are truncated in the middle: +rows+ caps how many rows are
139
+ # printed (default 20, split evenly around an ellipsis row), and
140
+ # +rows: nil+ prints every row. +index: false+ drops the index column.
141
+ def to_table(rows: 20, index: true, precision: 6)
142
+ head = rows && (rows + 1) / 2
143
+ render_table(head: head, tail: rows && rows - head,
144
+ index: index, precision: precision, footer: true)
145
+ end
146
+
147
+ # +to_s+ is the whole frame, +inspect+ the middle-elided one -- so +puts df+
148
+ # dumps everything and +p df+ stays a screenful. +inspect+ leads with the
149
+ # same summary line it always had (nrow, variable data types, index), so the
150
+ # table under it needs no row-count footer.
151
+ def to_s
152
+ to_table(rows: nil)
153
+ end
154
+
155
+ # The summary line (nrow, variable data types, index) followed by the
156
+ # middle-elided table.
157
+ # @return [String]
158
+ def inspect
159
+ parts = @columns.map { |k, v| "#{k}:#{v.data_type}#{v.ndim > 1 ? v.shape[1..].inspect : ''}" }
160
+ idx = @index ? " index=#{@axis_name.inspect}" : ""
161
+ head = "#<CAFrame nrow=#{@nrow} vars=[#{parts.join(', ')}]#{idx}>"
162
+ return head if @columns.empty?
163
+ head + "\n" + render_table(head: 8, tail: 2, index: true, precision: 6,
164
+ footer: false)
165
+ end
166
+
167
+ private def render_table(head:, tail:, index:, precision:, footer:)
168
+ names = []
169
+ columns = []
170
+ aligns = []
171
+
172
+ if index && @index
173
+ names << @axis_name
174
+ columns << @index
175
+ aligns << (@index.numeric? ? :right : :left)
176
+ end
177
+ @columns.each do |name, col|
178
+ names << name
179
+ columns << col
180
+ aligns << (col.ndim == 1 && col.numeric? ? :right : :left)
181
+ end
182
+ return "" if names.empty?
183
+
184
+ positions = table_row_positions(head, tail)
185
+ body = positions.map do |i|
186
+ if i.nil?
187
+ Array.new(columns.size, ":") # vertical ellipsis for the elided middle
188
+ else
189
+ columns.map { |col| format_table_cell(col, i, precision) }
190
+ end
191
+ end
192
+
193
+ widths = names.each_with_index.map do |name, j|
194
+ [display_width(name), *body.map { |cells| display_width(cells[j]) }].max
195
+ end
196
+
197
+ out = +""
198
+ out << table_row(names, widths, aligns) << "\n"
199
+ out << table_row(widths.map { |w| "-" * w }, widths, aligns) << "\n"
200
+ body.each { |cells| out << table_row(cells, widths, aligns) << "\n" }
201
+ if footer && positions.size - positions.count(nil) < @nrow
202
+ out << "(#{plural(@nrow, 'row')}, #{plural(@columns.size, 'variable')})\n"
203
+ end
204
+ out
205
+ end
206
+
207
+ # Row indices to print, with nil marking the elided middle. A nil head means
208
+ # no cap; a frame that already fits in head + tail is listed whole.
209
+ private def table_row_positions(head, tail)
210
+ return (0...@nrow).to_a if head.nil? || @nrow <= head + tail
211
+ (0...head).to_a + [nil] + ((@nrow - tail)...@nrow).to_a
212
+ end
213
+
214
+ private def table_row(cells, widths, aligns)
215
+ line = cells.each_with_index.map do |text, j|
216
+ pad = " " * (widths[j] - display_width(text))
217
+ aligns[j] == :right ? pad + text : text + pad
218
+ end.join(" ")
219
+ line.rstrip
220
+ end
221
+
222
+ # Column widths are counted in terminal cells, not characters: a CJK
223
+ # ideograph, kana, or full-width form occupies two cells, so counting
224
+ # characters would leave every column holding such a name ragged. The
225
+ # ranges below are the East Asian Wide / Fullwidth blocks; a combining
226
+ # mark takes no cell of its own.
227
+ WIDE_CHAR_RANGES = [
228
+ 0x1100..0x115F, 0x2E80..0x303E, 0x3041..0x33FF, 0x3400..0x4DBF,
229
+ 0x4E00..0x9FFF, 0xA000..0xA4CF, 0xA960..0xA97F, 0xAC00..0xD7A3,
230
+ 0xF900..0xFAFF, 0xFE10..0xFE19, 0xFE30..0xFE6F, 0xFF00..0xFF60,
231
+ 0xFFE0..0xFFE6, 0x1F300..0x1F64F, 0x1F900..0x1F9FF, 0x20000..0x3FFFD,
232
+ ].freeze
233
+ private_constant :WIDE_CHAR_RANGES
234
+
235
+ COMBINING_RANGES = [0x0300..0x036F, 0x1AB0..0x1AFF, 0x20D0..0x20F0].freeze
236
+ private_constant :COMBINING_RANGES
237
+
238
+ private def display_width(text)
239
+ text.each_char.sum do |ch|
240
+ cp = ch.ord
241
+ if COMBINING_RANGES.any? { |r| r.cover?(cp) }
242
+ 0
243
+ elsif WIDE_CHAR_RANGES.any? { |r| r.cover?(cp) }
244
+ 2
245
+ else
246
+ 1
247
+ end
248
+ end
249
+ end
250
+
251
+ private def plural(n, noun)
252
+ "#{n} #{noun}#{n == 1 ? '' : 's'}"
253
+ end
254
+
255
+ private def format_table_cell(col, i, precision)
256
+ e = elem_at(col, i)
257
+ if UNDEF.equal?(e) || e.nil?
258
+ "_"
259
+ elsif e.is_a?(Float) && precision
260
+ # Display rounding only: a full-precision float (141.67833333333334)
261
+ # sets the column width for every other row and makes the table hard
262
+ # to read. precision: nil prints the value as Ruby renders it.
263
+ e.round(precision).to_s
264
+ elsif e.is_a?(CArray)
265
+ e.to_a.inspect
266
+ elsif e.is_a?(String)
267
+ e
268
+ elsif e.respond_to?(:iso8601)
269
+ e.iso8601
270
+ else
271
+ e.to_s
272
+ end
273
+ end
274
+
275
+ private def format_csv_column(col)
276
+ col.to_a.map do |e|
277
+ if UNDEF.equal?(e) || e.nil?
278
+ nil
279
+ elsif e.is_a?(String)
280
+ e
281
+ elsif e.respond_to?(:iso8601)
282
+ e.iso8601
283
+ else
284
+ e.to_s
285
+ end
286
+ end
287
+ end
288
+
289
+ private def quote_csv_field(text, sep, quote)
290
+ return "" if text.nil?
291
+ if text.empty? || text.include?(sep) || text.include?(quote) ||
292
+ text.include?("\n") || text.include?("\r")
293
+ quote + text.gsub(quote, quote * 2) + quote
294
+ else
295
+ text
296
+ end
297
+ end
298
+
299
+ def self.build_frame(names, rows)
300
+ ncol = names ? names.size : (rows.map(&:size).max || 0)
301
+ names ||= Array.new(ncol) { |j| "c#{j}" }
302
+
303
+ cols = {}
304
+ if rows.empty?
305
+ names.each { |name| cols[name] = CArray.object(0) }
306
+ return new(cols)
307
+ end
308
+ rows.each_with_index do |r, i|
309
+ if r.size < ncol
310
+ r.concat(Array.new(ncol - r.size))
311
+ elsif r.size > ncol
312
+ raise ArgumentError,
313
+ "row #{i + 1} has #{r.size} fields, expected #{ncol}"
314
+ end
315
+ end
316
+ table = CArray.object(rows.size, ncol) { rows }
317
+ names.each_with_index { |name, j| cols[name] = table[nil, j] }
318
+ new(cols)
319
+ end
320
+ private_class_method :build_frame
321
+ end