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
|
@@ -13,7 +13,11 @@
|
|
|
13
13
|
# * An empty unquoted field is nil (missing); an empty quoted field ("") is
|
|
14
14
|
# the empty String. Both cast to UNDEF for numeric columns (parse-mask).
|
|
15
15
|
# * A UTF-8 BOM is stripped via the "bom|utf-8" read mode by default.
|
|
16
|
-
# *
|
|
16
|
+
# * Malformed input raises MalformedCSV naming the file and the line: a
|
|
17
|
+
# quote inside an unquoted field, text after a closing quote, a quoted
|
|
18
|
+
# field still open at the end of input, or a record with more fields
|
|
19
|
+
# than there are columns. A record continues onto the next line only
|
|
20
|
+
# inside a quoted field.
|
|
17
21
|
#
|
|
18
22
|
# Two entry points share the Tokenizer:
|
|
19
23
|
# * CSVParser.parse / parse_file -- whole-file parse into [headers, rows].
|
|
@@ -22,6 +26,7 @@
|
|
|
22
26
|
# column_names).
|
|
23
27
|
|
|
24
28
|
require "strscan"
|
|
29
|
+
require "stringio"
|
|
25
30
|
|
|
26
31
|
class CAFrame
|
|
27
32
|
# CSV tokenizer behind `CAFrame.from_csv`. It produces raw String cells
|
|
@@ -36,9 +41,35 @@ class CAFrame
|
|
|
36
41
|
module CSVParser
|
|
37
42
|
module_function
|
|
38
43
|
|
|
39
|
-
# Raised when the input cannot be
|
|
40
|
-
#
|
|
41
|
-
|
|
44
|
+
# Raised when the input cannot be read as written. The message starts
|
|
45
|
+
# with "path:line:" (or "line N:" for an IO with no path), the form an
|
|
46
|
+
# editor or terminal jumps to; the record number follows when it is not
|
|
47
|
+
# the line's, as after a quoted field of several lines or a +skip+.
|
|
48
|
+
# +path+, +lineno+ and +record+ give the same for a program.
|
|
49
|
+
class MalformedCSV < StandardError
|
|
50
|
+
attr_accessor :path
|
|
51
|
+
attr_reader :lineno, :record, :detail
|
|
52
|
+
|
|
53
|
+
def initialize(detail = nil, lineno: nil, record: nil, path: nil)
|
|
54
|
+
@detail = detail
|
|
55
|
+
@lineno = lineno
|
|
56
|
+
@record = record
|
|
57
|
+
@path = path
|
|
58
|
+
super(detail)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def message
|
|
62
|
+
return super if @detail.nil?
|
|
63
|
+
where =
|
|
64
|
+
if @path && @lineno then "#{@path}:#{@lineno}: "
|
|
65
|
+
elsif @path then "#{@path}: "
|
|
66
|
+
elsif @lineno then "line #{@lineno}: "
|
|
67
|
+
else ""
|
|
68
|
+
end
|
|
69
|
+
tail = @record && @record != @lineno ? " (record #{@record})" : ""
|
|
70
|
+
"#{where}#{@detail}#{tail}"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
42
73
|
|
|
43
74
|
# Parse a file into [headers, rows]. +encoding+ is an IO open-mode encoding
|
|
44
75
|
# string; the default strips a leading BOM and reads UTF-8.
|
|
@@ -47,7 +78,8 @@ class CAFrame
|
|
|
47
78
|
end
|
|
48
79
|
|
|
49
80
|
# Parse an IO (or anything answering +gets+). The first record supplies the
|
|
50
|
-
# headers; the rest are data rows.
|
|
81
|
+
# headers; the rest are data rows. Blank lines are skipped, including in a
|
|
82
|
+
# one-column file (from_csv reads them there as missing cells).
|
|
51
83
|
def parse(io, sep: ",", quote: '"', strip: false)
|
|
52
84
|
tok = Tokenizer.new(sep, quote, strip)
|
|
53
85
|
headers = nil
|
|
@@ -62,105 +94,250 @@ class CAFrame
|
|
|
62
94
|
[headers, rows]
|
|
63
95
|
end
|
|
64
96
|
|
|
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
97
|
# Splits records into fields. Regexps are compiled once and reused across
|
|
81
98
|
# every record, so per-row cost stays low.
|
|
82
99
|
class Tokenizer
|
|
100
|
+
attr_reader :sep, :quote, :strip
|
|
101
|
+
|
|
83
102
|
def initialize(sep, quote, strip)
|
|
103
|
+
unless sep.is_a?(String) && !sep.empty?
|
|
104
|
+
raise ArgumentError, "sep: must be a non-empty String (got #{sep.inspect})"
|
|
105
|
+
end
|
|
106
|
+
unless quote.is_a?(String) && quote.size == 1
|
|
107
|
+
raise ArgumentError, "quote: must be one character (got #{quote.inspect})"
|
|
108
|
+
end
|
|
109
|
+
if sep.include?(quote)
|
|
110
|
+
raise ArgumentError, "sep: #{sep.inspect} cannot contain the quote character"
|
|
111
|
+
end
|
|
84
112
|
@sep = sep
|
|
85
113
|
@quote = quote
|
|
86
114
|
@strip = strip
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
@
|
|
90
|
-
|
|
115
|
+
s = Regexp.escape(sep)
|
|
116
|
+
q = Regexp.escape(quote)
|
|
117
|
+
@sep_re = /#{s}/
|
|
118
|
+
# String#split(" ") splits on runs of whitespace (awk style); a
|
|
119
|
+
# Regexp splits at each separator as written, as the C reader does.
|
|
120
|
+
@split_sep = sep == " " ? @sep_re : sep
|
|
121
|
+
# strip: skips the spaces and tabs around a field, but not one that is
|
|
122
|
+
# the separator: with sep: "\t" a tab ends the field.
|
|
123
|
+
pad = [" ", "\t"].reject { |c| sep.include?(c) }
|
|
124
|
+
@pad_re = pad.empty? ? nil : /[#{Regexp.escape(pad.join)}]+/
|
|
125
|
+
@quote_re = /#{q}/
|
|
126
|
+
# An unquoted field runs up to the separator, a quote or the end of
|
|
127
|
+
# the record; a CR that does not end the record is part of the field.
|
|
128
|
+
# A character class says "up to the separator" for a one-character
|
|
129
|
+
# one; a longer one has to be matched as a whole, or "::" would end
|
|
130
|
+
# the field at a single ":".
|
|
131
|
+
@unquoted =
|
|
132
|
+
if sep.size == 1
|
|
133
|
+
/(?:[^#{s}#{q}\r\n]|\r(?!\n|\z))*/
|
|
134
|
+
else
|
|
135
|
+
/(?:(?!#{s})(?:[^#{q}\r\n]|\r(?!\n|\z)))*/
|
|
136
|
+
end
|
|
137
|
+
@inner = /[^#{q}]*/
|
|
138
|
+
@eol = /\r?\n|\r\z/
|
|
139
|
+
@blank_re = /\A[ \t]*\z/
|
|
140
|
+
@sep2 = sep * 2
|
|
141
|
+
@qq = quote * 2
|
|
142
|
+
# A whole quoted field: its quotes closed, the ones inside doubled.
|
|
143
|
+
@quoted_re = /\A#{q}(?:[^#{q}]|#{q}#{q})*#{q}\z/m
|
|
91
144
|
@recno = 0
|
|
145
|
+
@lineno = 0
|
|
146
|
+
@record_line = 0
|
|
92
147
|
end
|
|
93
148
|
|
|
94
|
-
#
|
|
95
|
-
|
|
149
|
+
# The lines read so far, and the line the last record started on.
|
|
150
|
+
attr_reader :lineno, :record_line
|
|
151
|
+
|
|
152
|
+
# The next line of +io+, counted.
|
|
153
|
+
def gets(io)
|
|
154
|
+
line = io.gets
|
|
155
|
+
@lineno += 1 if line
|
|
156
|
+
line
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# A MalformedCSV at +line+ of the current record.
|
|
160
|
+
def malformed(detail, line = @record_line)
|
|
161
|
+
MalformedCSV.new(detail, lineno: line, record: @recno)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Fields of the next record, or nil at EOF. A blank line -- empty, or
|
|
165
|
+
# only spaces and tabs with no separator -- cannot be a row of a file
|
|
166
|
+
# with more than one column and is skipped as noise between records.
|
|
167
|
+
# In a single-column file it is a row: an empty line is the only
|
|
168
|
+
# spelling a missing single field has, which is what to_csv writes for
|
|
169
|
+
# a masked cell, and spaces are a value. So the caller passes
|
|
170
|
+
# +blank_is_row: true+ once the column count is known to be one, and
|
|
171
|
+
# the line becomes a row for build_frame to pad. +blank?+ says whether
|
|
172
|
+
# the record just returned was such a line, for a caller that learns
|
|
173
|
+
# the column count only after the body.
|
|
174
|
+
#
|
|
175
|
+
# A line with no quote character is a whole record. One with a quote
|
|
176
|
+
# goes to the scanner, which reads further lines only while a field
|
|
177
|
+
# that opened with a quote is still open -- so a quote inside an
|
|
178
|
+
# unquoted field is malformed rather than a reason to swallow the lines
|
|
179
|
+
# after it, and a field of many lines is read once, not re-counted.
|
|
180
|
+
def read(io, blank_is_row: false, last_line: nil)
|
|
96
181
|
loop do
|
|
97
|
-
|
|
98
|
-
|
|
182
|
+
return nil if last_line && @lineno >= last_line
|
|
183
|
+
line = gets(io)
|
|
184
|
+
return nil if line.nil?
|
|
99
185
|
@recno += 1
|
|
100
|
-
|
|
101
|
-
|
|
186
|
+
@record_line = @lineno
|
|
187
|
+
@blank = false
|
|
188
|
+
if line.include?(@quote)
|
|
189
|
+
return (!@strip && quoted_line(line)) || scan(line, io)
|
|
190
|
+
end
|
|
191
|
+
rec = line.chomp
|
|
192
|
+
if rec.match?(@blank_re) && !rec.include?(@sep)
|
|
193
|
+
next unless blank_is_row
|
|
194
|
+
@blank = true
|
|
195
|
+
end
|
|
196
|
+
return simple(rec)
|
|
102
197
|
end
|
|
103
198
|
end
|
|
104
199
|
|
|
200
|
+
def blank?
|
|
201
|
+
@blank
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Count records and lines read elsewhere (the C reader), so the
|
|
205
|
+
# numbers in an error stay those of the file.
|
|
206
|
+
def advance(records, lines)
|
|
207
|
+
@recno += records
|
|
208
|
+
@lineno += lines
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# An empty field lies at the start or the end of the record or between
|
|
212
|
+
# two separators, so a record without one is the split as it stands.
|
|
105
213
|
private def simple(rec)
|
|
106
|
-
rec.split(@
|
|
107
|
-
|
|
108
|
-
cell.empty? ? nil : cell
|
|
214
|
+
fields = rec.split(@split_sep, -1)
|
|
215
|
+
if @strip
|
|
216
|
+
fields.map! { |cell| cell = cell.strip; cell.empty? ? nil : cell }
|
|
217
|
+
elsif rec.empty? || rec.start_with?(@sep) || rec.end_with?(@sep) || rec.include?(@sep2)
|
|
218
|
+
fields.map! { |cell| cell.empty? ? nil : cell }
|
|
109
219
|
end
|
|
220
|
+
fields
|
|
110
221
|
end
|
|
111
222
|
|
|
112
|
-
|
|
113
|
-
|
|
223
|
+
# The fields of a one-line record holding quotes, read by splitting at
|
|
224
|
+
# the separator: a quoted field is the pieces from one that opens with
|
|
225
|
+
# a quote to the one that closes it (it may hold the separator). nil
|
|
226
|
+
# when the record is not that simple -- a quoted field open at the end
|
|
227
|
+
# of the line, a quote inside an unquoted field, text after a closing
|
|
228
|
+
# quote -- for the scanner to read or to report.
|
|
229
|
+
private def quoted_line(line)
|
|
230
|
+
parts = line.chomp.split(@split_sep, -1)
|
|
231
|
+
fields = []
|
|
232
|
+
i = 0
|
|
233
|
+
while i < parts.size
|
|
234
|
+
part = parts[i]
|
|
235
|
+
if part.start_with?(@quote)
|
|
236
|
+
until part.match?(@quoted_re)
|
|
237
|
+
i += 1
|
|
238
|
+
return nil if i >= parts.size
|
|
239
|
+
part = part + @sep + parts[i]
|
|
240
|
+
end
|
|
241
|
+
inner = part[1...-1]
|
|
242
|
+
fields << (inner.include?(@quote) ? inner.gsub(@qq, @quote) : inner)
|
|
243
|
+
elsif part.include?(@quote)
|
|
244
|
+
return nil
|
|
245
|
+
else
|
|
246
|
+
fields << (part.empty? ? nil : part)
|
|
247
|
+
end
|
|
248
|
+
i += 1
|
|
249
|
+
end
|
|
250
|
+
fields
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Fields of the record that starts on +line+, reading more lines from
|
|
254
|
+
# +io+ while a quoted field is open. strip: skips spaces before a field,
|
|
255
|
+
# so a quote after them still opens one.
|
|
256
|
+
private def scan(line, io)
|
|
257
|
+
sc = StringScanner.new(line)
|
|
114
258
|
fields = []
|
|
115
259
|
loop do
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
end
|
|
260
|
+
sc.skip(@pad_re) if @strip && @pad_re
|
|
261
|
+
if sc.skip(@quote_re)
|
|
262
|
+
fields << quoted_field(sc, io, fields.size + 1, line_at(sc))
|
|
263
|
+
# A closed quoted field ends at the separator or the end of the
|
|
264
|
+
# record. Anything else there is malformed, and read on it would
|
|
265
|
+
# drop the rest of the record without a word. strip: lets spaces
|
|
266
|
+
# through, as it does around an unquoted field.
|
|
267
|
+
sc.skip(@pad_re) if @strip && @pad_re
|
|
268
|
+
unless sc.eos? || sc.check(@sep_re) || sc.check(@eol)
|
|
269
|
+
raise malformed("text #{sc.rest.chomp[0, 20].inspect} after the closing quote " \
|
|
270
|
+
"of field #{fields.size}", line_at(sc))
|
|
128
271
|
end
|
|
129
|
-
fields << buf
|
|
130
272
|
else
|
|
131
|
-
cell = sc.scan(@unquoted)
|
|
273
|
+
cell = sc.scan(@unquoted)
|
|
274
|
+
if sc.check(@quote_re)
|
|
275
|
+
raise malformed("a quote inside unquoted field #{fields.size + 1} " \
|
|
276
|
+
"(#{(cell + sc.rest.chomp)[0, 20].inspect}); a field holding " \
|
|
277
|
+
"a quote is written quoted, with the quote doubled", line_at(sc))
|
|
278
|
+
end
|
|
132
279
|
cell = cell.strip if @strip
|
|
133
280
|
fields << (cell.empty? ? nil : cell)
|
|
134
281
|
end
|
|
135
|
-
break unless sc.
|
|
282
|
+
break unless sc.skip(@sep_re)
|
|
136
283
|
end
|
|
284
|
+
sc.skip(@eol)
|
|
137
285
|
fields
|
|
138
286
|
end
|
|
287
|
+
|
|
288
|
+
# The line of the file the scanner is at: the record's first line and
|
|
289
|
+
# the line breaks it has passed in quoted fields.
|
|
290
|
+
private def line_at(sc)
|
|
291
|
+
@record_line + sc.string[0, sc.pos].count("\n")
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# The rest of a field whose opening quote has been read, through its
|
|
295
|
+
# closing quote. A doubled quote is a literal one; the end of the line
|
|
296
|
+
# inside the field brings the next line in.
|
|
297
|
+
private def quoted_field(sc, io, field_no, open_line)
|
|
298
|
+
buf = +""
|
|
299
|
+
loop do
|
|
300
|
+
buf << sc.scan(@inner)
|
|
301
|
+
if sc.eos?
|
|
302
|
+
more = gets(io)
|
|
303
|
+
if more.nil?
|
|
304
|
+
raise malformed("quoted field #{field_no} opened here is never closed", open_line)
|
|
305
|
+
end
|
|
306
|
+
sc << more
|
|
307
|
+
next
|
|
308
|
+
end
|
|
309
|
+
sc.skip(@quote_re)
|
|
310
|
+
break unless sc.skip(@quote_re)
|
|
311
|
+
buf << @quote
|
|
312
|
+
end
|
|
313
|
+
buf
|
|
314
|
+
end
|
|
139
315
|
end
|
|
140
316
|
end
|
|
141
317
|
|
|
142
|
-
# The
|
|
143
|
-
#
|
|
144
|
-
# in order, how to consume the stream:
|
|
318
|
+
# The reading-control block for CAFrame.from_csv. A file often has
|
|
319
|
+
# preamble lines, a units row, or no header at all; the block is given the
|
|
320
|
+
# reader and says, in order, how to consume the stream:
|
|
145
321
|
#
|
|
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
|
-
#
|
|
322
|
+
# CAFrame.from_csv(path) do |r|
|
|
323
|
+
# r.skip 3 # drop 3 preamble lines
|
|
324
|
+
# r.header # next record supplies the column names
|
|
325
|
+
# r.skip 1 # drop a units row
|
|
326
|
+
# r.data # the rest are data rows
|
|
151
327
|
# end
|
|
152
328
|
#
|
|
153
|
-
# CAFrame.from_csv(path)
|
|
154
|
-
# column_names "date", "temp", "rh"
|
|
155
|
-
# body
|
|
156
|
-
# end
|
|
329
|
+
# CAFrame.from_csv(path) { it.column_names "date", "temp", "rh"; it.data }
|
|
157
330
|
#
|
|
158
|
-
# The verbs are +skip+ / +header+ / +column_names+ / +
|
|
159
|
-
# value useful inline (header returns its fields) and the ordering is the
|
|
160
|
-
# script.
|
|
331
|
+
# The verbs are +skip+ / +header+ / +column_names+ / +columns+ / +data+;
|
|
332
|
+
# each returns a value useful inline (header returns its fields) and the ordering is the
|
|
333
|
+
# script. The reader is a parameter, not self, so a local variable named
|
|
334
|
+
# +data+ or +header+ cannot stand in for the verb. Without a block,
|
|
335
|
+
# from_csv reads by its header: / data: / column_names: (see +layout+).
|
|
161
336
|
class CSVReader
|
|
162
|
-
def initialize(io, sep: ",", quote: '"', strip: false)
|
|
337
|
+
def initialize(io, sep: ",", quote: '"', strip: false, read_text_as_const_string: false)
|
|
338
|
+
@read_text_as_const_string = read_text_as_const_string
|
|
163
339
|
@io = io
|
|
340
|
+
@path = io.path if io.respond_to?(:path)
|
|
164
341
|
@tok = CSVParser::Tokenizer.new(sep, quote, strip)
|
|
165
342
|
@names = nil
|
|
166
343
|
@rows = nil
|
|
@@ -168,7 +345,11 @@ class CAFrame
|
|
|
168
345
|
|
|
169
346
|
# Drop +n+ raw lines (preamble, units, notes).
|
|
170
347
|
def skip(n = 1)
|
|
171
|
-
n.
|
|
348
|
+
unless n.is_a?(Integer) && n >= 0
|
|
349
|
+
raise ArgumentError, "skip takes a number of lines (got #{n.inspect})"
|
|
350
|
+
end
|
|
351
|
+
n.times { @tok.gets(@io) }
|
|
352
|
+
@skipped = true if n > 0
|
|
172
353
|
self
|
|
173
354
|
end
|
|
174
355
|
|
|
@@ -177,35 +358,368 @@ class CAFrame
|
|
|
177
358
|
# names. Returns the record's fields either way.
|
|
178
359
|
def header(name = nil)
|
|
179
360
|
fields = @tok.read(@io)
|
|
180
|
-
|
|
361
|
+
if fields.nil?
|
|
362
|
+
# An input with no record at all is a table with no columns: that is
|
|
363
|
+
# what to_csv writes for a frame without columns or index. Once lines
|
|
364
|
+
# have been skipped to reach the header, a missing header is an error.
|
|
365
|
+
if name.nil? && !@skipped
|
|
366
|
+
@names = []
|
|
367
|
+
@header_line = @tok.lineno
|
|
368
|
+
@names_to_check = nil
|
|
369
|
+
return []
|
|
370
|
+
end
|
|
371
|
+
raise CSVParser::MalformedCSV.new("header expected but input ended",
|
|
372
|
+
lineno: @tok.lineno + 1)
|
|
373
|
+
end
|
|
181
374
|
if name.nil?
|
|
182
375
|
@names = fields.map(&:to_s)
|
|
376
|
+
@header_line = @tok.record_line
|
|
377
|
+
@names_to_check = nil
|
|
183
378
|
else
|
|
184
379
|
(@named_headers ||= {})[name.to_s] = fields
|
|
185
380
|
end
|
|
186
381
|
fields
|
|
187
382
|
end
|
|
188
383
|
|
|
189
|
-
# Set the column names explicitly
|
|
384
|
+
# Set the column names explicitly: for a file with no header, or in
|
|
385
|
+
# place of the names a header gave. Their number has to be the file's
|
|
386
|
+
# number of columns -- the header's, or else that of the first record
|
|
387
|
+
# of the data -- or a name would go to no column, or a column without
|
|
388
|
+
# one.
|
|
190
389
|
def column_names(*names)
|
|
191
|
-
|
|
390
|
+
names = names.flatten.map(&:to_s)
|
|
391
|
+
if @header_line
|
|
392
|
+
check_column_names(names, @names.size, @header_line)
|
|
393
|
+
else
|
|
394
|
+
@names_to_check = names
|
|
395
|
+
end
|
|
396
|
+
@names = names
|
|
192
397
|
self
|
|
193
398
|
end
|
|
194
399
|
|
|
195
|
-
#
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
400
|
+
# Read only these columns: names of the header or of column_names, or
|
|
401
|
+
# indexes of the file's columns from 0 (Integers or Ranges), in the order
|
|
402
|
+
# the frame is to have them. The fields of the others are read past, so
|
|
403
|
+
# a few columns of a wide file cost little more than a narrow file.
|
|
404
|
+
def columns(*selection)
|
|
405
|
+
selection = selection.flatten
|
|
406
|
+
if selection.empty?
|
|
407
|
+
raise ArgumentError, "from_csv: columns: names no column"
|
|
408
|
+
end
|
|
409
|
+
selection.each do |c|
|
|
410
|
+
next if c.is_a?(String)
|
|
411
|
+
next if c.is_a?(Integer) && c >= 0
|
|
412
|
+
next if c.is_a?(Range) && (c.begin.nil? || (c.begin.is_a?(Integer) && c.begin >= 0)) &&
|
|
413
|
+
(c.end.nil? || c.end.is_a?(Integer))
|
|
414
|
+
raise ArgumentError, "from_csv: columns: takes column names, or indexes from 0 " \
|
|
415
|
+
"and Ranges of them (got #{c.inspect})"
|
|
416
|
+
end
|
|
417
|
+
@selection = selection
|
|
418
|
+
self
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# The indexes of the selected columns, in the order asked for, among
|
|
422
|
+
# ncol columns named +names+ (nil: by index only). nil when ncol is not
|
|
423
|
+
# known yet and the selection needs it.
|
|
424
|
+
private def selected_indexes(names, ncol)
|
|
425
|
+
indexes = @selection.flat_map do |c|
|
|
426
|
+
case c
|
|
427
|
+
when String
|
|
428
|
+
j = names&.index(c)
|
|
429
|
+
raise KeyError, "from_csv: columns: names no column #{c.inspect}" if j.nil?
|
|
430
|
+
j
|
|
431
|
+
when Integer then c
|
|
432
|
+
else
|
|
433
|
+
first = c.begin || 0
|
|
434
|
+
last = c.end
|
|
435
|
+
return nil if last.nil? && ncol.nil?
|
|
436
|
+
last = ncol - 1 if last.nil?
|
|
437
|
+
last -= 1 if c.end && c.exclude_end?
|
|
438
|
+
(first..last).to_a
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
dup = indexes.tally.select { |_, k| k > 1 }.keys
|
|
442
|
+
unless dup.empty?
|
|
443
|
+
raise ArgumentError, "from_csv: columns: selects column #{dup.first} more than once"
|
|
444
|
+
end
|
|
445
|
+
if ncol && (bad = indexes.find { |j| j >= ncol })
|
|
446
|
+
raise ArgumentError, "from_csv: columns: #{bad} is past the #{ncol} columns of the file"
|
|
447
|
+
end
|
|
448
|
+
indexes
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
# The indexes C is to read, ascending, when it can tell which they are
|
|
452
|
+
# before it reads; nil for every column.
|
|
453
|
+
private def columns_to_read
|
|
454
|
+
@selection && selected_indexes(@names, @names&.size)&.sort
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
private def check_column_names(names, ncol, line)
|
|
458
|
+
return if names.size == ncol
|
|
459
|
+
raise ArgumentError, "from_csv: column_names: gives #{names.size} " \
|
|
460
|
+
"name#{names.size == 1 ? '' : 's'} for the #{ncol} columns of line #{line}"
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
# Consume the remaining records as data rows. A second data adds what is
|
|
464
|
+
# left, which is nothing; it does not discard the rows already read.
|
|
465
|
+
#
|
|
466
|
+
# Without names the column count is known only once every row is read,
|
|
467
|
+
# so blank lines are kept and marked, and +result+ drops them unless the
|
|
468
|
+
# file turns out to have one column -- the same rows a header would have
|
|
469
|
+
# given.
|
|
470
|
+
#
|
|
471
|
+
# Without strip:, the rest of the input is read in C (CArray.__csv_read_body_as_string_cells__)
|
|
472
|
+
# straight into the table, with the column count of the names, or of the
|
|
473
|
+
# first record when there are none. When C declines -- text not in
|
|
474
|
+
# UTF-8, a record it does not take, or one longer than the first in a
|
|
475
|
+
# file without names -- the text from there on is read here, which pads
|
|
476
|
+
# the earlier rows or reports what is wrong.
|
|
477
|
+
def data
|
|
478
|
+
read_data(nil)
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
# The header: / data: / column_names: of from_csv, as the verbs above.
|
|
482
|
+
# They index the lines of the file from 0, as File.readlines does (an
|
|
483
|
+
# error names a line from 1, as an editor does). header: defaults to 0,
|
|
484
|
+
# or to none (false) when column_names: is given; data: is the first line
|
|
485
|
+
# of the data or a Range of lines, by default from the line after the
|
|
486
|
+
# header. A record that starts within data: is read whole.
|
|
487
|
+
def layout(header: nil, data: nil, column_names: nil, columns: nil)
|
|
488
|
+
header = column_names ? false : 0 if header.nil?
|
|
489
|
+
unless header == false || (header.is_a?(Integer) && header >= 0)
|
|
490
|
+
raise ArgumentError, "from_csv: header: takes the index of a line, from 0, " \
|
|
491
|
+
"or false (got #{header.inspect})"
|
|
492
|
+
end
|
|
493
|
+
first, last = CSVReader.data_lines(data)
|
|
494
|
+
if header
|
|
495
|
+
skip(header - @tok.lineno) if header > @tok.lineno
|
|
496
|
+
self.header
|
|
497
|
+
end
|
|
498
|
+
column_names(*column_names) if column_names
|
|
499
|
+
self.columns(*(columns.is_a?(Array) ? columns : [columns])) if columns
|
|
500
|
+
first ||= @tok.lineno
|
|
501
|
+
if first < @tok.lineno
|
|
502
|
+
raise ArgumentError, "from_csv: data: #{first} does not come after header: #{header}"
|
|
503
|
+
end
|
|
504
|
+
skip(first - @tok.lineno)
|
|
505
|
+
read_data(last && last + 1)
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# [first, last] line indexes of a data: setting, nil for an end left open.
|
|
509
|
+
def self.data_lines(spec)
|
|
510
|
+
first, last =
|
|
511
|
+
case spec
|
|
512
|
+
when nil then [nil, nil]
|
|
513
|
+
when Integer then [spec, nil]
|
|
514
|
+
when Range
|
|
515
|
+
e = spec.end
|
|
516
|
+
e -= 1 if e.is_a?(Integer) && spec.exclude_end?
|
|
517
|
+
[spec.begin, e]
|
|
518
|
+
else
|
|
519
|
+
raise ArgumentError, "from_csv: data: takes the index of a line or a Range of " \
|
|
520
|
+
"them, from 0 (got #{spec.inspect})"
|
|
521
|
+
end
|
|
522
|
+
[first, last].each do |n|
|
|
523
|
+
next if n.nil? || (n.is_a?(Integer) && n >= 0)
|
|
524
|
+
raise ArgumentError, "from_csv: data: lines are indexed from 0 (got #{spec.inspect})"
|
|
525
|
+
end
|
|
526
|
+
if first && last && last < first
|
|
527
|
+
raise ArgumentError, "from_csv: data: #{spec.inspect} ends before it starts"
|
|
528
|
+
end
|
|
529
|
+
[first, last]
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
# The records from here through those starting on +last+ (nil: the end).
|
|
533
|
+
private def read_data(last)
|
|
534
|
+
body_in_c(last) if @rows.nil? && !@tok.strip && @io.respond_to?(:read)
|
|
535
|
+
return self if @table
|
|
536
|
+
rows = (@rows ||= [])
|
|
537
|
+
@blank_rows ||= []
|
|
538
|
+
blank_is_row = @names.nil? || @names.size == 1
|
|
539
|
+
ncol = @names&.size
|
|
540
|
+
while (fields = @tok.read(@io, blank_is_row: blank_is_row, last_line: last))
|
|
541
|
+
if @names_to_check && !@tok.blank?
|
|
542
|
+
check_column_names(@names_to_check, fields.size, @tok.record_line)
|
|
543
|
+
@names_to_check = nil
|
|
544
|
+
end
|
|
545
|
+
if ncol && fields.size > ncol
|
|
546
|
+
raise @tok.malformed("#{fields.size} fields, but there are #{ncol} columns")
|
|
547
|
+
end
|
|
548
|
+
@blank_rows << rows.size if @names.nil? && @tok.blank?
|
|
199
549
|
rows << fields
|
|
200
550
|
end
|
|
201
|
-
@rows = rows
|
|
202
551
|
self
|
|
203
552
|
end
|
|
204
553
|
|
|
554
|
+
# Run the block, naming the file in a MalformedCSV it raises.
|
|
555
|
+
def reporting
|
|
556
|
+
yield
|
|
557
|
+
rescue CSVParser::MalformedCSV => e
|
|
558
|
+
e.path ||= @path
|
|
559
|
+
raise
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
# The text C reads at a time when the column count is known; a chunk
|
|
563
|
+
# ends at a line end, and not inside a quoted field.
|
|
564
|
+
CHUNK_BYTES = 1 << 22
|
|
565
|
+
|
|
566
|
+
# With names, the input goes to C a chunk at a time, so the text is not
|
|
567
|
+
# held whole beside its cells. Without them, the column count and which
|
|
568
|
+
# blank lines are rows are known only at the end, so it goes in whole;
|
|
569
|
+
# so it does when the IO transcodes, since read(length) does not.
|
|
570
|
+
#
|
|
571
|
+
# With read_text_as_const_string:, the body goes to C whole, into one
|
|
572
|
+
# CAConstString per column over the text itself: the text is the buffer
|
|
573
|
+
# the columns read, so it is held whole either way. When C declines, the
|
|
574
|
+
# text is read here from the start of the body.
|
|
575
|
+
private def read_body_into_const_string_columns
|
|
576
|
+
ncol = @names && !@names_to_check ? @names.size : 0
|
|
577
|
+
keep = columns_to_read
|
|
578
|
+
text = @io.read || ""
|
|
579
|
+
n, buffer, pairs, records = CArray.__csv_read_body_as_const_string_columns__(text, @tok.sep, @tok.quote, ncol, keep)
|
|
580
|
+
unless buffer
|
|
581
|
+
@rows = []
|
|
582
|
+
@io = StringIO.new(text)
|
|
583
|
+
return
|
|
584
|
+
end
|
|
585
|
+
if @names_to_check
|
|
586
|
+
check_column_names(@names_to_check, n, @tok.lineno + first_record_line(text))
|
|
587
|
+
@names_to_check = nil
|
|
588
|
+
end
|
|
589
|
+
@tok.advance(records, text.count("\n") + (text.empty? || text.end_with?("\n") ? 0 : 1))
|
|
590
|
+
@table = ConstStringColumns.new(pairs.map { |pr| CAConstString.wrap(pr, buffer: buffer) })
|
|
591
|
+
@table_ncol = n
|
|
592
|
+
@table_keep = keep
|
|
593
|
+
@rows = []
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
# The columns read_body_into_const_string_columns made, as +result+
|
|
597
|
+
# hands them to CAFrame.from_csv.
|
|
598
|
+
ConstStringColumns = Struct.new(:columns)
|
|
599
|
+
|
|
600
|
+
private def body_in_c(last = nil, chunk_bytes = CHUNK_BYTES)
|
|
601
|
+
return read_body_into_const_string_columns if last.nil? && @read_text_as_const_string
|
|
602
|
+
chunked = last.nil? && @names && @io.respond_to?(:external_encoding) &&
|
|
603
|
+
@io.internal_encoding.nil?
|
|
604
|
+
# Names that have to agree with the first record leave its column
|
|
605
|
+
# count to C, to be checked against them.
|
|
606
|
+
ncol = @names && !@names_to_check ? @names.size : 0
|
|
607
|
+
keep = columns_to_read
|
|
608
|
+
cells = []
|
|
609
|
+
loop do
|
|
610
|
+
text = if chunked then next_chunk(chunk_bytes)
|
|
611
|
+
elsif last then lines_through(last)
|
|
612
|
+
else @io.read || ""
|
|
613
|
+
end
|
|
614
|
+
break if text.nil?
|
|
615
|
+
n, flat, records = CArray.__csv_read_body_as_string_cells__(text, @tok.sep, @tok.quote, ncol, keep)
|
|
616
|
+
unless flat
|
|
617
|
+
# This chunk and the rest are read by the Ruby tokenizer, after the
|
|
618
|
+
# rows read so far.
|
|
619
|
+
if keep && ncol > 0 && !cells.empty?
|
|
620
|
+
# The rows read so far hold the selected fields only; spread
|
|
621
|
+
# them back to their columns for the Ruby reader.
|
|
622
|
+
@rows = cells.each_slice(keep.size).map do |row|
|
|
623
|
+
full = Array.new(ncol)
|
|
624
|
+
keep.each_with_index { |j, k| full[j] = UNDEF.equal?(row[k]) ? nil : row[k] }
|
|
625
|
+
full
|
|
626
|
+
end
|
|
627
|
+
else
|
|
628
|
+
@rows = ncol > 0 ? cells.each_slice(ncol).to_a : []
|
|
629
|
+
end
|
|
630
|
+
@io = StringIO.new(chunked ? text + (@io.read || "") : text)
|
|
631
|
+
return
|
|
632
|
+
end
|
|
633
|
+
if @names_to_check
|
|
634
|
+
check_column_names(@names_to_check, n, @tok.lineno + first_record_line(text))
|
|
635
|
+
@names_to_check = nil
|
|
636
|
+
end
|
|
637
|
+
ncol = n
|
|
638
|
+
cells.concat(flat)
|
|
639
|
+
@tok.advance(records, text.count("\n") + (text.empty? || text.end_with?("\n") ? 0 : 1))
|
|
640
|
+
break unless chunked
|
|
641
|
+
end
|
|
642
|
+
if ncol == 0 # no record, for the names to be checked against
|
|
643
|
+
@rows = []
|
|
644
|
+
return
|
|
645
|
+
end
|
|
646
|
+
width = keep ? keep.size : ncol
|
|
647
|
+
@table = CArray.object(cells.size / width, width) { cells }
|
|
648
|
+
@table_ncol = ncol
|
|
649
|
+
@table_keep = keep
|
|
650
|
+
@rows = []
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
# The line, from 1 within +text+, of its first record: past the blank
|
|
654
|
+
# lines before it, as C reads them.
|
|
655
|
+
private def first_record_line(text)
|
|
656
|
+
line = 1
|
|
657
|
+
text.each_line do |l|
|
|
658
|
+
rec = l.chomp
|
|
659
|
+
break unless rec.match?(/\A[ \t]*\z/) && !rec.include?(@tok.sep)
|
|
660
|
+
line += 1
|
|
661
|
+
end
|
|
662
|
+
line
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
# The lines through +last+, read on past any quoted field still open.
|
|
666
|
+
private def lines_through(last)
|
|
667
|
+
buf = +""
|
|
668
|
+
(last - @tok.lineno).times do
|
|
669
|
+
line = @io.gets or break
|
|
670
|
+
buf << line
|
|
671
|
+
end
|
|
672
|
+
while buf.count(@tok.quote).odd? && (more = @io.gets)
|
|
673
|
+
buf << more
|
|
674
|
+
end
|
|
675
|
+
buf
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
# The next chunk_bytes of the input, read on to the end of its line and
|
|
679
|
+
# past any quoted field still open, in the IO's encoding; nil at EOF.
|
|
680
|
+
private def next_chunk(chunk_bytes)
|
|
681
|
+
buf = @io.read(chunk_bytes)
|
|
682
|
+
return nil if buf.nil?
|
|
683
|
+
buf.force_encoding(@io.external_encoding || Encoding.default_external)
|
|
684
|
+
if !buf.end_with?("\n") && (rest = @io.gets)
|
|
685
|
+
buf << rest
|
|
686
|
+
end
|
|
687
|
+
while buf.count(@tok.quote).odd? && (more = @io.gets)
|
|
688
|
+
buf << more
|
|
689
|
+
end
|
|
690
|
+
buf
|
|
691
|
+
end
|
|
692
|
+
|
|
205
693
|
# [names_or_nil, rows] for CAFrame.from_csv to build from. names is nil when
|
|
206
694
|
# neither header nor column_names ran (positional names are generated).
|
|
207
695
|
def result
|
|
208
|
-
|
|
696
|
+
return table_result if @table
|
|
697
|
+
rows = @rows || []
|
|
698
|
+
if @names.nil? && @blank_rows && !@blank_rows.empty? &&
|
|
699
|
+
rows.map(&:size).max != 1
|
|
700
|
+
drop = @blank_rows.to_h { |i| [i, true] }
|
|
701
|
+
rows = rows.reject.with_index { |_, i| drop[i] }
|
|
702
|
+
end
|
|
703
|
+
return [@names, rows] unless @selection
|
|
704
|
+
ncol = @names ? @names.size : (rows.map(&:size).max || 0)
|
|
705
|
+
indexes = selected_indexes(@names, ncol)
|
|
706
|
+
names = indexes.map { |j| @names ? @names[j] : "c#{j}" }
|
|
707
|
+
[names, rows.map { |r| r.values_at(*indexes) }]
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
# The table C read, its columns those selected, in the order asked for.
|
|
711
|
+
private def table_result
|
|
712
|
+
return [@names, @table] unless @selection
|
|
713
|
+
indexes = selected_indexes(@names, @table_ncol)
|
|
714
|
+
names = indexes.map { |j| @names ? @names[j] : "c#{j}" }
|
|
715
|
+
kept = @table_keep || (0...@table_ncol).to_a
|
|
716
|
+
order = indexes.map { |j| kept.index(j) }
|
|
717
|
+
table =
|
|
718
|
+
if order == (0...kept.size).to_a then @table
|
|
719
|
+
elsif @table.is_a?(ConstStringColumns) then ConstStringColumns.new(@table.columns.values_at(*order))
|
|
720
|
+
else @table[nil, CA_INT64(order)]
|
|
721
|
+
end
|
|
722
|
+
[names, table]
|
|
209
723
|
end
|
|
210
724
|
end
|
|
211
725
|
end
|