carray 3.0.1 → 3.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +824 -0
- data/README.md +5 -6
- data/carray.gemspec +1 -1
- data/ext/ca_axis_dispatch.c +258 -4
- data/ext/ca_axis_group.c +368 -184
- data/ext/ca_binop_dispatch.c +54 -32
- data/ext/ca_binop_dispatch.h +1 -0
- data/ext/ca_categorical_iterator.c +90 -41
- data/ext/ca_compare.c +8 -2
- data/ext/ca_for_buffer.h +21 -68
- data/ext/ca_group_iter.c +27 -11
- data/ext/ca_kernel_iterator.c +960 -245
- data/ext/ca_kernel_iterator.h +352 -181
- data/ext/ca_monop_dispatch.c +4 -4
- data/ext/ca_monop_dispatch.h +4 -0
- data/ext/ca_obj_array.c +194 -42
- data/ext/ca_obj_bincmp.c +152 -121
- data/ext/ca_obj_binop.c +107 -210
- data/ext/ca_obj_bitarray.c +37 -24
- data/ext/ca_obj_bitfield.c +95 -62
- data/ext/ca_obj_block.c +5 -4
- data/ext/ca_obj_byte_swap.c +40 -40
- data/ext/ca_obj_const_string.c +180 -50
- data/ext/ca_obj_face.c +428 -62
- data/ext/ca_obj_face.h +107 -47
- data/ext/ca_obj_fake.c +21 -50
- data/ext/ca_obj_farray.c +1 -0
- data/ext/ca_obj_field.c +16 -3
- data/ext/ca_obj_fixlen_string.c +39 -26
- data/ext/ca_obj_grid.c +181 -124
- data/ext/ca_obj_meld.c +235 -55
- data/ext/ca_obj_moncmp.c +61 -61
- data/ext/ca_obj_monop.c +125 -107
- data/ext/ca_obj_object.c +551 -233
- data/ext/ca_obj_record.c +51 -16
- data/ext/ca_obj_reduce.c +5 -8
- data/ext/ca_obj_refer.c +117 -33
- data/ext/ca_obj_remap.c +57 -17
- data/ext/ca_obj_repeat.c +21 -5
- data/ext/ca_obj_roll.c +115 -11
- data/ext/ca_obj_select.c +88 -75
- data/ext/ca_obj_select_axis.c +58 -60
- data/ext/ca_obj_shift.c +22 -102
- data/ext/ca_obj_stack.c +223 -123
- data/ext/ca_obj_stride.c +267 -35
- data/ext/ca_obj_string.c +25 -22
- data/ext/ca_obj_tile.c +32 -16
- data/ext/ca_obj_time.c +23 -25
- data/ext/ca_obj_timedelta.c +21 -23
- data/ext/ca_obj_transpose.c +12 -3
- data/ext/ca_obj_triop.c +110 -117
- data/ext/ca_obj_window.c +157 -29
- data/ext/ca_op_ipower.c +25 -13
- data/ext/ca_op_powi.h +57 -30
- data/ext/ca_pow5_table.h +656 -0
- data/ext/ca_rng_normal.h +42 -0
- data/ext/ca_rng_xoshiro256pp.h +105 -0
- data/ext/ca_sort_kernels.h +70 -0
- data/ext/ca_sweep_engine.c +385 -198
- data/ext/ca_sweep_engine.h +31 -23
- data/ext/ca_triop_dispatch.c +4 -1
- data/ext/ca_triop_dispatch.h +5 -2
- data/ext/caframe_csv_reader.c +463 -0
- data/ext/carray.h +90 -28
- data/ext/carray_access.c +487 -231
- data/ext/carray_address_basis.c +731 -0
- data/ext/carray_attribute.c +45 -3
- data/ext/carray_bincount.c +32 -2
- data/ext/carray_broadcast.c +49 -17
- data/ext/carray_call_cfunc.c +667 -511
- data/ext/carray_cast.c +1556 -247
- data/ext/carray_cast_func.rb +34 -21
- data/ext/carray_conversion.c +197 -73
- data/ext/carray_copy.c +84 -38
- data/ext/carray_core.c +591 -119
- data/ext/carray_count.c +148 -96
- data/ext/carray_element.c +18 -27
- data/ext/carray_factorize.c +604 -291
- data/ext/carray_generate.c +76 -40
- data/ext/carray_histogram.c +152 -94
- data/ext/carray_hold.c +2 -1
- data/ext/carray_index_classifier.c +63 -96
- data/ext/carray_internal.h +400 -1
- data/ext/carray_lazy.c +834 -79
- data/ext/carray_loop.c +88 -66
- data/ext/carray_mask.c +705 -324
- data/ext/carray_math_kernel.h +1 -0
- data/ext/carray_median_percentile.c +263 -63
- data/ext/carray_memory_view.c +254 -248
- data/ext/carray_operator.c +1093 -363
- data/ext/carray_order.c +133 -69
- data/ext/carray_partition.c +102 -56
- data/ext/carray_random.c +582 -204
- data/ext/carray_scatter.c +308 -65
- data/ext/carray_segment.c +257 -0
- data/ext/carray_slab.c +183 -70
- data/ext/carray_sort.c +235 -240
- data/ext/carray_sort_kernel.c +8 -1
- data/ext/carray_struct.c +99 -21
- data/ext/carray_test.c +167 -64
- data/ext/carray_time_text.c +1182 -0
- data/ext/carray_utils.c +363 -98
- data/ext/extconf.rb +27 -2
- data/ext/mk_call_cfunc.rb +103 -118
- data/ext/mk_pow5_table.rb +35 -0
- data/ext/mkkernel.rb +1349 -851
- data/ext/ruby_carray.c +17 -2
- data/ext/version.h +4 -4
- data/ext/version.rb +4 -2
- data/lib/carray/attribute.rb +39 -0
- data/lib/carray/autoload_carray.rb +24 -3
- data/lib/carray/autoload_method_extension.rb +18 -0
- data/lib/carray/axis_group.rb +245 -20
- data/lib/carray/basics.rb +95 -25
- data/lib/carray/bincount_nd.rb +6 -0
- data/lib/carray/block_iterator.rb +200 -54
- data/lib/carray/boolean_reduce.rb +21 -29
- data/lib/carray/categorical.rb +244 -59
- data/lib/carray/categorical_iterator.rb +391 -757
- data/lib/carray/complex.rb +36 -29
- data/lib/carray/conditional.rb +94 -7
- data/lib/carray/const_string.rb +177 -27
- data/lib/carray/construct.rb +40 -0
- data/lib/carray/data_type_extension.rb +3 -0
- data/lib/carray/data_type_limits.rb +91 -0
- data/lib/carray/fixlen_string.rb +1 -1
- data/lib/carray/frame/concat.rb +157 -6
- data/lib/carray/frame/convert.rb +13 -7
- data/lib/carray/frame/csv_parser.rb +590 -76
- data/lib/carray/frame/describe.rb +79 -0
- data/lib/carray/frame/frame.rb +205 -35
- data/lib/carray/frame/group.rb +199 -40
- data/lib/carray/frame/io.rb +452 -60
- data/lib/carray/frame/join.rb +134 -46
- data/lib/carray/frame/records.rb +54 -19
- data/lib/carray/frame/reshape.rb +448 -0
- data/lib/carray/frame/sort.rb +26 -2
- data/lib/carray/frame/verbs.rb +726 -56
- data/lib/carray/frame.rb +2 -0
- data/lib/carray/fuse_source.rb +111 -2
- data/lib/carray/fusion.rb +229 -38
- data/lib/carray/inspect.rb +71 -20
- data/lib/carray/iterator.rb +157 -2
- data/lib/carray/lazy.rb +54 -67
- data/lib/carray/mask_gap_fill.rb +66 -50
- data/lib/carray/math.rb +32 -19
- data/lib/carray/meld_reduce.rb +40 -7
- data/lib/carray/methods/align_addr.rb +8 -4
- data/lib/carray/methods/bincount.rb +2 -2
- data/lib/carray/methods/bit_string.rb +14 -3
- data/lib/carray/methods/composition.rb +16 -10
- data/lib/carray/methods/discovery_along.rb +74 -0
- data/lib/carray/methods/factorize.rb +50 -0
- data/lib/carray/methods/gather_nd.rb +13 -2
- data/lib/carray/methods/insert_block.rb +9 -14
- data/lib/carray/methods/is_in.rb +130 -6
- data/lib/carray/methods/join.rb +1 -1
- data/lib/carray/methods/locate_addr.rb +235 -21
- data/lib/carray/methods/mask_duplicates.rb +35 -1
- data/lib/carray/methods/mode.rb +18 -1
- data/lib/carray/methods/nunique.rb +22 -1
- data/lib/carray/methods/repeat.rb +91 -0
- data/lib/carray/methods/resize.rb +21 -23
- data/lib/carray/methods/snap.rb +34 -12
- data/lib/carray/methods/unique.rb +52 -3
- data/lib/carray/record_order.rb +208 -0
- data/lib/carray/rng.rb +86 -0
- data/lib/carray/runtime.rb +14 -11
- data/lib/carray/segment_iterator.rb +990 -0
- data/lib/carray/serialize.rb +197 -27
- data/lib/carray/slab_iterator.rb +109 -40
- data/lib/carray/stack.rb +10 -4
- data/lib/carray/string_operation_extension.rb +9 -4
- data/lib/carray/struct.rb +142 -51
- data/lib/carray/struct_builder.rb +110 -14
- data/lib/carray/table.rb +2 -2
- data/lib/carray/time.rb +535 -105
- data/lib/carray/window_iterator.rb +259 -82
- data/lib/carray.rb +4 -2
- data/yard-stubs/ca_obj_array.rb +4 -4
- data/yard-stubs/ca_obj_bitarray.rb +7 -0
- data/yard-stubs/ca_obj_bitfield.rb +23 -12
- data/yard-stubs/ca_obj_block.rb +2 -7
- data/yard-stubs/ca_obj_fake.rb +1 -1
- data/yard-stubs/ca_obj_field.rb +1 -1
- data/yard-stubs/ca_obj_refer.rb +3 -3
- data/yard-stubs/ca_obj_roll.rb +0 -1
- data/yard-stubs/ca_obj_stride.rb +10 -7
- data/yard-stubs/ca_obj_transpose.rb +1 -1
- data/yard-stubs/ca_obj_window.rb +11 -4
- data/yard-stubs/carray_access.rb +6 -5
- data/yard-stubs/carray_broadcast.rb +2 -2
- data/yard-stubs/carray_cast.rb +78 -43
- data/yard-stubs/carray_conversion.rb +13 -1
- data/yard-stubs/carray_copy.rb +8 -3
- data/yard-stubs/carray_core.rb +3 -83
- data/yard-stubs/carray_count.rb +16 -2
- data/yard-stubs/carray_lazy.rb +210 -1
- data/yard-stubs/carray_loop.rb +4 -1
- data/yard-stubs/carray_mask.rb +34 -10
- data/yard-stubs/carray_math.rb +1499 -4
- data/yard-stubs/carray_median_percentile.rb +26 -8
- data/yard-stubs/carray_order.rb +43 -82
- data/yard-stubs/carray_scatter.rb +10 -3
- data/yard-stubs/carray_segment.rb +72 -0
- data/yard-stubs/carray_slab.rb +11 -8
- data/yard-stubs/carray_sort.rb +50 -8
- data/yard-stubs/carray_test.rb +10 -0
- data/yard-stubs/carray_utils.rb +0 -26
- metadata +19 -19
- data/ext/ca_for_each_element.h +0 -239
- data/ext/carray_build_flags.h +0 -3
- data/ext/carray_config.h +0 -28
- data/ext/carray_kernels_bincmp.c +0 -4446
- data/ext/carray_kernels_binop.c +0 -11001
- data/ext/carray_kernels_init.c +0 -1131
- data/ext/carray_kernels_map.c +0 -3467
- data/ext/carray_kernels_moncmp.c +0 -2097
- data/ext/carray_kernels_monop.c +0 -18313
- data/ext/carray_kernels_reduce_aggregate.c +0 -25837
- data/ext/carray_kernels_reduce_boolean.c +0 -330
- data/ext/carray_kernels_reduce_cumulative.c +0 -14593
- data/ext/carray_kernels_reduce_extreme.c +0 -16948
- data/ext/carray_kernels_reduce_variance.c +0 -3910
- data/ext/carray_kernels_scan.c +0 -3693
- data/ext/carray_kernels_search.c +0 -32138
- data/ext/carray_kernels_sort.c +0 -10626
- data/ext/carray_kernels_triop.c +0 -1392
data/lib/carray/frame/io.rb
CHANGED
|
@@ -4,62 +4,244 @@ require "carray/frame/csv_parser"
|
|
|
4
4
|
|
|
5
5
|
class CAFrame
|
|
6
6
|
# Read a CSV into a frame. The header row supplies column names (Strings,
|
|
7
|
-
# §3.7); every column is built raw as
|
|
8
|
-
# (§4.2
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# (parse-mask, §6-2).
|
|
7
|
+
# §3.7); every column is built raw as a CAString of the cell strings, all
|
|
8
|
+
# over one object array (§4.2). Casting is a separate step: +types:+ takes
|
|
9
|
+
# anything +cast+ takes as a map -- { "temp" => :float64 }, :infer, or a
|
|
10
|
+
# map with +default:+ -- and casts the frame with it on load, which is the
|
|
11
|
+
# same as calling +cast+ afterwards. Broken cells become UNDEF
|
|
12
|
+
# automatically (parse-mask, §6-2).
|
|
13
|
+
#
|
|
14
|
+
# CAFrame.from_csv("obs.csv", types: :infer)
|
|
15
|
+
# CAFrame.from_csv("obs.csv", types: { default: :infer, "code" => :int32, "id" => nil })
|
|
16
|
+
# CAFrame.from_csv("obs.csv", types: { default: :float64, "station" => nil })
|
|
17
|
+
#
|
|
18
|
+
# +source+ is a path, or an open IO -- anything answering +gets+, which a
|
|
19
|
+
# StringIO is. So CSV that is already in memory does not have to go to a
|
|
20
|
+
# temporary file first:
|
|
21
|
+
#
|
|
22
|
+
# CAFrame.from_csv("obs.csv") # a path
|
|
23
|
+
# CAFrame.from_csv(StringIO.new(body)) # text already in hand
|
|
24
|
+
# File.open("obs.csv") { |io| CAFrame.from_csv(io) }
|
|
25
|
+
#
|
|
26
|
+
# A String is always read as a path, never as CSV text: guessing between the
|
|
27
|
+
# two by looking for a newline is the kind of guess that is right until it is
|
|
28
|
+
# not, and StringIO says which one you meant in six characters. An IO is read
|
|
29
|
+
# from where it is and left open -- the caller opened it and closes it.
|
|
13
30
|
#
|
|
14
31
|
# Parsing uses the built-in fast tokenizer (CSVParser). Options:
|
|
15
32
|
# sep: field separator (default ",")
|
|
16
33
|
# quote: quote character (default '"')
|
|
17
34
|
# strip: trim spaces from unquoted fields (default false, RFC spacing)
|
|
18
|
-
# +encoding+:
|
|
19
|
-
#
|
|
35
|
+
# +encoding+: open-mode encoding for a path (default "bom|utf-8", strips a
|
|
36
|
+
# BOM). It has nothing to open when +source+ is an IO, so there
|
|
37
|
+
# the IO's own encoding governs and a BOM is the caller's. To
|
|
38
|
+
# read a file in another encoding, name it and the one to
|
|
39
|
+
# transcode to: "CP932:UTF-8" for a CSV written by Excel in
|
|
40
|
+
# Japanese.
|
|
41
|
+
# parser: a callable source -> [headers, rows] to inject another parser
|
|
20
42
|
# (e.g. the stdlib +csv+, or a typed-table source); when given,
|
|
21
43
|
# sep/quote/strip/encoding and any block are that parser's concern.
|
|
44
|
+
# on_error: what a +types:+ cast does with a cell that does not read
|
|
45
|
+
# (:mask default, :warn, :raise); see +cast+.
|
|
46
|
+
#
|
|
47
|
+
# Where the header and the data are is given by the index of the line,
|
|
48
|
+
# from 0, as in File.readlines (an error names the line from 1, as an
|
|
49
|
+
# editor numbers it):
|
|
50
|
+
# header: the line of the column names (default 0), or false for none
|
|
51
|
+
# data: the first line of the data (default the line after the
|
|
52
|
+
# header), or a Range of lines; a record that starts in it is
|
|
53
|
+
# read whole
|
|
54
|
+
# column_names: the names, for a file with none or to replace them; the
|
|
55
|
+
# header then defaults to none
|
|
56
|
+
# columns: the columns to read, by name or by index from 0 (Integers
|
|
57
|
+
# and Ranges), in the order the frame has them; the others are
|
|
58
|
+
# read past without a String made for their fields
|
|
59
|
+
#
|
|
60
|
+
# CAFrame.from_csv("obs.csv", header: 2) # a title above
|
|
61
|
+
# CAFrame.from_csv("obs.csv", header: 0, data: 3) # units on lines 1-2
|
|
62
|
+
# CAFrame.from_csv("big.csv", data: 1..100) # the first 100 lines
|
|
63
|
+
# CAFrame.from_csv("raw.csv", column_names: %w[date temp rh])
|
|
64
|
+
# CAFrame.from_csv("wide.csv", columns: %w[date temp])
|
|
65
|
+
#
|
|
66
|
+
# A block, given the reader, reads in any other order with +skip+ /
|
|
67
|
+
# +header+ / +header(name)+ / +column_names+ / +columns+ / +data+ (see
|
|
68
|
+
# CSVReader):
|
|
22
69
|
#
|
|
23
|
-
#
|
|
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+.
|
|
70
|
+
# CAFrame.from_csv("obs.csv") { |r| r.skip 2; r.header; r.skip 1; r.data }
|
|
26
71
|
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
72
|
+
# A missing field -- an unquoted empty one, or a cell a short row never
|
|
73
|
+
# reached -- is UNDEF in the frame, whether or not the column is cast by
|
|
74
|
+
# +types:+. A quoted empty field ("") is the empty string, which is a value.
|
|
75
|
+
# So the mask +to_csv+ writes comes back as a mask.
|
|
30
76
|
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
|
|
77
|
+
# Files that spell "missing" some other way say so with +missing:+. A field
|
|
78
|
+
# whose text is one of the given tokens is UNDEF too, before +types:+
|
|
79
|
+
# casts, so a -999 sentinel never reaches the column as a number:
|
|
80
|
+
#
|
|
81
|
+
# CAFrame.from_csv("obs.csv", missing: ["-999", "///"]) # every column
|
|
82
|
+
# CAFrame.from_csv("obs.csv", missing: { "temp" => "-999" }) # one column
|
|
83
|
+
# CAFrame.from_csv("obs.csv", missing: { default: "-999", "id" => [] })
|
|
84
|
+
#
|
|
85
|
+
# Tokens are Strings and are compared with the field's text as read (after
|
|
86
|
+
# +strip:+, quoted or not), so "-999" does not match "-999.0". In a Hash,
|
|
87
|
+
# +:default+ gives the tokens for every column and a column name gives that
|
|
88
|
+
# column's own, which replace the default ([] or "" for none); without
|
|
89
|
+
# +:default+ the columns it does not name keep only the empty field. As in
|
|
90
|
+
# +to_csv+, "" means the empty field, so a quoted "" is still a value.
|
|
91
|
+
#
|
|
92
|
+
# Without +types:+, the columns are handed to the frame as CABlock views
|
|
93
|
+
# over one backing object array (§3.6 view-by-default). With +types:+, the
|
|
94
|
+
# body is read without making a String per cell: a column cast to a number
|
|
95
|
+
# is read from the text directly, and a column left as text becomes its own
|
|
96
|
+
# CAString. Casting a column materializes it, and +copy+ gives an
|
|
97
|
+
# independent frame.
|
|
98
|
+
def self.from_csv(source, types: nil, on_error: :mask, missing: nil,
|
|
99
|
+
header: nil, data: nil, column_names: nil, columns: nil,
|
|
35
100
|
sep: ",", quote: '"', strip: false,
|
|
36
101
|
encoding: "bom|utf-8", parser: nil, &block)
|
|
102
|
+
layout = { header: header, data: data, column_names: column_names,
|
|
103
|
+
columns: columns }.compact
|
|
104
|
+
if block && !layout.empty?
|
|
105
|
+
raise ArgumentError, "from_csv: give #{layout.keys.map { |k| "#{k}:" }.join(', ')} " \
|
|
106
|
+
"or a reading block, not both"
|
|
107
|
+
end
|
|
108
|
+
if block && block.arity != 1
|
|
109
|
+
raise ArgumentError, "from_csv: the reading block takes the reader as its " \
|
|
110
|
+
"parameter, as in from_csv(path) { |r| r.skip 2; r.header; r.data }"
|
|
111
|
+
end
|
|
112
|
+
missing = missing_tokens(missing) if missing
|
|
113
|
+
# With types:, the body is read into CAConstString columns, so a column
|
|
114
|
+
# cast to a number never becomes Strings; the text columns left over
|
|
115
|
+
# become CAString below. Without types: every column stays text, so the
|
|
116
|
+
# body goes straight to Strings. missing: masks text in place, which
|
|
117
|
+
# wants the Strings first, so it takes that path too.
|
|
118
|
+
read_text_as_const_string = !types.nil? && missing.nil?
|
|
37
119
|
names, rows =
|
|
38
120
|
if parser
|
|
39
|
-
parser.call(
|
|
121
|
+
parser.call(source)
|
|
122
|
+
elsif source.respond_to?(:gets)
|
|
123
|
+
with_encoding_hint("open the IO in the file's encoding, as in " \
|
|
124
|
+
"File.open(path, \"r:<file encoding>:UTF-8\")") do
|
|
125
|
+
read_csv(source, sep: sep, quote: quote, strip: strip, layout: layout,
|
|
126
|
+
read_text_as_const_string: read_text_as_const_string, &block)
|
|
127
|
+
end
|
|
40
128
|
else
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
reader.body
|
|
129
|
+
with_encoding_hint("it was opened as #{encoding.inspect}; pass the " \
|
|
130
|
+
"file's encoding, as in " \
|
|
131
|
+
"encoding: \"<file encoding>:UTF-8\"") do
|
|
132
|
+
File.open(source, "r:#{encoding}") do |io|
|
|
133
|
+
read_csv(io, sep: sep, quote: quote, strip: strip, layout: layout,
|
|
134
|
+
read_text_as_const_string: read_text_as_const_string, &block)
|
|
48
135
|
end
|
|
49
|
-
reader.result
|
|
50
136
|
end
|
|
51
137
|
end
|
|
52
138
|
|
|
53
139
|
frame = build_frame(names, rows)
|
|
54
|
-
frame
|
|
140
|
+
mask_missing_tokens(frame, missing) if missing
|
|
141
|
+
frame.cast(types, on_error: on_error, option_name: :types) unless types.nil?
|
|
142
|
+
convert_const_string_columns_to_castring(frame)
|
|
55
143
|
frame
|
|
56
144
|
end
|
|
57
145
|
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
146
|
+
# A text column from_csv reads is a CAString, whichever way the body was
|
|
147
|
+
# read: the string operations (strip!, gsub!, ...) write through to it.
|
|
148
|
+
def self.convert_const_string_columns_to_castring(frame)
|
|
149
|
+
frame.column_names.each do |name|
|
|
150
|
+
col = frame[name]
|
|
151
|
+
next unless col.is_a?(CAConstString)
|
|
152
|
+
frame[name] = col.to_string
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
private_class_method :convert_const_string_columns_to_castring
|
|
156
|
+
|
|
157
|
+
# Split a +missing:+ argument, as from_csv and to_csv both take it, into the
|
|
158
|
+
# setting for every column and a Hash of per-column settings that replace
|
|
159
|
+
# it. A Hash says :default for every column and a column name for one;
|
|
160
|
+
# column names are Strings, so the Symbol cannot collide with one. A
|
|
161
|
+
# setting that is not a Hash is the default.
|
|
162
|
+
def self.split_missing(missing, verb)
|
|
163
|
+
return [missing, {}] unless missing.is_a?(Hash)
|
|
164
|
+
default = nil
|
|
165
|
+
per_column = {}
|
|
166
|
+
missing.each do |key, value|
|
|
167
|
+
case key
|
|
168
|
+
when :default then default = value
|
|
169
|
+
when String then per_column[key] = value
|
|
170
|
+
else
|
|
171
|
+
raise ArgumentError,
|
|
172
|
+
"#{verb}: missing: takes column names (Strings) and :default " \
|
|
173
|
+
"as keys (got #{key.inspect})"
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
[default, per_column]
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# The read side of missing:, checked before the file is read: each setting
|
|
180
|
+
# becomes an Array of token Strings.
|
|
181
|
+
def self.missing_tokens(missing)
|
|
182
|
+
default, per_column = split_missing(missing, "from_csv")
|
|
183
|
+
tokens = lambda do |value|
|
|
184
|
+
list = Array(value)
|
|
185
|
+
bad = list.reject { |t| t.is_a?(String) }
|
|
186
|
+
unless bad.empty?
|
|
187
|
+
raise ArgumentError,
|
|
188
|
+
"missing: tokens are matched against a field's text, so give them " \
|
|
189
|
+
"as Strings (#{bad.map { |t| t.to_s.inspect }.join(', ')}, not " \
|
|
190
|
+
"#{bad.map(&:inspect).join(', ')})"
|
|
191
|
+
end
|
|
192
|
+
# "" is the empty field, which is missing already; as on the write side
|
|
193
|
+
# it adds nothing, so a quoted "" stays the empty string.
|
|
194
|
+
list.reject(&:empty?)
|
|
195
|
+
end
|
|
196
|
+
[tokens.(default), per_column.transform_values(&tokens)]
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def self.mask_missing_tokens(frame, missing)
|
|
200
|
+
default, per_column = missing
|
|
201
|
+
per_column.each_key { |name| frame[name] } # KeyError for a name the file lacks
|
|
202
|
+
frame.column_names.each do |name|
|
|
203
|
+
col = frame[name]
|
|
204
|
+
per_column.fetch(name, default).each { |t| col[:eq, t] = UNDEF }
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
private_class_method :split_missing, :missing_tokens, :mask_missing_tokens
|
|
209
|
+
|
|
210
|
+
# Drive the reading-control DSL over one open IO and hand back
|
|
211
|
+
# [names, rows]. Shared by the path and the IO source, so the two cannot
|
|
212
|
+
# come to read a file differently.
|
|
213
|
+
def self.read_csv (io, sep:, quote:, strip:, layout:, read_text_as_const_string: false, &block)
|
|
214
|
+
reader = CSVReader.new(io, sep: sep, quote: quote, strip: strip,
|
|
215
|
+
read_text_as_const_string: read_text_as_const_string)
|
|
216
|
+
reader.reporting do
|
|
217
|
+
block ? block.call(reader) : reader.layout(**layout)
|
|
218
|
+
end
|
|
219
|
+
reader.result
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
private_class_method :read_csv
|
|
223
|
+
|
|
224
|
+
# Re-raise a failure to decode the input with a line saying how to name the
|
|
225
|
+
# file's encoding. The error and its class are kept; only the message grows.
|
|
226
|
+
# Which encoding the file is in cannot be told from its bytes, so the hint
|
|
227
|
+
# names the option, not a value.
|
|
228
|
+
def self.with_encoding_hint(hint)
|
|
229
|
+
yield
|
|
230
|
+
rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError,
|
|
231
|
+
Encoding::CompatibilityError, ArgumentError => e
|
|
232
|
+
raise if e.is_a?(ArgumentError) && !e.message.include?("invalid byte sequence")
|
|
233
|
+
if e.is_a?(Encoding::CompatibilityError)
|
|
234
|
+
# The text decoded; the separator or quote is in another encoding.
|
|
235
|
+
raise e.class, "#{e.message} -- sep: and quote: are matched against the " \
|
|
236
|
+
"text as read, so read the file transcoded to UTF-8, as in " \
|
|
237
|
+
"encoding: \"<file encoding>:UTF-8\"", e.backtrace
|
|
238
|
+
end
|
|
239
|
+
raise e.class, "#{e.message} -- the CSV is not in the encoding it was " \
|
|
240
|
+
"read as: #{hint}", e.backtrace
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
private_class_method :with_encoding_hint
|
|
244
|
+
|
|
63
245
|
# Write the frame as CSV. CSV is a flat table of scalar cells, so this is the
|
|
64
246
|
# text form of the same all-scalar subset +to_ca+ requires (§11.9): every
|
|
65
247
|
# column must be 1-D. Unlike +to_ca+ it does not promote to a common data type --
|
|
@@ -79,14 +261,50 @@ class CAFrame
|
|
|
79
261
|
# csv = df.to_csv # get a String
|
|
80
262
|
#
|
|
81
263
|
# Options: +sep+ / +quote+ mirror +from_csv+; +header+ writes the name row
|
|
82
|
-
# (default true); +index+ writes the index column (default true)
|
|
83
|
-
|
|
264
|
+
# (default true); +index+ writes the index column (default true);
|
|
265
|
+
# +encoding+ transcodes the text before it is written or returned (default
|
|
266
|
+
# nil leaves it as built, UTF-8). A character the encoding cannot hold
|
|
267
|
+
# raises Encoding::UndefinedConversionError rather than being dropped. A
|
|
268
|
+
# String cell in another encoding is transcoded to UTF-8; one with bytes
|
|
269
|
+
# that are not valid in it, or with no encoding (ASCII-8BIT), raises. Each
|
|
270
|
+
# names the row and the column of the cell.
|
|
271
|
+
#
|
|
272
|
+
# df.to_csv("out.csv", encoding: "CP932") # for Excel in Japanese
|
|
273
|
+
#
|
|
274
|
+
# +missing:+ writes a masked cell as that String instead of an empty field,
|
|
275
|
+
# for a reader that expects a sentinel; +from_csv(missing:)+ with the same
|
|
276
|
+
# String reads it back as UNDEF. A Hash sets it per column the way
|
|
277
|
+
# +from_csv+ does: +:default+ for every column, and a column name (or the
|
|
278
|
+
# index's axis name) for one, replacing the default; "" is the empty field.
|
|
279
|
+
# A cell whose value would be written as the same text raises, since the
|
|
280
|
+
# file could not tell the two apart.
|
|
281
|
+
#
|
|
282
|
+
# df.to_csv("out.csv", missing: "-999")
|
|
283
|
+
# df.to_csv("out.csv", missing: { default: "-999", "comment" => "" })
|
|
284
|
+
def to_csv(path = nil, sep: ",", quote: '"', header: true, index: true,
|
|
285
|
+
encoding: nil, missing: nil)
|
|
286
|
+
default, per_column = CAFrame.send(:split_missing, missing, "to_csv")
|
|
287
|
+
[default, *per_column.values].each do |text|
|
|
288
|
+
next if text.nil? || text.is_a?(String)
|
|
289
|
+
raise ArgumentError, "to_csv: missing: is the String written for a masked " \
|
|
290
|
+
"cell (got #{text.inspect})"
|
|
291
|
+
end
|
|
84
292
|
nd = @columns.find { |_, c| c.ndim != 1 }
|
|
85
293
|
if nd
|
|
86
294
|
raise ArgumentError,
|
|
87
295
|
"to_csv needs all-scalar (1-D) columns; #{nd.first.inspect} is " \
|
|
88
296
|
"#{nd.last.ndim}-D — export it per column or via to_records + JSON"
|
|
89
297
|
end
|
|
298
|
+
# A record cell has no text form that reads back; its members do.
|
|
299
|
+
rec = @columns.find { |_, c| c.is_a?(CARecord) }
|
|
300
|
+
if rec
|
|
301
|
+
name, col = rec
|
|
302
|
+
member = col.members.first
|
|
303
|
+
raise ArgumentError,
|
|
304
|
+
"to_csv cannot write the record column #{name.inspect}; write its " \
|
|
305
|
+
"members as columns, e.g. df[#{"#{name}_#{member}".inspect}] = " \
|
|
306
|
+
"df[#{name.inspect}][#{member.inspect}]"
|
|
307
|
+
end
|
|
90
308
|
|
|
91
309
|
names = []
|
|
92
310
|
formatted = []
|
|
@@ -98,17 +316,46 @@ class CAFrame
|
|
|
98
316
|
names << name
|
|
99
317
|
formatted << format_csv_column(col)
|
|
100
318
|
end
|
|
319
|
+
unknown = per_column.keys - names
|
|
320
|
+
raise KeyError, "to_csv: missing: names no column #{unknown.first.inspect}" unless unknown.empty?
|
|
321
|
+
formatted.each_with_index do |fcol, j|
|
|
322
|
+
token = per_column.fetch(names[j], default)
|
|
323
|
+
next if token.nil? || token.empty? # the empty field, as without missing:
|
|
324
|
+
i = fcol.index(token)
|
|
325
|
+
if i
|
|
326
|
+
raise ArgumentError,
|
|
327
|
+
"to_csv: row #{i} of #{names[j].inspect} is written as " \
|
|
328
|
+
"#{token.inspect}, the text given for a masked cell"
|
|
329
|
+
end
|
|
330
|
+
fcol.map! { |text| text.nil? ? token : text }
|
|
331
|
+
end
|
|
101
332
|
|
|
102
|
-
out =
|
|
103
|
-
|
|
104
|
-
|
|
333
|
+
out = begin
|
|
334
|
+
csv_text(names, formatted, header, sep, quote)
|
|
335
|
+
rescue Encoding::CompatibilityError
|
|
336
|
+
nil
|
|
105
337
|
end
|
|
106
|
-
|
|
107
|
-
|
|
338
|
+
# A cell in another encoding, or with bytes that are not text, is found
|
|
339
|
+
# and named here, after the text was built in one pass, so a frame of
|
|
340
|
+
# UTF-8 text pays nothing for the check.
|
|
341
|
+
unless out && out.encoding == Encoding::UTF_8 && out.valid_encoding?
|
|
342
|
+
names = names.each_with_index.map { |t, j| csv_text_in_utf8(t, "the name of column #{j}") }
|
|
343
|
+
formatted = formatted.each_with_index.map do |fcol, j|
|
|
344
|
+
fcol.each_with_index.map { |t, i| csv_text_in_utf8(t, "row #{i} of #{names[j].inspect}") }
|
|
345
|
+
end
|
|
346
|
+
out = csv_text(names, formatted, header, sep, quote)
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
if encoding
|
|
350
|
+
out = begin
|
|
351
|
+
out.encode(encoding)
|
|
352
|
+
rescue Encoding::UndefinedConversionError => e
|
|
353
|
+
raise csv_unwritable_cell(names, formatted, encoding) || e
|
|
354
|
+
end
|
|
108
355
|
end
|
|
109
356
|
|
|
110
357
|
if path
|
|
111
|
-
File.
|
|
358
|
+
File.binwrite(path, out)
|
|
112
359
|
self
|
|
113
360
|
else
|
|
114
361
|
out
|
|
@@ -146,20 +393,22 @@ class CAFrame
|
|
|
146
393
|
|
|
147
394
|
# +to_s+ is the whole frame, +inspect+ the middle-elided one -- so +puts df+
|
|
148
395
|
# dumps everything and +p df+ stays a screenful. +inspect+ leads with the
|
|
149
|
-
# same summary line it always had (nrow,
|
|
396
|
+
# same summary line it always had (nrow, column data types, index), so the
|
|
150
397
|
# table under it needs no row-count footer.
|
|
151
398
|
def to_s
|
|
152
399
|
to_table(rows: nil)
|
|
153
400
|
end
|
|
154
401
|
|
|
155
|
-
# The summary line (nrow,
|
|
402
|
+
# The summary line (nrow, column data types, index) followed by the
|
|
156
403
|
# middle-elided table.
|
|
157
404
|
# @return [String]
|
|
158
405
|
def inspect
|
|
159
406
|
parts = @columns.map { |k, v| "#{k}:#{v.data_type}#{v.ndim > 1 ? v.shape[1..].inspect : ''}" }
|
|
160
407
|
idx = @index ? " index=#{@axis_name.inspect}" : ""
|
|
161
|
-
head = "#<CAFrame nrow=#{@nrow}
|
|
162
|
-
|
|
408
|
+
head = "#<CAFrame nrow=#{@nrow} columns=[#{parts.join(', ')}]#{idx}>"
|
|
409
|
+
# The table counts the index as a column, so a frame whose only data is its
|
|
410
|
+
# index has one to show. Gate on the same thing render_table does.
|
|
411
|
+
return head if @columns.empty? && @index.nil?
|
|
163
412
|
head + "\n" + render_table(head: 8, tail: 2, index: true, precision: 6,
|
|
164
413
|
footer: false)
|
|
165
414
|
end
|
|
@@ -199,7 +448,7 @@ class CAFrame
|
|
|
199
448
|
out << table_row(widths.map { |w| "-" * w }, widths, aligns) << "\n"
|
|
200
449
|
body.each { |cells| out << table_row(cells, widths, aligns) << "\n" }
|
|
201
450
|
if footer && positions.size - positions.count(nil) < @nrow
|
|
202
|
-
out << "(#{plural(@nrow, 'row')}, #{plural(@columns.size, '
|
|
451
|
+
out << "(#{plural(@nrow, 'row')}, #{plural(@columns.size, 'column')})\n"
|
|
203
452
|
end
|
|
204
453
|
out
|
|
205
454
|
end
|
|
@@ -252,8 +501,31 @@ class CAFrame
|
|
|
252
501
|
"#{n} #{noun}#{n == 1 ? '' : 's'}"
|
|
253
502
|
end
|
|
254
503
|
|
|
504
|
+
# Render one N-D cell as a bracketed list. Each element is shown as a
|
|
505
|
+
# scalar cell of its kind would be -- floats rounded to +precision+, a
|
|
506
|
+
# masked element as the table's missing marker -- except that a String
|
|
507
|
+
# keeps its quotes, so the list stays readable when one holds a comma.
|
|
508
|
+
private def format_nested_cell(v, precision)
|
|
509
|
+
case v
|
|
510
|
+
when Array then "[" + v.map { |x| format_nested_cell(x, precision) }.join(", ") + "]"
|
|
511
|
+
when String then v.inspect
|
|
512
|
+
else format_scalar_cell(v, precision)
|
|
513
|
+
end
|
|
514
|
+
end
|
|
515
|
+
|
|
255
516
|
private def format_table_cell(col, i, precision)
|
|
256
517
|
e = elem_at(col, i)
|
|
518
|
+
if e.is_a?(CArray)
|
|
519
|
+
# An N-D cell renders its elements, and a masked element among them
|
|
520
|
+
# takes the same marker a masked scalar does -- UNDEF's own inspect
|
|
521
|
+
# would put a second spelling of "missing" in the same table.
|
|
522
|
+
format_nested_cell(e.to_a, precision)
|
|
523
|
+
else
|
|
524
|
+
format_scalar_cell(e, precision)
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
private def format_scalar_cell(e, precision)
|
|
257
529
|
if UNDEF.equal?(e) || e.nil?
|
|
258
530
|
"_"
|
|
259
531
|
elsif e.is_a?(Float) && precision
|
|
@@ -261,8 +533,6 @@ class CAFrame
|
|
|
261
533
|
# sets the column width for every other row and makes the table hard
|
|
262
534
|
# to read. precision: nil prints the value as Ruby renders it.
|
|
263
535
|
e.round(precision).to_s
|
|
264
|
-
elsif e.is_a?(CArray)
|
|
265
|
-
e.to_a.inspect
|
|
266
536
|
elsif e.is_a?(String)
|
|
267
537
|
e
|
|
268
538
|
elsif e.respond_to?(:iso8601)
|
|
@@ -273,6 +543,15 @@ class CAFrame
|
|
|
273
543
|
end
|
|
274
544
|
|
|
275
545
|
private def format_csv_column(col)
|
|
546
|
+
# A float32 value goes out as its own shortest decimal ("0.1"), which
|
|
547
|
+
# from_csv reads back as the same value, not as the double it widens
|
|
548
|
+
# to ("0.10000000149011612"); cmplx64 has that in each part.
|
|
549
|
+
if !col.face? && (col.data_type == CA_FLOAT32 || col.data_type == CA_CMPLX64)
|
|
550
|
+
col = col.__shortest_float64__
|
|
551
|
+
end
|
|
552
|
+
# A boolean goes out as 0 / 1, as it does in every other serialization;
|
|
553
|
+
# from_csv reads it back with types: { name => :boolean }.
|
|
554
|
+
col = col.to_type(:uint8) if !col.face? && col.data_type == CA_BOOLEAN
|
|
276
555
|
col.to_a.map do |e|
|
|
277
556
|
if UNDEF.equal?(e) || e.nil?
|
|
278
557
|
nil
|
|
@@ -286,36 +565,149 @@ class CAFrame
|
|
|
286
565
|
end
|
|
287
566
|
end
|
|
288
567
|
|
|
568
|
+
private def csv_text(names, formatted, header, sep, quote)
|
|
569
|
+
out = +""
|
|
570
|
+
if header
|
|
571
|
+
out << names.map { |t| quote_csv_field(t, sep, quote) }.join(sep) << "\n"
|
|
572
|
+
end
|
|
573
|
+
@nrow.times do |i|
|
|
574
|
+
out << formatted.map { |fcol| quote_csv_field(fcol[i], sep, quote) }.join(sep) << "\n"
|
|
575
|
+
end
|
|
576
|
+
out
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
# The text of a cell as UTF-8, the encoding the CSV is built in: text in
|
|
580
|
+
# another encoding is transcoded; bytes that are not valid in their
|
|
581
|
+
# encoding, or have none (ASCII-8BIT), raise naming the cell.
|
|
582
|
+
private def csv_text_in_utf8(text, where)
|
|
583
|
+
return text if text.nil?
|
|
584
|
+
enc = text.encoding
|
|
585
|
+
unless text.valid_encoding?
|
|
586
|
+
raise Encoding::InvalidByteSequenceError,
|
|
587
|
+
"to_csv: #{where} (#{csv_excerpt(text)}) is not valid #{enc}"
|
|
588
|
+
end
|
|
589
|
+
return text if enc == Encoding::UTF_8 || (enc.ascii_compatible? && text.ascii_only?)
|
|
590
|
+
if enc == Encoding::BINARY
|
|
591
|
+
raise Encoding::CompatibilityError,
|
|
592
|
+
"to_csv: #{where} (#{csv_excerpt(text)}) is bytes with no encoding " \
|
|
593
|
+
"(ASCII-8BIT); give it one with force_encoding"
|
|
594
|
+
end
|
|
595
|
+
text.encode(Encoding::UTF_8)
|
|
596
|
+
rescue Encoding::UndefinedConversionError
|
|
597
|
+
raise Encoding::UndefinedConversionError,
|
|
598
|
+
"to_csv: #{where} (#{csv_excerpt(text)}) has a character with no UTF-8 form in #{enc}"
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
# The start of a cell's text, quoted, for an error. Text is shown as it
|
|
602
|
+
# is, whatever the locale (inspect would escape it where the locale is not
|
|
603
|
+
# UTF-8); control characters and bytes that are not text are escaped.
|
|
604
|
+
private def csv_excerpt(text)
|
|
605
|
+
head = text[0, 20]
|
|
606
|
+
return head.dump unless head.valid_encoding? && head.encoding != Encoding::BINARY
|
|
607
|
+
head = head.encode(Encoding::UTF_8)
|
|
608
|
+
'"' + head.gsub(/[\x00-\x1f\x7f"\\]/) { |c| c.dump[1..-2] } + '"'
|
|
609
|
+
rescue EncodingError
|
|
610
|
+
text[0, 20].dump
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
# The error for the first cell (or name) +encoding+ cannot hold, or nil.
|
|
614
|
+
private def csv_unwritable_cell(names, formatted, encoding)
|
|
615
|
+
cells = names.each_with_index.map { |t, j| [t, "the name of column #{j}"] }
|
|
616
|
+
formatted.each_with_index do |fcol, j|
|
|
617
|
+
fcol.each_with_index { |t, i| cells << [t, "row #{i} of #{names[j].inspect}"] if t }
|
|
618
|
+
end
|
|
619
|
+
cells.each do |text, where|
|
|
620
|
+
text.encode(encoding)
|
|
621
|
+
rescue Encoding::UndefinedConversionError => e
|
|
622
|
+
return Encoding::UndefinedConversionError.new(
|
|
623
|
+
"to_csv: #{where} (#{csv_excerpt(text)}): #{csv_excerpt(e.error_char)} (U+#{format("%04X", e.error_char.ord)}) cannot be written in #{encoding}")
|
|
624
|
+
end
|
|
625
|
+
nil
|
|
626
|
+
end
|
|
627
|
+
|
|
289
628
|
private def quote_csv_field(text, sep, quote)
|
|
290
629
|
return "" if text.nil?
|
|
630
|
+
# With a separator longer than one character, a value can end in part of
|
|
631
|
+
# it ("a:" before "::"), and the reader would find the separator inside
|
|
632
|
+
# the value; such a value is quoted too. A value of spaces and tabs is
|
|
633
|
+
# quoted so that it is neither a blank line the reader skips (a header of
|
|
634
|
+
# one column) nor a field strip: empties.
|
|
291
635
|
if text.empty? || text.include?(sep) || text.include?(quote) ||
|
|
292
|
-
text.include?("\n") || text.include?("\r")
|
|
636
|
+
text.include?("\n") || text.include?("\r") || text.each_byte.all? { |b| b == 0x20 || b == 0x09 } ||
|
|
637
|
+
(sep.size > 1 && (text + sep).index(sep) != text.size)
|
|
293
638
|
quote + text.gsub(quote, quote * 2) + quote
|
|
294
639
|
else
|
|
295
640
|
text
|
|
296
641
|
end
|
|
297
642
|
end
|
|
298
643
|
|
|
644
|
+
# Build a frame from parsed [names, rows]. When names is nil (headerless and
|
|
645
|
+
# no column_names) positional names "c0".."cN" are generated from the widest
|
|
646
|
+
# row. Rows are squared off to the column count (short rows padded with nil,
|
|
647
|
+
# over-long rows raise), one 2-D object array is bulk-filled, and each column
|
|
648
|
+
# is a view into it (§3.6).
|
|
299
649
|
def self.build_frame(names, rows)
|
|
300
|
-
ncol = names
|
|
650
|
+
ncol = if names then names.size
|
|
651
|
+
elsif rows.is_a?(CArray) then rows.shape[1]
|
|
652
|
+
else rows.map(&:size).max || 0
|
|
653
|
+
end
|
|
301
654
|
names ||= Array.new(ncol) { |j| "c#{j}" }
|
|
655
|
+
# A frame holds one column per name, so a repeated one would keep only
|
|
656
|
+
# the last of its columns.
|
|
657
|
+
dup = names.tally.select { |_, count| count > 1 }.keys
|
|
658
|
+
unless dup.empty?
|
|
659
|
+
raise ArgumentError,
|
|
660
|
+
"the header names #{dup.map(&:inspect).join(', ')} more than once; " \
|
|
661
|
+
"name the columns yourself: from_csv(path, header: 0, " \
|
|
662
|
+
"column_names: [...])"
|
|
663
|
+
end
|
|
302
664
|
|
|
303
665
|
cols = {}
|
|
304
|
-
|
|
305
|
-
|
|
666
|
+
# Columns the C reader built as CAConstStrings over the text, for
|
|
667
|
+
# cast to read without a String per cell.
|
|
668
|
+
if rows.is_a?(CSVReader::ConstStringColumns)
|
|
669
|
+
names.zip(rows.columns) { |name, col| cols[name] = col }
|
|
670
|
+
return new(cols)
|
|
671
|
+
end
|
|
672
|
+
# A table the C reader built: one row per record, missing cells masked.
|
|
673
|
+
table = rows if rows.is_a?(CArray) && rows.elements > 0
|
|
674
|
+
if table.nil? && (rows.is_a?(CArray) || rows.empty?)
|
|
675
|
+
names.each { |name| cols[name] = CArray.string(CArray.object(0)) }
|
|
306
676
|
return new(cols)
|
|
307
677
|
end
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
678
|
+
table ||= table_of_rows(rows, ncol)
|
|
679
|
+
# Each column is a CAString over its view of the table: the text, with
|
|
680
|
+
# the string operations (strip!, gsub, extract, ...) at hand, writing
|
|
681
|
+
# through to the same cells.
|
|
682
|
+
names.each_with_index { |name, j| cols[name] = CArray.string(table[nil, j]) }
|
|
683
|
+
new(cols)
|
|
684
|
+
end
|
|
685
|
+
private_class_method :build_frame
|
|
686
|
+
|
|
687
|
+
def self.table_of_rows(rows, ncol)
|
|
688
|
+
# A missing field is UNDEF, not a Ruby nil sitting in a cell. The
|
|
689
|
+
# tokenizer says missing with nil (an unquoted empty field; a quoted
|
|
690
|
+
# one is the empty string and stays a value), and an object array will
|
|
691
|
+
# hold that nil quite happily -- so a column read without `types:` used
|
|
692
|
+
# to carry nil where the same column read with one carried UNDEF, and
|
|
693
|
+
# the mask a to_csv had written did not survive the trip back. A row
|
|
694
|
+
# holding a nil gets UNDEF in its place before the table is built (an
|
|
695
|
+
# UNDEF in the rows becomes a masked cell), so a row with nothing
|
|
696
|
+
# missing costs no pass over its cells. Short rows are padded, and a
|
|
697
|
+
# row with a nil is changed, on a copy: the rows may be a parser:
|
|
698
|
+
# callable's own arrays.
|
|
699
|
+
rows = rows.each_with_index.map do |r, i|
|
|
700
|
+
if r.size > ncol
|
|
312
701
|
raise ArgumentError,
|
|
313
702
|
"row #{i + 1} has #{r.size} fields, expected #{ncol}"
|
|
314
703
|
end
|
|
704
|
+
r = r + Array.new(ncol - r.size) if r.size < ncol
|
|
705
|
+
# compact tests for nil without calling == on every cell, as
|
|
706
|
+
# include?(nil) would.
|
|
707
|
+
r = r.map { |cell| cell.nil? ? UNDEF : cell } if r.compact.size < ncol
|
|
708
|
+
r
|
|
315
709
|
end
|
|
316
|
-
|
|
317
|
-
names.each_with_index { |name, j| cols[name] = table[nil, j] }
|
|
318
|
-
new(cols)
|
|
710
|
+
CArray.object(rows.size, ncol) { rows }
|
|
319
711
|
end
|
|
320
|
-
private_class_method :
|
|
712
|
+
private_class_method :table_of_rows
|
|
321
713
|
end
|