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,95 @@
1
+ # CAFrame row iteration and matrix conversion (memo §11.9, §11.11).
2
+
3
+ class CAFrame
4
+ # Iterate rows as Ruby Hashes (memo §11.11). This is an escape path — the
5
+ # primary idiom is column-vectorized work (§4.3); each_row is for touching
6
+ # heterogeneous / Face-carrying rows now and then, not hot loops. Without a
7
+ # block, returns an Enumerator.
8
+ def each_row
9
+ return enum_for(:each_row) unless block_given?
10
+ nrow.times { |i| yield row(i) }
11
+ self
12
+ end
13
+
14
+ # Export rows as an Array of plain Ruby Hashes (memo §11.11) — the inverse of
15
+ # +from_records+ and the shape +JSON.generate+ wants. Each row is one Hash of
16
+ # column name => value; a scalar cell is a Ruby value, an N-D cell is a Ruby
17
+ # Array, and a masked cell (UNDEF) is +nil+. Normalizing UNDEF -> nil and
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.
21
+ def to_records
22
+ each_row.map { |row| row.transform_values { |v| record_value(v) } }
23
+ end
24
+
25
+ # 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
27
+ # order (§12-C). Nothing is materialised — the result is a +CAStack+ over
28
+ # the stored columns, so reads gather from them and writes flow back
29
+ # (§3.6). Call +copy+ on it for an independent, owned matrix; +CArray.tabulate+
30
+ # is the eager sibling that builds one directly.
31
+ #
32
+ # 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
34
+ # dtype set is promoted to a common type (+result_type+, §12-F) through
35
+ # lazy cast lanes, so the promotion costs no buffer either.
36
+ #
37
+ # +writable: true+ demands a result whose writes reach this frame's own
38
+ # columns (the 3.0 +to_ca+ contract). That holds only when every column
39
+ # enters the stack unchanged, so it is refused when a column is read-only
40
+ # or when the common type promotes it: the promoted column is stacked
41
+ # through a cast lane, and a write there is no longer the value the caller
42
+ # handed over. Cast the frame first (+df.cast+) when the promoted matrix is
43
+ # what should be written to.
44
+ def to_ca(writable: false)
45
+ cols = @columns.values
46
+ raise ArgumentError, "frame has no columns to stack into a matrix" if cols.empty?
47
+ nd = @columns.find { |_, c| c.ndim != 1 }
48
+ if nd
49
+ raise ArgumentError,
50
+ "to_ca needs all-scalar (1-D) columns; #{nd.first.inspect} is " \
51
+ "#{nd.last.ndim}-D — escape per column with df[name]"
52
+ end
53
+ refuse_unshared_columns(cols) if writable
54
+ begin
55
+ CArray.stack(cols, axis: 1)
56
+ rescue ArgumentError, RuntimeError => e
57
+ # The columns have no common type (a text or Face column beside a
58
+ # numeric one). Point at the frame-level verb that gives them one.
59
+ raise e.class,
60
+ "#{e.message} -- df.promote(:object) brings every column to its " \
61
+ "surface values, and that frame stacks"
62
+ end
63
+ end
64
+
65
+ # The +writable: true+ half of +to_ca+: raise unless the stack's parents are
66
+ # the stored columns themselves. +promote_list+ hands back each column
67
+ # untouched when no coercion is needed, so identity is the exact test for
68
+ # "this column is shared, not re-expressed".
69
+ private def refuse_unshared_columns(cols)
70
+ ro = @columns.find { |_, c| c.read_only? }
71
+ if ro
72
+ raise "CAFrame#to_ca cannot satisfy `writable: true': column " \
73
+ "#{ro.first.inspect} is read-only"
74
+ end
75
+ promoted = CArray.promote_list(cols)
76
+ recast = @columns.keys.zip(cols, promoted).find { |_, col, p| !p.equal?(col) }
77
+ if recast
78
+ raise "CAFrame#to_ca cannot satisfy `writable: true': column " \
79
+ "#{recast[0].inspect} (#{recast[1].data_type}) is promoted to " \
80
+ "#{recast[2].data_type} for the common type, so it is stacked " \
81
+ "through a cast lane; cast the frame first (df.cast) or take the " \
82
+ "column with df[name]"
83
+ end
84
+ end
85
+
86
+ private def record_value(v)
87
+ if UNDEF.equal?(v)
88
+ nil
89
+ elsif v.is_a?(CArray)
90
+ v.to_a.map { |e| UNDEF.equal?(e) ? nil : e }
91
+ else
92
+ v
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,211 @@
1
+ # A small, self-contained CSV tokenizer for CAFrame.from_csv.
2
+ #
3
+ # It produces raw String cells and does no type inference — casting is a
4
+ # separate step. The tokenizer is tuned for the
5
+ # common shape of scientific / ETL CSV: records with no quote character take a
6
+ # single String#split fast path, and only quote-bearing records fall back to
7
+ # the field scanner. Quoted fields carry embedded separators, embedded newlines
8
+ # (multi-line records) and doubled-quote ("") escapes.
9
+ #
10
+ # Correctness choices:
11
+ # * RFC 4180 spacing by default -- spaces are significant and preserved.
12
+ # Pass +strip: true+ to trim unquoted fields (handy for numeric columns).
13
+ # * An empty unquoted field is nil (missing); an empty quoted field ("") is
14
+ # the empty String. Both cast to UNDEF for numeric columns (parse-mask).
15
+ # * A UTF-8 BOM is stripped via the "bom|utf-8" read mode by default.
16
+ # * A parse error names the record number.
17
+ #
18
+ # Two entry points share the Tokenizer:
19
+ # * CSVParser.parse / parse_file -- whole-file parse into [headers, rows].
20
+ # * CSVReader -- the block reading-control DSL used by
21
+ # CAFrame.from_csv (header / skip / body /
22
+ # column_names).
23
+
24
+ require "strscan"
25
+
26
+ class CAFrame
27
+ # CSV tokenizer behind `CAFrame.from_csv`. It produces raw String cells
28
+ # and does no type inference — casting is a separate step.
29
+ #
30
+ # Records with no quote character take a `String#split` fast path; only
31
+ # quote-bearing records fall back to the field scanner, which handles
32
+ # embedded separators, embedded newlines and doubled-quote escapes.
33
+ # Spacing follows RFC 4180 (significant and preserved) unless `strip:` is
34
+ # given. An empty unquoted field is `nil` (missing); an empty quoted field
35
+ # is the empty String.
36
+ module CSVParser
37
+ module_function
38
+
39
+ # Raised when the input cannot be tokenized, e.g. a quoted field that
40
+ # never closes before end of input.
41
+ class MalformedCSV < StandardError; end
42
+
43
+ # Parse a file into [headers, rows]. +encoding+ is an IO open-mode encoding
44
+ # string; the default strips a leading BOM and reads UTF-8.
45
+ def parse_file(path, encoding: "bom|utf-8", **opts)
46
+ File.open(path, "r:#{encoding}") { |io| parse(io, **opts) }
47
+ end
48
+
49
+ # Parse an IO (or anything answering +gets+). The first record supplies the
50
+ # headers; the rest are data rows. Fully blank lines are skipped.
51
+ def parse(io, sep: ",", quote: '"', strip: false)
52
+ tok = Tokenizer.new(sep, quote, strip)
53
+ headers = nil
54
+ rows = []
55
+ while (fields = tok.read(io))
56
+ if headers.nil?
57
+ headers = fields.map(&:to_s)
58
+ else
59
+ rows << fields
60
+ end
61
+ end
62
+ [headers, rows]
63
+ end
64
+
65
+ # Read one logical record, joining continuation lines while a quoted field
66
+ # is still open (an odd number of quote characters means unbalanced).
67
+ def read_record(io, quote)
68
+ line = io.gets
69
+ return nil if line.nil?
70
+ rec = line.dup
71
+ while rec.count(quote).odd?
72
+ more = io.gets
73
+ raise MalformedCSV, "unterminated quoted field at end of input" if more.nil?
74
+ rec << more
75
+ end
76
+ rec.chomp!
77
+ rec
78
+ end
79
+
80
+ # Splits records into fields. Regexps are compiled once and reused across
81
+ # every record, so per-row cost stays low.
82
+ class Tokenizer
83
+ def initialize(sep, quote, strip)
84
+ @sep = sep
85
+ @quote = quote
86
+ @strip = strip
87
+ @sep_re = /#{Regexp.escape(sep)}/
88
+ @quote_re = /#{Regexp.escape(quote)}/
89
+ @unquoted = /[^#{Regexp.escape(sep)}]*/
90
+ @inner = /[^#{Regexp.escape(quote)}]*/
91
+ @recno = 0
92
+ end
93
+
94
+ # Fields of the next non-blank record, or nil at EOF.
95
+ def read(io)
96
+ loop do
97
+ rec = CSVParser.read_record(io, @quote)
98
+ return nil if rec.nil?
99
+ @recno += 1
100
+ next if rec.empty?
101
+ return rec.count(@quote).zero? ? simple(rec) : scan(rec)
102
+ end
103
+ end
104
+
105
+ private def simple(rec)
106
+ rec.split(@sep, -1).map do |cell|
107
+ cell = cell.strip if @strip
108
+ cell.empty? ? nil : cell
109
+ end
110
+ end
111
+
112
+ private def scan(rec)
113
+ sc = StringScanner.new(rec)
114
+ fields = []
115
+ loop do
116
+ if sc.scan(@quote_re)
117
+ buf = +""
118
+ loop do
119
+ buf << sc.scan(@inner)
120
+ unless sc.scan(@quote_re)
121
+ raise MalformedCSV, "unterminated quoted field in record #{@recno}"
122
+ end
123
+ if sc.scan(@quote_re) # doubled quote -> literal quote, field continues
124
+ buf << @quote
125
+ else
126
+ break
127
+ end
128
+ end
129
+ fields << buf
130
+ else
131
+ cell = sc.scan(@unquoted) || ""
132
+ cell = cell.strip if @strip
133
+ fields << (cell.empty? ? nil : cell)
134
+ end
135
+ break unless sc.scan(@sep_re)
136
+ end
137
+ fields
138
+ end
139
+ end
140
+ end
141
+
142
+ # The block reading-control DSL for CAFrame.from_csv. A file
143
+ # often has preamble lines, a units row, or no header at all; the block says,
144
+ # in order, how to consume the stream:
145
+ #
146
+ # CAFrame.from_csv(path) do
147
+ # skip 3 # drop 3 preamble lines
148
+ # header # next record supplies the column names
149
+ # skip 1 # drop a units row
150
+ # body # the rest are data rows
151
+ # end
152
+ #
153
+ # CAFrame.from_csv(path) do # headerless file
154
+ # column_names "date", "temp", "rh"
155
+ # body
156
+ # end
157
+ #
158
+ # The verbs are +skip+ / +header+ / +column_names+ / +body+; each returns a
159
+ # value useful inline (header returns its fields) and the ordering is the
160
+ # script. Without a block, from_csv runs the default +header+ then +body+.
161
+ class CSVReader
162
+ def initialize(io, sep: ",", quote: '"', strip: false)
163
+ @io = io
164
+ @tok = CSVParser::Tokenizer.new(sep, quote, strip)
165
+ @names = nil
166
+ @rows = nil
167
+ end
168
+
169
+ # Drop +n+ raw lines (preamble, units, notes).
170
+ def skip(n = 1)
171
+ n.times { @io.gets }
172
+ self
173
+ end
174
+
175
+ # Read one record. With no argument it becomes the column names; with a
176
+ # name it is a secondary header (e.g. units) -- read, returned, not used as
177
+ # names. Returns the record's fields either way.
178
+ def header(name = nil)
179
+ fields = @tok.read(@io)
180
+ raise CSVParser::MalformedCSV, "header expected but input ended" if fields.nil?
181
+ if name.nil?
182
+ @names = fields.map(&:to_s)
183
+ else
184
+ (@named_headers ||= {})[name.to_s] = fields
185
+ end
186
+ fields
187
+ end
188
+
189
+ # Set the column names explicitly (headerless files).
190
+ def column_names(*names)
191
+ @names = names.flatten.map(&:to_s)
192
+ self
193
+ end
194
+
195
+ # Consume the remaining records as data rows.
196
+ def body
197
+ rows = []
198
+ while (fields = @tok.read(@io))
199
+ rows << fields
200
+ end
201
+ @rows = rows
202
+ self
203
+ end
204
+
205
+ # [names_or_nil, rows] for CAFrame.from_csv to build from. names is nil when
206
+ # neither header nor column_names ran (positional names are generated).
207
+ def result
208
+ [@names, @rows || []]
209
+ end
210
+ end
211
+ end