csv 3.1.9 → 3.2.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/NEWS.md +130 -0
- data/README.md +3 -6
- data/doc/csv/options/generating/write_headers.rdoc +1 -1
- data/doc/csv/recipes/generating.rdoc +1 -1
- data/doc/csv/recipes/parsing.rdoc +3 -3
- data/lib/csv/fields_converter.rb +6 -2
- data/lib/csv/input_record_separator.rb +18 -0
- data/lib/csv/parser.rb +202 -65
- data/lib/csv/row.rb +22 -0
- data/lib/csv/table.rb +17 -5
- data/lib/csv/version.rb +1 -1
- data/lib/csv/writer.rb +2 -1
- data/lib/csv.rb +309 -152
- metadata +7 -6
data/lib/csv/parser.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
require "strscan"
|
4
4
|
|
5
5
|
require_relative "delete_suffix"
|
6
|
+
require_relative "input_record_separator"
|
6
7
|
require_relative "match_p"
|
7
8
|
require_relative "row"
|
8
9
|
require_relative "table"
|
@@ -26,6 +27,10 @@ class CSV
|
|
26
27
|
class InvalidEncoding < StandardError
|
27
28
|
end
|
28
29
|
|
30
|
+
# Raised when unexpected case is happen.
|
31
|
+
class UnexpectedError < StandardError
|
32
|
+
end
|
33
|
+
|
29
34
|
#
|
30
35
|
# CSV::Scanner receives a CSV output, scans it and return the content.
|
31
36
|
# It also controls the life cycle of the object with its methods +keep_start+,
|
@@ -77,16 +82,17 @@ class CSV
|
|
77
82
|
# +keep_end+, +keep_back+, +keep_drop+.
|
78
83
|
#
|
79
84
|
# CSV::InputsScanner.scan() tries to match with pattern at the current position.
|
80
|
-
# If there's a match, the scanner advances the
|
85
|
+
# If there's a match, the scanner advances the "scan pointer" and returns the matched string.
|
81
86
|
# Otherwise, the scanner returns nil.
|
82
87
|
#
|
83
|
-
# CSV::InputsScanner.rest() returns the
|
88
|
+
# CSV::InputsScanner.rest() returns the "rest" of the string (i.e. everything after the scan pointer).
|
84
89
|
# If there is no more data (eos? = true), it returns "".
|
85
90
|
#
|
86
91
|
class InputsScanner
|
87
|
-
def initialize(inputs, encoding, chunk_size: 8192)
|
92
|
+
def initialize(inputs, encoding, row_separator, chunk_size: 8192)
|
88
93
|
@inputs = inputs.dup
|
89
94
|
@encoding = encoding
|
95
|
+
@row_separator = row_separator
|
90
96
|
@chunk_size = chunk_size
|
91
97
|
@last_scanner = @inputs.empty?
|
92
98
|
@keeps = []
|
@@ -94,11 +100,13 @@ class CSV
|
|
94
100
|
end
|
95
101
|
|
96
102
|
def each_line(row_separator)
|
103
|
+
return enum_for(__method__, row_separator) unless block_given?
|
97
104
|
buffer = nil
|
98
105
|
input = @scanner.rest
|
99
106
|
position = @scanner.pos
|
100
107
|
offset = 0
|
101
108
|
n_row_separator_chars = row_separator.size
|
109
|
+
# trace(__method__, :start, line, input)
|
102
110
|
while true
|
103
111
|
input.each_line(row_separator) do |line|
|
104
112
|
@scanner.pos += line.bytesize
|
@@ -138,25 +146,28 @@ class CSV
|
|
138
146
|
end
|
139
147
|
|
140
148
|
def scan(pattern)
|
149
|
+
# trace(__method__, pattern, :start)
|
141
150
|
value = @scanner.scan(pattern)
|
151
|
+
# trace(__method__, pattern, :done, :last, value) if @last_scanner
|
142
152
|
return value if @last_scanner
|
143
153
|
|
144
|
-
if value
|
145
|
-
|
146
|
-
|
147
|
-
else
|
148
|
-
nil
|
149
|
-
end
|
154
|
+
read_chunk if value and @scanner.eos?
|
155
|
+
# trace(__method__, pattern, :done, value)
|
156
|
+
value
|
150
157
|
end
|
151
158
|
|
152
159
|
def scan_all(pattern)
|
160
|
+
# trace(__method__, pattern, :start)
|
153
161
|
value = @scanner.scan(pattern)
|
162
|
+
# trace(__method__, pattern, :done, :last, value) if @last_scanner
|
154
163
|
return value if @last_scanner
|
155
164
|
|
156
165
|
return nil if value.nil?
|
157
166
|
while @scanner.eos? and read_chunk and (sub_value = @scanner.scan(pattern))
|
167
|
+
# trace(__method__, pattern, :sub, sub_value)
|
158
168
|
value << sub_value
|
159
169
|
end
|
170
|
+
# trace(__method__, pattern, :done, value)
|
160
171
|
value
|
161
172
|
end
|
162
173
|
|
@@ -165,76 +176,135 @@ class CSV
|
|
165
176
|
end
|
166
177
|
|
167
178
|
def keep_start
|
168
|
-
|
179
|
+
# trace(__method__, :start)
|
180
|
+
adjust_last_keep
|
181
|
+
@keeps.push([@scanner, @scanner.pos, nil])
|
182
|
+
# trace(__method__, :done)
|
169
183
|
end
|
170
184
|
|
171
185
|
def keep_end
|
172
|
-
|
173
|
-
|
186
|
+
# trace(__method__, :start)
|
187
|
+
scanner, start, buffer = @keeps.pop
|
188
|
+
if scanner == @scanner
|
189
|
+
keep = @scanner.string.byteslice(start, @scanner.pos - start)
|
190
|
+
else
|
191
|
+
keep = @scanner.string.byteslice(0, @scanner.pos)
|
192
|
+
end
|
174
193
|
if buffer
|
175
194
|
buffer << keep
|
176
195
|
keep = buffer
|
177
196
|
end
|
197
|
+
# trace(__method__, :done, keep)
|
178
198
|
keep
|
179
199
|
end
|
180
200
|
|
181
201
|
def keep_back
|
182
|
-
|
202
|
+
# trace(__method__, :start)
|
203
|
+
scanner, start, buffer = @keeps.pop
|
183
204
|
if buffer
|
205
|
+
# trace(__method__, :rescan, start, buffer)
|
184
206
|
string = @scanner.string
|
185
|
-
|
207
|
+
if scanner == @scanner
|
208
|
+
keep = string.byteslice(start, string.bytesize - start)
|
209
|
+
else
|
210
|
+
keep = string
|
211
|
+
end
|
186
212
|
if keep and not keep.empty?
|
187
213
|
@inputs.unshift(StringIO.new(keep))
|
188
214
|
@last_scanner = false
|
189
215
|
end
|
190
216
|
@scanner = StringScanner.new(buffer)
|
191
217
|
else
|
218
|
+
if @scanner != scanner
|
219
|
+
message = "scanners are different but no buffer: "
|
220
|
+
message += "#{@scanner.inspect}(#{@scanner.object_id}): "
|
221
|
+
message += "#{scanner.inspect}(#{scanner.object_id})"
|
222
|
+
raise UnexpectedError, message
|
223
|
+
end
|
224
|
+
# trace(__method__, :repos, start, buffer)
|
192
225
|
@scanner.pos = start
|
193
226
|
end
|
194
227
|
read_chunk if @scanner.eos?
|
195
228
|
end
|
196
229
|
|
197
230
|
def keep_drop
|
198
|
-
@keeps.pop
|
231
|
+
_, _, buffer = @keeps.pop
|
232
|
+
# trace(__method__, :done, :empty) unless buffer
|
233
|
+
return unless buffer
|
234
|
+
|
235
|
+
last_keep = @keeps.last
|
236
|
+
# trace(__method__, :done, :no_last_keep) unless last_keep
|
237
|
+
return unless last_keep
|
238
|
+
|
239
|
+
if last_keep[2]
|
240
|
+
last_keep[2] << buffer
|
241
|
+
else
|
242
|
+
last_keep[2] = buffer
|
243
|
+
end
|
244
|
+
# trace(__method__, :done)
|
199
245
|
end
|
200
246
|
|
201
247
|
def rest
|
202
248
|
@scanner.rest
|
203
249
|
end
|
204
250
|
|
251
|
+
def check(pattern)
|
252
|
+
@scanner.check(pattern)
|
253
|
+
end
|
254
|
+
|
205
255
|
private
|
206
|
-
def
|
207
|
-
|
256
|
+
def trace(*args)
|
257
|
+
pp([*args, @scanner, @scanner&.string, @scanner&.pos, @keeps])
|
258
|
+
end
|
208
259
|
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
260
|
+
def adjust_last_keep
|
261
|
+
# trace(__method__, :start)
|
262
|
+
|
263
|
+
keep = @keeps.last
|
264
|
+
# trace(__method__, :done, :empty) if keep.nil?
|
265
|
+
return if keep.nil?
|
266
|
+
|
267
|
+
scanner, start, buffer = keep
|
268
|
+
string = @scanner.string
|
269
|
+
if @scanner != scanner
|
270
|
+
start = 0
|
271
|
+
end
|
272
|
+
if start == 0 and @scanner.eos?
|
273
|
+
keep_data = string
|
274
|
+
else
|
275
|
+
keep_data = string.byteslice(start, @scanner.pos - start)
|
276
|
+
end
|
277
|
+
if keep_data
|
278
|
+
if buffer
|
279
|
+
buffer << keep_data
|
280
|
+
else
|
281
|
+
keep[2] = keep_data.dup
|
221
282
|
end
|
222
|
-
keep[0] = 0
|
223
283
|
end
|
224
284
|
|
285
|
+
# trace(__method__, :done)
|
286
|
+
end
|
287
|
+
|
288
|
+
def read_chunk
|
289
|
+
return false if @last_scanner
|
290
|
+
|
291
|
+
adjust_last_keep
|
292
|
+
|
225
293
|
input = @inputs.first
|
226
294
|
case input
|
227
295
|
when StringIO
|
228
296
|
string = input.read
|
229
297
|
raise InvalidEncoding unless string.valid_encoding?
|
298
|
+
# trace(__method__, :stringio, string)
|
230
299
|
@scanner = StringScanner.new(string)
|
231
300
|
@inputs.shift
|
232
301
|
@last_scanner = @inputs.empty?
|
233
302
|
true
|
234
303
|
else
|
235
|
-
chunk = input.gets(
|
304
|
+
chunk = input.gets(@row_separator, @chunk_size)
|
236
305
|
if chunk
|
237
306
|
raise InvalidEncoding unless chunk.valid_encoding?
|
307
|
+
# trace(__method__, :chunk, chunk)
|
238
308
|
@scanner = StringScanner.new(chunk)
|
239
309
|
if input.respond_to?(:eof?) and input.eof?
|
240
310
|
@inputs.shift
|
@@ -242,6 +312,7 @@ class CSV
|
|
242
312
|
end
|
243
313
|
true
|
244
314
|
else
|
315
|
+
# trace(__method__, :no_chunk)
|
245
316
|
@scanner = StringScanner.new("".encode(@encoding))
|
246
317
|
@inputs.shift
|
247
318
|
@last_scanner = @inputs.empty?
|
@@ -276,7 +347,11 @@ class CSV
|
|
276
347
|
end
|
277
348
|
|
278
349
|
def field_size_limit
|
279
|
-
@
|
350
|
+
@max_field_size&.succ
|
351
|
+
end
|
352
|
+
|
353
|
+
def max_field_size
|
354
|
+
@max_field_size
|
280
355
|
end
|
281
356
|
|
282
357
|
def skip_lines
|
@@ -344,6 +419,16 @@ class CSV
|
|
344
419
|
end
|
345
420
|
message = "Invalid byte sequence in #{@encoding}"
|
346
421
|
raise MalformedCSVError.new(message, lineno)
|
422
|
+
rescue UnexpectedError => error
|
423
|
+
if @scanner
|
424
|
+
ignore_broken_line
|
425
|
+
lineno = @lineno
|
426
|
+
else
|
427
|
+
lineno = @lineno + 1
|
428
|
+
end
|
429
|
+
message = "This should not be happen: #{error.message}: "
|
430
|
+
message += "Please report this to https://github.com/ruby/csv/issues"
|
431
|
+
raise MalformedCSVError.new(message, lineno)
|
347
432
|
end
|
348
433
|
end
|
349
434
|
|
@@ -360,6 +445,7 @@ class CSV
|
|
360
445
|
prepare_skip_lines
|
361
446
|
prepare_strip
|
362
447
|
prepare_separators
|
448
|
+
validate_strip_and_col_sep_options
|
363
449
|
prepare_quoted
|
364
450
|
prepare_unquoted
|
365
451
|
prepare_line
|
@@ -387,7 +473,7 @@ class CSV
|
|
387
473
|
@backslash_quote = false
|
388
474
|
end
|
389
475
|
@unconverted_fields = @options[:unconverted_fields]
|
390
|
-
@
|
476
|
+
@max_field_size = @options[:max_field_size]
|
391
477
|
@skip_blanks = @options[:skip_blanks]
|
392
478
|
@fields_converter = @options[:fields_converter]
|
393
479
|
@header_fields_converter = @options[:header_fields_converter]
|
@@ -479,9 +565,9 @@ class CSV
|
|
479
565
|
begin
|
480
566
|
StringScanner.new("x").scan("x")
|
481
567
|
rescue TypeError
|
482
|
-
|
568
|
+
STRING_SCANNER_SCAN_ACCEPT_STRING = false
|
483
569
|
else
|
484
|
-
|
570
|
+
STRING_SCANNER_SCAN_ACCEPT_STRING = true
|
485
571
|
end
|
486
572
|
|
487
573
|
def prepare_separators
|
@@ -505,7 +591,7 @@ class CSV
|
|
505
591
|
@first_column_separators = Regexp.new(@escaped_first_column_separator +
|
506
592
|
"+".encode(@encoding))
|
507
593
|
else
|
508
|
-
if
|
594
|
+
if STRING_SCANNER_SCAN_ACCEPT_STRING
|
509
595
|
@column_end = @column_separator
|
510
596
|
else
|
511
597
|
@column_end = Regexp.new(@escaped_column_separator)
|
@@ -526,10 +612,32 @@ class CSV
|
|
526
612
|
|
527
613
|
@cr = "\r".encode(@encoding)
|
528
614
|
@lf = "\n".encode(@encoding)
|
529
|
-
@
|
615
|
+
@line_end = Regexp.new("\r\n|\n|\r".encode(@encoding))
|
530
616
|
@not_line_end = Regexp.new("[^\r\n]+".encode(@encoding))
|
531
617
|
end
|
532
618
|
|
619
|
+
# This method verifies that there are no (obvious) ambiguities with the
|
620
|
+
# provided +col_sep+ and +strip+ parsing options. For example, if +col_sep+
|
621
|
+
# and +strip+ were both equal to +\t+, then there would be no clear way to
|
622
|
+
# parse the input.
|
623
|
+
def validate_strip_and_col_sep_options
|
624
|
+
return unless @strip
|
625
|
+
|
626
|
+
if @strip.is_a?(String)
|
627
|
+
if @column_separator.start_with?(@strip) || @column_separator.end_with?(@strip)
|
628
|
+
raise ArgumentError,
|
629
|
+
"The provided strip (#{@escaped_strip}) and " \
|
630
|
+
"col_sep (#{@escaped_column_separator}) options are incompatible."
|
631
|
+
end
|
632
|
+
else
|
633
|
+
if Regexp.new("\\A[#{@escaped_strip}]|[#{@escaped_strip}]\\z").match?(@column_separator)
|
634
|
+
raise ArgumentError,
|
635
|
+
"The provided strip (true) and " \
|
636
|
+
"col_sep (#{@escaped_column_separator}) options are incompatible."
|
637
|
+
end
|
638
|
+
end
|
639
|
+
end
|
640
|
+
|
533
641
|
def prepare_quoted
|
534
642
|
if @quote_character
|
535
643
|
@quotes = Regexp.new(@escaped_quote_character +
|
@@ -605,7 +713,7 @@ class CSV
|
|
605
713
|
# do nothing: ensure will set default
|
606
714
|
end
|
607
715
|
end
|
608
|
-
separator =
|
716
|
+
separator = InputRecordSeparator.value if separator == :auto
|
609
717
|
end
|
610
718
|
separator.to_s.encode(@encoding)
|
611
719
|
end
|
@@ -704,26 +812,28 @@ class CSV
|
|
704
812
|
sample[0, 128].index(@quote_character)
|
705
813
|
end
|
706
814
|
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
@io = StringIO.new(string, "rb:#{string.encoding}")
|
712
|
-
end
|
815
|
+
class UnoptimizedStringIO # :nodoc:
|
816
|
+
def initialize(string)
|
817
|
+
@io = StringIO.new(string, "rb:#{string.encoding}")
|
818
|
+
end
|
713
819
|
|
714
|
-
|
715
|
-
|
716
|
-
|
820
|
+
def gets(*args)
|
821
|
+
@io.gets(*args)
|
822
|
+
end
|
717
823
|
|
718
|
-
|
719
|
-
|
720
|
-
|
824
|
+
def each_line(*args, &block)
|
825
|
+
@io.each_line(*args, &block)
|
826
|
+
end
|
721
827
|
|
722
|
-
|
723
|
-
|
724
|
-
end
|
828
|
+
def eof?
|
829
|
+
@io.eof?
|
725
830
|
end
|
831
|
+
end
|
726
832
|
|
833
|
+
SCANNER_TEST = (ENV["CSV_PARSER_SCANNER_TEST"] == "yes")
|
834
|
+
if SCANNER_TEST
|
835
|
+
SCANNER_TEST_CHUNK_SIZE_NAME = "CSV_PARSER_SCANNER_TEST_CHUNK_SIZE"
|
836
|
+
SCANNER_TEST_CHUNK_SIZE_VALUE = ENV[SCANNER_TEST_CHUNK_SIZE_NAME]
|
727
837
|
def build_scanner
|
728
838
|
inputs = @samples.collect do |sample|
|
729
839
|
UnoptimizedStringIO.new(sample)
|
@@ -733,17 +843,27 @@ class CSV
|
|
733
843
|
else
|
734
844
|
inputs << @input
|
735
845
|
end
|
736
|
-
|
846
|
+
begin
|
847
|
+
chunk_size_value = ENV[SCANNER_TEST_CHUNK_SIZE_NAME]
|
848
|
+
rescue # Ractor::IsolationError
|
849
|
+
# Ractor on Ruby 3.0 can't read ENV value.
|
850
|
+
chunk_size_value = SCANNER_TEST_CHUNK_SIZE_VALUE
|
851
|
+
end
|
852
|
+
chunk_size = Integer((chunk_size_value || "1"), 10)
|
737
853
|
InputsScanner.new(inputs,
|
738
854
|
@encoding,
|
739
|
-
|
855
|
+
@row_separator,
|
856
|
+
chunk_size: chunk_size)
|
740
857
|
end
|
741
858
|
else
|
742
859
|
def build_scanner
|
743
860
|
string = nil
|
744
861
|
if @samples.empty? and @input.is_a?(StringIO)
|
745
862
|
string = @input.read
|
746
|
-
elsif @samples.size == 1 and
|
863
|
+
elsif @samples.size == 1 and
|
864
|
+
@input != ARGF and
|
865
|
+
@input.respond_to?(:eof?) and
|
866
|
+
@input.eof?
|
747
867
|
string = @samples[0]
|
748
868
|
end
|
749
869
|
if string
|
@@ -762,7 +882,7 @@ class CSV
|
|
762
882
|
StringIO.new(sample)
|
763
883
|
end
|
764
884
|
inputs << @input
|
765
|
-
InputsScanner.new(inputs, @encoding)
|
885
|
+
InputsScanner.new(inputs, @encoding, @row_separator)
|
766
886
|
end
|
767
887
|
end
|
768
888
|
end
|
@@ -796,6 +916,14 @@ class CSV
|
|
796
916
|
end
|
797
917
|
end
|
798
918
|
|
919
|
+
def validate_field_size(field)
|
920
|
+
return unless @max_field_size
|
921
|
+
return if field.size <= @max_field_size
|
922
|
+
ignore_broken_line
|
923
|
+
message = "Field size exceeded: #{field.size} > #{@max_field_size}"
|
924
|
+
raise MalformedCSVError.new(message, @lineno)
|
925
|
+
end
|
926
|
+
|
799
927
|
def parse_no_quote(&block)
|
800
928
|
@scanner.each_line(@row_separator) do |line|
|
801
929
|
next if @skip_lines and skip_line?(line)
|
@@ -808,6 +936,11 @@ class CSV
|
|
808
936
|
else
|
809
937
|
line = strip_value(line)
|
810
938
|
row = line.split(@split_column_separator, -1)
|
939
|
+
if @max_field_size
|
940
|
+
row.each do |column|
|
941
|
+
validate_field_size(column)
|
942
|
+
end
|
943
|
+
end
|
811
944
|
n_columns = row.size
|
812
945
|
i = 0
|
813
946
|
while i < n_columns
|
@@ -863,6 +996,7 @@ class CSV
|
|
863
996
|
@need_robust_parsing = true
|
864
997
|
return parse_quotable_robust(&block)
|
865
998
|
end
|
999
|
+
validate_field_size(row[i])
|
866
1000
|
end
|
867
1001
|
i += 1
|
868
1002
|
end
|
@@ -886,10 +1020,7 @@ class CSV
|
|
886
1020
|
value = parse_column_value
|
887
1021
|
if value
|
888
1022
|
@scanner.scan_all(@strip_value) if @strip_value
|
889
|
-
|
890
|
-
ignore_broken_line
|
891
|
-
raise MalformedCSVError.new("Field size exceeded", @lineno)
|
892
|
-
end
|
1023
|
+
validate_field_size(value)
|
893
1024
|
end
|
894
1025
|
if parse_column_end
|
895
1026
|
row << value
|
@@ -910,11 +1041,17 @@ class CSV
|
|
910
1041
|
break
|
911
1042
|
else
|
912
1043
|
if @quoted_column_value
|
1044
|
+
if liberal_parsing? and (new_line = @scanner.check(@line_end))
|
1045
|
+
message =
|
1046
|
+
"Illegal end-of-line sequence outside of a quoted field " +
|
1047
|
+
"<#{new_line.inspect}>"
|
1048
|
+
else
|
1049
|
+
message = "Any value after quoted field isn't allowed"
|
1050
|
+
end
|
913
1051
|
ignore_broken_line
|
914
|
-
message = "Any value after quoted field isn't allowed"
|
915
1052
|
raise MalformedCSVError.new(message, @lineno)
|
916
1053
|
elsif @unquoted_column_value and
|
917
|
-
(new_line = @scanner.scan(@
|
1054
|
+
(new_line = @scanner.scan(@line_end))
|
918
1055
|
ignore_broken_line
|
919
1056
|
message = "Unquoted fields do not allow new line " +
|
920
1057
|
"<#{new_line.inspect}>"
|
@@ -923,7 +1060,7 @@ class CSV
|
|
923
1060
|
ignore_broken_line
|
924
1061
|
message = "Illegal quoting"
|
925
1062
|
raise MalformedCSVError.new(message, @lineno)
|
926
|
-
elsif (new_line = @scanner.scan(@
|
1063
|
+
elsif (new_line = @scanner.scan(@line_end))
|
927
1064
|
ignore_broken_line
|
928
1065
|
message = "New line must be <#{@row_separator.inspect}> " +
|
929
1066
|
"not <#{new_line.inspect}>"
|
@@ -1089,7 +1226,7 @@ class CSV
|
|
1089
1226
|
|
1090
1227
|
def ignore_broken_line
|
1091
1228
|
@scanner.scan_all(@not_line_end)
|
1092
|
-
@scanner.scan_all(@
|
1229
|
+
@scanner.scan_all(@line_end)
|
1093
1230
|
@lineno += 1
|
1094
1231
|
end
|
1095
1232
|
|
data/lib/csv/row.rb
CHANGED
@@ -659,8 +659,30 @@ class CSV
|
|
659
659
|
end
|
660
660
|
alias_method :to_hash, :to_h
|
661
661
|
|
662
|
+
# :call-seq:
|
663
|
+
# row.deconstruct_keys(keys) -> hash
|
664
|
+
#
|
665
|
+
# Returns the new \Hash suitable for pattern matching containing only the
|
666
|
+
# keys specified as an argument.
|
667
|
+
def deconstruct_keys(keys)
|
668
|
+
if keys.nil?
|
669
|
+
to_h
|
670
|
+
else
|
671
|
+
keys.to_h { |key| [key, self[key]] }
|
672
|
+
end
|
673
|
+
end
|
674
|
+
|
662
675
|
alias_method :to_ary, :to_a
|
663
676
|
|
677
|
+
# :call-seq:
|
678
|
+
# row.deconstruct -> array
|
679
|
+
#
|
680
|
+
# Returns the new \Array suitable for pattern matching containing the values
|
681
|
+
# of the row.
|
682
|
+
def deconstruct
|
683
|
+
fields
|
684
|
+
end
|
685
|
+
|
664
686
|
# :call-seq:
|
665
687
|
# row.to_csv -> csv_string
|
666
688
|
#
|
data/lib/csv/table.rb
CHANGED
@@ -932,7 +932,9 @@ class CSV
|
|
932
932
|
return enum_for(__method__) { @mode == :col ? headers.size : size } unless block_given?
|
933
933
|
|
934
934
|
if @mode == :col
|
935
|
-
headers.each
|
935
|
+
headers.each.with_index do |header, i|
|
936
|
+
yield([header, @table.map {|row| row[header, i]}])
|
937
|
+
end
|
936
938
|
else
|
937
939
|
@table.each(&block)
|
938
940
|
end
|
@@ -997,9 +999,15 @@ class CSV
|
|
997
999
|
# Omits the headers if option +write_headers+ is given as +false+
|
998
1000
|
# (see {Option +write_headers+}[../CSV.html#class-CSV-label-Option+write_headers]):
|
999
1001
|
# table.to_csv(write_headers: false) # => "foo,0\nbar,1\nbaz,2\n"
|
1000
|
-
|
1002
|
+
#
|
1003
|
+
# Limit rows if option +limit+ is given like +2+:
|
1004
|
+
# table.to_csv(limit: 2) # => "Name,Value\nfoo,0\nbar,1\n"
|
1005
|
+
def to_csv(write_headers: true, limit: nil, **options)
|
1001
1006
|
array = write_headers ? [headers.to_csv(**options)] : []
|
1002
|
-
@table.
|
1007
|
+
limit ||= @table.size
|
1008
|
+
limit = @table.size + 1 + limit if limit < 0
|
1009
|
+
limit = 0 if limit < 0
|
1010
|
+
@table.first(limit).each do |row|
|
1003
1011
|
array.push(row.fields.to_csv(**options)) unless row.header_row?
|
1004
1012
|
end
|
1005
1013
|
|
@@ -1036,9 +1044,13 @@ class CSV
|
|
1036
1044
|
# Example:
|
1037
1045
|
# source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
1038
1046
|
# table = CSV.parse(source, headers: true)
|
1039
|
-
# table.inspect # => "#<CSV::Table mode:col_or_row row_count:4
|
1047
|
+
# table.inspect # => "#<CSV::Table mode:col_or_row row_count:4>\nName,Value\nfoo,0\nbar,1\nbaz,2\n"
|
1048
|
+
#
|
1040
1049
|
def inspect
|
1041
|
-
"#<#{self.class} mode:#{@mode} row_count:#{to_a.size}>"
|
1050
|
+
inspected = +"#<#{self.class} mode:#{@mode} row_count:#{to_a.size}>"
|
1051
|
+
summary = to_csv(limit: 5)
|
1052
|
+
inspected << "\n" << summary if summary.encoding.ascii_compatible?
|
1053
|
+
inspected
|
1042
1054
|
end
|
1043
1055
|
end
|
1044
1056
|
end
|
data/lib/csv/version.rb
CHANGED
data/lib/csv/writer.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "input_record_separator"
|
3
4
|
require_relative "match_p"
|
4
5
|
require_relative "row"
|
5
6
|
|
@@ -133,7 +134,7 @@ class CSV
|
|
133
134
|
@column_separator = @options[:column_separator].to_s.encode(@encoding)
|
134
135
|
row_separator = @options[:row_separator]
|
135
136
|
if row_separator == :auto
|
136
|
-
@row_separator =
|
137
|
+
@row_separator = InputRecordSeparator.value.encode(@encoding)
|
137
138
|
else
|
138
139
|
@row_separator = row_separator.to_s.encode(@encoding)
|
139
140
|
end
|