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.
- checksums.yaml +4 -4
- data/.yardopts +5 -25
- data/CHANGELOG.md +16 -0
- data/LICENSE +1 -1
- data/NEWS.md +3 -0
- data/README.md +128 -44
- data/carray.gemspec +22 -24
- data/ext/ca_array_pool.c +91 -0
- data/ext/ca_axis_descriptor.h +186 -0
- data/ext/ca_axis_dispatch.c +924 -0
- data/ext/ca_axis_group.c +1208 -0
- data/ext/ca_bincmp_dispatch.c +76 -0
- data/ext/ca_bincmp_dispatch.h +85 -0
- data/ext/ca_binop_dispatch.c +125 -0
- data/ext/ca_binop_dispatch.h +159 -0
- data/ext/ca_categorical_iterator.c +1375 -0
- data/ext/ca_compare.c +94 -0
- data/ext/ca_compare.h +26 -0
- data/ext/ca_composite_dispatch.c +414 -0
- data/ext/ca_composite_dispatch.h +116 -0
- data/ext/ca_for_buffer.h +96 -0
- data/ext/ca_for_each_element.h +241 -0
- data/ext/ca_group_iter.c +304 -0
- data/ext/ca_iter_substrate.h +325 -0
- data/ext/ca_kernel_iterator.c +4321 -0
- data/ext/ca_kernel_iterator.h +2603 -0
- data/ext/ca_moncmp_dispatch.c +37 -0
- data/ext/ca_moncmp_dispatch.h +62 -0
- data/ext/ca_monop_dispatch.c +200 -0
- data/ext/ca_monop_dispatch.h +235 -0
- data/ext/ca_obj_array.c +355 -359
- data/ext/ca_obj_bincmp.c +809 -0
- data/ext/ca_obj_binop.c +892 -0
- data/ext/ca_obj_bitarray.c +369 -164
- data/ext/ca_obj_bitfield.c +294 -234
- data/ext/ca_obj_block.c +189 -711
- data/ext/ca_obj_byte_swap.c +766 -0
- data/ext/ca_obj_const_string.c +965 -0
- data/ext/ca_obj_face.c +670 -0
- data/ext/ca_obj_face.h +247 -0
- data/ext/ca_obj_fake.c +228 -100
- data/ext/ca_obj_farray.c +54 -441
- data/ext/ca_obj_field.c +82 -529
- data/ext/ca_obj_fixlen_string.c +306 -0
- data/ext/ca_obj_grid.c +858 -440
- data/ext/ca_obj_meld.c +1034 -0
- data/ext/ca_obj_moncmp.c +569 -0
- data/ext/ca_obj_monop.c +1111 -0
- data/ext/ca_obj_object.c +774 -298
- data/ext/ca_obj_record.c +468 -0
- data/ext/ca_obj_reduce.c +97 -82
- data/ext/ca_obj_refer.c +569 -459
- data/ext/ca_obj_remap.c +475 -0
- data/ext/ca_obj_repeat.c +92 -477
- data/ext/ca_obj_roll.c +616 -0
- data/ext/ca_obj_select.c +344 -296
- data/ext/ca_obj_select_axis.c +1296 -0
- data/ext/ca_obj_shift.c +230 -792
- data/ext/ca_obj_source.c +78 -0
- data/ext/ca_obj_stack.c +1173 -0
- data/ext/ca_obj_stride.c +2501 -0
- data/ext/ca_obj_string.c +268 -0
- data/ext/ca_obj_tile.c +614 -0
- data/ext/ca_obj_time.c +546 -0
- data/ext/ca_obj_timedelta.c +435 -0
- data/ext/ca_obj_transpose.c +62 -516
- data/ext/ca_obj_triop.c +746 -0
- data/ext/ca_obj_unbound_repeat.c +208 -241
- data/ext/ca_obj_window.c +1131 -563
- data/ext/ca_op_byte_swap.c +175 -0
- data/ext/ca_op_ipower.c +319 -0
- data/ext/ca_op_powi.h +88 -0
- data/ext/ca_sort_kernels.h +132 -0
- data/ext/ca_sweep_engine.c +430 -0
- data/ext/ca_sweep_engine.h +157 -0
- data/ext/ca_transform_common.c +228 -0
- data/ext/ca_triop_dispatch.c +55 -0
- data/ext/ca_triop_dispatch.h +62 -0
- data/ext/carray.h +795 -402
- data/ext/carray_access.c +831 -711
- data/ext/carray_attribute.c +98 -330
- data/ext/carray_bincount.c +255 -0
- data/ext/carray_broadcast.c +283 -0
- data/ext/carray_call_cfunc.c +1360 -828
- data/ext/carray_call_cfunc.h +160 -0
- data/ext/carray_cast.c +1212 -301
- data/ext/carray_cast_func.rb +81 -40
- data/ext/carray_class.c +53 -63
- data/ext/carray_config.h +28 -0
- data/ext/carray_conversion.c +350 -346
- data/ext/carray_copy.c +156 -268
- data/ext/carray_core.c +1342 -199
- data/ext/carray_count.c +312 -0
- data/ext/carray_data_type.c +43 -19
- data/ext/carray_element.c +585 -213
- data/ext/carray_factorize.c +2542 -0
- data/ext/carray_generate.c +230 -559
- data/ext/carray_histogram.c +490 -0
- data/ext/carray_hold.c +228 -0
- data/ext/carray_index_classifier.c +1035 -0
- data/ext/carray_index_classifier.h +27 -0
- data/ext/carray_internal.h +120 -0
- data/ext/carray_kernels_bincmp.c +4445 -0
- data/ext/carray_kernels_binop.c +10979 -0
- data/ext/carray_kernels_init.c +36 -0
- data/ext/carray_kernels_map.c +3466 -0
- data/ext/carray_kernels_moncmp.c +2096 -0
- data/ext/carray_kernels_monop.c +18312 -0
- data/ext/carray_kernels_reduce_aggregate.c +25836 -0
- data/ext/carray_kernels_reduce_boolean.c +329 -0
- data/ext/carray_kernels_reduce_cumulative.c +14592 -0
- data/ext/carray_kernels_reduce_extreme.c +16947 -0
- data/ext/carray_kernels_reduce_variance.c +3909 -0
- data/ext/carray_kernels_scan.c +3692 -0
- data/ext/carray_kernels_search.c +32137 -0
- data/ext/carray_kernels_sort.c +10625 -0
- data/ext/carray_kernels_triop.c +1391 -0
- data/ext/carray_lazy.c +567 -0
- data/ext/carray_loop.c +88 -200
- data/ext/carray_mask.c +848 -154
- data/ext/carray_math_kernel.h +120 -0
- data/ext/carray_mathfunc.c +10 -241
- data/ext/carray_median_percentile.c +1257 -0
- data/ext/carray_memory_view.c +1625 -0
- data/ext/carray_operator.c +1526 -318
- data/ext/carray_order.c +664 -1394
- data/ext/carray_partition.c +416 -0
- data/ext/carray_random.c +518 -0
- data/ext/carray_scatter.c +357 -0
- data/ext/carray_slab.c +1219 -0
- data/ext/carray_slab.h +84 -0
- data/ext/carray_sort.c +829 -0
- data/ext/carray_sort_kernel.c +620 -0
- data/ext/carray_struct.c +695 -0
- data/ext/carray_test.c +343 -229
- data/ext/carray_undef.c +34 -17
- data/ext/carray_utils.c +175 -74
- data/ext/extconf.rb +216 -55
- data/ext/mk_call_cfunc.rb +480 -0
- data/ext/mkkernel.rb +8842 -0
- data/ext/ruby_carray.c +202 -101
- data/ext/version.h +4 -14
- data/ext/version.rb +5 -13
- data/lib/carray/arrow_tensor.rb +401 -0
- data/lib/carray/attribute.rb +166 -0
- data/lib/carray/autoload_carray.rb +220 -0
- data/lib/carray/autoload_method_extension.rb +44 -0
- data/lib/carray/axis_group.rb +711 -0
- data/lib/carray/basics.rb +481 -0
- data/lib/carray/bincount_nd.rb +358 -0
- data/lib/carray/block_iterator.rb +604 -0
- data/lib/carray/boolean_reduce.rb +109 -0
- data/lib/carray/categorical.rb +561 -0
- data/lib/carray/categorical_iterator.rb +1062 -0
- data/lib/carray/complex.rb +150 -0
- data/lib/carray/conditional.rb +216 -0
- data/lib/carray/const_string.rb +228 -0
- data/lib/carray/construct.rb +139 -328
- data/lib/carray/core_extensions.rb +240 -0
- data/lib/carray/data_type_extension.rb +233 -0
- data/lib/carray/fixlen_string.rb +95 -0
- data/lib/carray/frame/concat.rb +132 -0
- data/lib/carray/frame/convert.rb +95 -0
- data/lib/carray/frame/csv_parser.rb +211 -0
- data/lib/carray/frame/frame.rb +649 -0
- data/lib/carray/frame/group.rb +186 -0
- data/lib/carray/frame/io.rb +164 -0
- data/lib/carray/frame/join.rb +248 -0
- data/lib/carray/frame/records.rb +99 -0
- data/lib/carray/frame/sort.rb +113 -0
- data/lib/carray/frame/verbs.rb +299 -0
- data/lib/carray/frame.rb +16 -0
- data/lib/carray/histogram.rb +512 -0
- data/lib/carray/inspect.rb +37 -20
- data/lib/carray/iterator.rb +57 -349
- data/lib/carray/lazy.rb +889 -0
- data/lib/carray/mask_gap_fill.rb +200 -0
- data/lib/carray/math.rb +78 -342
- data/lib/carray/meld_reduce.rb +289 -0
- data/lib/carray/methods/align_addr.rb +116 -0
- data/lib/carray/methods/bin.rb +128 -0
- data/lib/carray/methods/bincount.rb +87 -0
- data/lib/carray/methods/bit_string.rb +92 -0
- data/lib/carray/methods/broadcast.rb +63 -0
- data/lib/carray/methods/choose.rb +39 -0
- data/lib/carray/methods/composition.rb +280 -0
- data/lib/carray/methods/gather_nd.rb +206 -0
- data/lib/carray/methods/index.rb +39 -0
- data/lib/carray/methods/insert_block.rb +99 -0
- data/lib/carray/methods/is_in.rb +141 -0
- data/lib/carray/methods/join.rb +90 -0
- data/lib/carray/methods/locate_addr.rb +47 -0
- data/lib/carray/methods/mask_duplicates.rb +41 -0
- data/lib/carray/methods/meshgrid.rb +91 -0
- data/lib/carray/methods/mode.rb +126 -0
- data/lib/carray/methods/nunique.rb +46 -0
- data/lib/carray/methods/resize.rb +56 -0
- data/lib/carray/methods/snap.rb +156 -0
- data/lib/carray/methods/string_format.rb +57 -0
- data/lib/carray/methods/unique.rb +47 -0
- data/lib/carray/methods/value_counts.rb +71 -0
- data/lib/carray/mkmf.rb +124 -101
- data/lib/carray/runtime.rb +108 -0
- data/lib/carray/serialize.rb +478 -167
- data/lib/carray/slab_iterator.rb +292 -0
- data/lib/carray/stack.rb +291 -0
- data/lib/carray/string.rb +56 -180
- data/lib/carray/string_operation_extension.rb +289 -0
- data/lib/carray/struct.rb +335 -323
- data/lib/carray/struct_builder.rb +697 -0
- data/lib/carray/table.rb +41 -2
- data/lib/carray/time.rb +2255 -38
- data/lib/carray/window_iterator.rb +655 -0
- data/lib/carray.rb +55 -57
- metadata +163 -130
- data/Rakefile +0 -51
- data/TODO.md +0 -18
- data/ext/ca_iter_block.c +0 -257
- data/ext/ca_iter_dimension.c +0 -299
- data/ext/ca_iter_window.c +0 -214
- data/ext/ca_obj_mapping.c +0 -644
- data/ext/carray_iterator.c +0 -641
- data/ext/carray_math.rb +0 -850
- data/ext/carray_numeric.c +0 -259
- data/ext/carray_sort_addr.c +0 -254
- data/ext/carray_stat.c +0 -2100
- data/ext/carray_stat_proc.rb +0 -1999
- data/ext/mkmath.rb +0 -741
- data/ext/ruby_ccomplex.c +0 -509
- data/ext/ruby_float_func.c +0 -86
- data/lib/carray/array.rb +0 -8
- data/lib/carray/autoload/autoload_base.rb +0 -19
- data/lib/carray/autoload/autoload_gem_cairo.rb +0 -9
- data/lib/carray/autoload/autoload_gem_ffi.rb +0 -9
- data/lib/carray/autoload/autoload_gem_gnuplot.rb +0 -2
- data/lib/carray/autoload/autoload_gem_io_csv.rb +0 -14
- data/lib/carray/autoload/autoload_gem_io_pg.rb +0 -6
- data/lib/carray/autoload/autoload_gem_io_sqlite3.rb +0 -12
- data/lib/carray/autoload/autoload_gem_narray.rb +0 -10
- data/lib/carray/autoload/autoload_gem_numo_narray.rb +0 -15
- data/lib/carray/autoload/autoload_gem_opencv.rb +0 -16
- data/lib/carray/autoload/autoload_gem_random.rb +0 -8
- data/lib/carray/autoload/autoload_gem_rmagick.rb +0 -23
- data/lib/carray/autoload/autoload_gem_zimg.rb +0 -3
- data/lib/carray/autoload/autoload_io_imagemagick.rb +0 -6
- data/lib/carray/autoload/autoload_math_histogram.rb +0 -5
- data/lib/carray/autoload/autoload_math_recurrence.rb +0 -6
- data/lib/carray/autoload/autoload_object_iterator.rb +0 -1
- data/lib/carray/autoload/autoload_object_link.rb +0 -1
- data/lib/carray/autoload/autoload_object_pack.rb +0 -2
- data/lib/carray/autoload.rb +0 -141
- data/lib/carray/basic.rb +0 -191
- data/lib/carray/broadcast.rb +0 -101
- data/lib/carray/compose.rb +0 -315
- data/lib/carray/convert.rb +0 -115
- data/lib/carray/info.rb +0 -110
- data/lib/carray/io/imagemagick.rb +0 -235
- data/lib/carray/mask.rb +0 -102
- data/lib/carray/math/histogram.rb +0 -177
- data/lib/carray/math/recurrence.rb +0 -93
- data/lib/carray/object/ca_obj_iterator.rb +0 -50
- data/lib/carray/object/ca_obj_link.rb +0 -50
- data/lib/carray/object/ca_obj_pack.rb +0 -99
- data/lib/carray/obsolete.rb +0 -256
- data/lib/carray/ordering.rb +0 -181
- data/lib/carray/testing.rb +0 -51
- data/lib/carray/transform.rb +0 -109
- data/misc/Methods.ja.md +0 -182
- data/misc/NOTE +0 -51
- data/spec/Classes/CABitfield_spec.rb +0 -58
- data/spec/Classes/CABlockIterator_spec.rb +0 -114
- data/spec/Classes/CABlock_spec.rb +0 -205
- data/spec/Classes/CAField_spec.rb +0 -39
- data/spec/Classes/CAGrid_spec.rb +0 -75
- data/spec/Classes/CAMap_spec.rb +0 -0
- data/spec/Classes/CAMapping_spec.rb +0 -105
- data/spec/Classes/CAObject_attribute_spec.rb +0 -33
- data/spec/Classes/CAObject_spec.rb +0 -33
- data/spec/Classes/CARefer_spec.rb +0 -93
- data/spec/Classes/CARepeat_spec.rb +0 -65
- data/spec/Classes/CASelect_spec.rb +0 -22
- data/spec/Classes/CAShift_spec.rb +0 -16
- data/spec/Classes/CAStruct_spec.rb +0 -71
- data/spec/Classes/CATranspose_spec.rb +0 -60
- data/spec/Classes/CAUnboudRepeat_spec.rb +0 -102
- data/spec/Classes/CAWindow_spec.rb +0 -54
- data/spec/Classes/CAWrap_spec.rb +0 -8
- data/spec/Classes/CArray_spec.rb +0 -184
- data/spec/Classes/CScalar_spec.rb +0 -55
- data/spec/Classes/ex1.rb +0 -46
- data/spec/Features/feature_130_spec.rb +0 -19
- data/spec/Features/feature_attributes_spec.rb +0 -280
- data/spec/Features/feature_boolean_spec.rb +0 -98
- data/spec/Features/feature_broadcast.rb +0 -116
- data/spec/Features/feature_cast_function.rb +0 -19
- data/spec/Features/feature_cast_spec.rb +0 -33
- data/spec/Features/feature_class_spec.rb +0 -84
- data/spec/Features/feature_complex_spec.rb +0 -42
- data/spec/Features/feature_composite_spec.rb +0 -124
- data/spec/Features/feature_convert_spec.rb +0 -46
- data/spec/Features/feature_copy_spec.rb +0 -123
- data/spec/Features/feature_creation_spec.rb +0 -84
- data/spec/Features/feature_element_spec.rb +0 -144
- data/spec/Features/feature_extream_spec.rb +0 -54
- data/spec/Features/feature_generate_spec.rb +0 -74
- data/spec/Features/feature_index_spec.rb +0 -69
- data/spec/Features/feature_mask_spec.rb +0 -580
- data/spec/Features/feature_math_spec.rb +0 -97
- data/spec/Features/feature_order_spec.rb +0 -146
- data/spec/Features/feature_ref_store_spec.rb +0 -209
- data/spec/Features/feature_serialization_spec.rb +0 -125
- data/spec/Features/feature_stat_spec.rb +0 -397
- data/spec/Features/feature_virtual_spec.rb +0 -48
- data/spec/Features/method_eq_spec.rb +0 -81
- data/spec/Features/method_is_nan_spec.rb +0 -12
- data/spec/Features/method_map_spec.rb +0 -54
- data/spec/Features/method_max_with.rb +0 -20
- data/spec/Features/method_min_with.rb +0 -19
- data/spec/Features/method_ne_spec.rb +0 -18
- data/spec/Features/method_project_spec.rb +0 -188
- data/spec/Features/method_ref_spec.rb +0 -27
- data/spec/Features/method_round_spec.rb +0 -11
- data/spec/Features/method_s_linspace_spec.rb +0 -48
- data/spec/Features/method_s_span_spec.rb +0 -14
- data/spec/Features/method_seq_spec.rb +0 -47
- data/spec/Features/method_sort_with.rb +0 -43
- data/spec/Features/method_sorted_with.rb +0 -29
- data/spec/Features/method_span_spec.rb +0 -42
- data/spec/Features/method_wrap_readonly_spec.rb +0 -43
- data/spec/UnitTest/test_CAVirtual.rb +0 -214
- data/spec/spec_all.rb +0 -10
- data/utils/ca_ase.rb +0 -21
- data/utils/ca_methods.rb +0 -15
- data/utils/cast_checker.rb +0 -30
- data/utils/convert_test.rb +0 -73
- data/utils/extract_yard.rb +0 -22
- data/utils/guess_shape.rb +0 -76
- data/utils/monkey_patch_methods.rb +0 -62
- 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
|