csv 3.2.2 → 3.3.5
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 +324 -5
- data/README.md +2 -2
- data/doc/csv/options/generating/write_headers.rdoc +1 -1
- data/doc/csv/options/parsing/liberal_parsing.rdoc +21 -2
- data/doc/csv/recipes/filtering.rdoc +85 -17
- data/doc/csv/recipes/generating.rdoc +2 -2
- data/doc/csv/recipes/parsing.rdoc +14 -5
- data/lib/csv/core_ext/array.rb +1 -1
- data/lib/csv/core_ext/string.rb +1 -1
- data/lib/csv/fields_converter.rb +10 -2
- data/lib/csv/input_record_separator.rb +1 -14
- data/lib/csv/parser.rb +238 -108
- data/lib/csv/row.rb +1 -1
- data/lib/csv/table.rb +15 -6
- data/lib/csv/version.rb +1 -1
- data/lib/csv/writer.rb +4 -5
- data/lib/csv.rb +256 -44
- metadata +7 -67
- data/lib/csv/delete_suffix.rb +0 -18
- data/lib/csv/match_p.rb +0 -20
data/lib/csv/row.rb
CHANGED
|
@@ -703,7 +703,7 @@ class CSV
|
|
|
703
703
|
# by +index_or_header+ and +specifiers+.
|
|
704
704
|
#
|
|
705
705
|
# The nested objects may be instances of various classes.
|
|
706
|
-
# See {Dig Methods}[
|
|
706
|
+
# See {Dig Methods}[rdoc-ref:dig_methods.rdoc].
|
|
707
707
|
#
|
|
708
708
|
# Examples:
|
|
709
709
|
# source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
data/lib/csv/table.rb
CHANGED
|
@@ -890,9 +890,8 @@ class CSV
|
|
|
890
890
|
if @mode == :row or @mode == :col_or_row # by index
|
|
891
891
|
@table.delete_if(&block)
|
|
892
892
|
else # by header
|
|
893
|
-
deleted = []
|
|
894
893
|
headers.each do |header|
|
|
895
|
-
|
|
894
|
+
delete(header) if yield([header, self[header]])
|
|
896
895
|
end
|
|
897
896
|
end
|
|
898
897
|
|
|
@@ -999,9 +998,15 @@ class CSV
|
|
|
999
998
|
# Omits the headers if option +write_headers+ is given as +false+
|
|
1000
999
|
# (see {Option +write_headers+}[../CSV.html#class-CSV-label-Option+write_headers]):
|
|
1001
1000
|
# table.to_csv(write_headers: false) # => "foo,0\nbar,1\nbaz,2\n"
|
|
1002
|
-
|
|
1001
|
+
#
|
|
1002
|
+
# Limit rows if option +limit+ is given like +2+:
|
|
1003
|
+
# table.to_csv(limit: 2) # => "Name,Value\nfoo,0\nbar,1\n"
|
|
1004
|
+
def to_csv(write_headers: true, limit: nil, **options)
|
|
1003
1005
|
array = write_headers ? [headers.to_csv(**options)] : []
|
|
1004
|
-
@table.
|
|
1006
|
+
limit ||= @table.size
|
|
1007
|
+
limit = @table.size + 1 + limit if limit < 0
|
|
1008
|
+
limit = 0 if limit < 0
|
|
1009
|
+
@table.first(limit).each do |row|
|
|
1005
1010
|
array.push(row.fields.to_csv(**options)) unless row.header_row?
|
|
1006
1011
|
end
|
|
1007
1012
|
|
|
@@ -1038,9 +1043,13 @@ class CSV
|
|
|
1038
1043
|
# Example:
|
|
1039
1044
|
# source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
|
1040
1045
|
# table = CSV.parse(source, headers: true)
|
|
1041
|
-
# table.inspect # => "#<CSV::Table mode:col_or_row row_count:4
|
|
1046
|
+
# table.inspect # => "#<CSV::Table mode:col_or_row row_count:4>\nName,Value\nfoo,0\nbar,1\nbaz,2\n"
|
|
1047
|
+
#
|
|
1042
1048
|
def inspect
|
|
1043
|
-
"#<#{self.class} mode:#{@mode} row_count:#{to_a.size}>"
|
|
1049
|
+
inspected = +"#<#{self.class} mode:#{@mode} row_count:#{to_a.size}>"
|
|
1050
|
+
summary = to_csv(limit: 5)
|
|
1051
|
+
inspected << "\n" << summary if summary.encoding.ascii_compatible?
|
|
1052
|
+
inspected
|
|
1044
1053
|
end
|
|
1045
1054
|
end
|
|
1046
1055
|
end
|
data/lib/csv/version.rb
CHANGED
data/lib/csv/writer.rb
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "input_record_separator"
|
|
4
|
-
require_relative "match_p"
|
|
5
4
|
require_relative "row"
|
|
6
5
|
|
|
7
|
-
using CSV::MatchP if CSV.const_defined?(:MatchP)
|
|
8
|
-
|
|
9
6
|
class CSV
|
|
10
7
|
# Note: Don't use this class directly. This is an internal class.
|
|
11
8
|
class Writer
|
|
@@ -42,7 +39,9 @@ class CSV
|
|
|
42
39
|
@headers ||= row if @use_headers
|
|
43
40
|
@lineno += 1
|
|
44
41
|
|
|
45
|
-
|
|
42
|
+
if @fields_converter
|
|
43
|
+
row = @fields_converter.convert(row, nil, lineno)
|
|
44
|
+
end
|
|
46
45
|
|
|
47
46
|
i = -1
|
|
48
47
|
converted_row = row.collect do |field|
|
|
@@ -97,7 +96,7 @@ class CSV
|
|
|
97
96
|
return unless @headers
|
|
98
97
|
|
|
99
98
|
converter = @options[:header_fields_converter]
|
|
100
|
-
@headers = converter.convert(@headers, nil, 0)
|
|
99
|
+
@headers = converter.convert(@headers, nil, 0, [])
|
|
101
100
|
@headers.each do |header|
|
|
102
101
|
header.freeze if header.is_a?(String)
|
|
103
102
|
end
|
data/lib/csv.rb
CHANGED
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
# == What is CSV, really?
|
|
71
71
|
#
|
|
72
72
|
# CSV maintains a pretty strict definition of CSV taken directly from
|
|
73
|
-
# {the RFC}[
|
|
73
|
+
# {the RFC}[https://www.ietf.org/rfc/rfc4180.txt]. I relax the rules in only one
|
|
74
74
|
# place and that is to make using this library easier. CSV will parse all valid
|
|
75
75
|
# CSV.
|
|
76
76
|
#
|
|
@@ -91,28 +91,18 @@
|
|
|
91
91
|
|
|
92
92
|
require "forwardable"
|
|
93
93
|
require "date"
|
|
94
|
+
require "time"
|
|
94
95
|
require "stringio"
|
|
95
96
|
|
|
96
97
|
require_relative "csv/fields_converter"
|
|
97
98
|
require_relative "csv/input_record_separator"
|
|
98
|
-
require_relative "csv/match_p"
|
|
99
99
|
require_relative "csv/parser"
|
|
100
100
|
require_relative "csv/row"
|
|
101
101
|
require_relative "csv/table"
|
|
102
102
|
require_relative "csv/writer"
|
|
103
103
|
|
|
104
|
-
using CSV::MatchP if CSV.const_defined?(:MatchP)
|
|
105
|
-
|
|
106
104
|
# == \CSV
|
|
107
105
|
#
|
|
108
|
-
# === In a Hurry?
|
|
109
|
-
#
|
|
110
|
-
# If you are familiar with \CSV data and have a particular task in mind,
|
|
111
|
-
# you may want to go directly to the:
|
|
112
|
-
# - {Recipes for CSV}[doc/csv/recipes/recipes_rdoc.html].
|
|
113
|
-
#
|
|
114
|
-
# Otherwise, read on here, about the API: classes, methods, and constants.
|
|
115
|
-
#
|
|
116
106
|
# === \CSV Data
|
|
117
107
|
#
|
|
118
108
|
# \CSV (comma-separated values) data is a text representation of a table:
|
|
@@ -357,7 +347,9 @@ using CSV::MatchP if CSV.const_defined?(:MatchP)
|
|
|
357
347
|
# - +row_sep+: Specifies the row separator; used to delimit rows.
|
|
358
348
|
# - +col_sep+: Specifies the column separator; used to delimit fields.
|
|
359
349
|
# - +quote_char+: Specifies the quote character; used to quote fields.
|
|
360
|
-
# - +field_size_limit+: Specifies the maximum field size allowed.
|
|
350
|
+
# - +field_size_limit+: Specifies the maximum field size + 1 allowed.
|
|
351
|
+
# Deprecated since 3.2.3. Use +max_field_size+ instead.
|
|
352
|
+
# - +max_field_size+: Specifies the maximum field size allowed.
|
|
361
353
|
# - +converters+: Specifies the field converters to be used.
|
|
362
354
|
# - +unconverted_fields+: Specifies whether unconverted fields are to be available.
|
|
363
355
|
# - +headers+: Specifies whether data contains headers,
|
|
@@ -530,6 +522,7 @@ using CSV::MatchP if CSV.const_defined?(:MatchP)
|
|
|
530
522
|
# - <tt>:float</tt>: converts each \String-embedded float into a true \Float.
|
|
531
523
|
# - <tt>:date</tt>: converts each \String-embedded date into a true \Date.
|
|
532
524
|
# - <tt>:date_time</tt>: converts each \String-embedded date-time into a true \DateTime
|
|
525
|
+
# - <tt>:time</tt>: converts each \String-embedded time into a true \Time
|
|
533
526
|
# .
|
|
534
527
|
# This example creates a converter proc, then stores it:
|
|
535
528
|
# strip_converter = proc {|field| field.strip }
|
|
@@ -640,6 +633,7 @@ using CSV::MatchP if CSV.const_defined?(:MatchP)
|
|
|
640
633
|
# [:numeric, [:integer, :float]]
|
|
641
634
|
# [:date, Proc]
|
|
642
635
|
# [:date_time, Proc]
|
|
636
|
+
# [:time, Proc]
|
|
643
637
|
# [:all, [:date_time, :numeric]]
|
|
644
638
|
#
|
|
645
639
|
# Each of these converters transcodes values to UTF-8 before attempting conversion.
|
|
@@ -684,6 +678,15 @@ using CSV::MatchP if CSV.const_defined?(:MatchP)
|
|
|
684
678
|
# csv = CSV.parse_line(data, converters: :date_time)
|
|
685
679
|
# csv # => [#<DateTime: 2020-05-07T14:59:00-05:00 ((2458977j,71940s,0n),-18000s,2299161j)>, "x"]
|
|
686
680
|
#
|
|
681
|
+
# Converter +time+ converts each field that Time::parse accepts:
|
|
682
|
+
# data = '2020-05-07T14:59:00-05:00,x'
|
|
683
|
+
# # Without the converter
|
|
684
|
+
# csv = CSV.parse_line(data)
|
|
685
|
+
# csv # => ["2020-05-07T14:59:00-05:00", "x"]
|
|
686
|
+
# # With the converter
|
|
687
|
+
# csv = CSV.parse_line(data, converters: :time)
|
|
688
|
+
# csv # => [2020-05-07 14:59:00 -0500, "x"]
|
|
689
|
+
#
|
|
687
690
|
# Converter +:numeric+ converts with both +:date_time+ and +:numeric+..
|
|
688
691
|
#
|
|
689
692
|
# As seen above, method #convert adds \converters to a \CSV instance,
|
|
@@ -855,6 +858,15 @@ class CSV
|
|
|
855
858
|
end
|
|
856
859
|
end
|
|
857
860
|
|
|
861
|
+
# The error thrown when the parser encounters invalid encoding in CSV.
|
|
862
|
+
class InvalidEncodingError < MalformedCSVError
|
|
863
|
+
attr_reader :encoding
|
|
864
|
+
def initialize(encoding, line_number)
|
|
865
|
+
@encoding = encoding
|
|
866
|
+
super("Invalid byte sequence in #{encoding}", line_number)
|
|
867
|
+
end
|
|
868
|
+
end
|
|
869
|
+
|
|
858
870
|
#
|
|
859
871
|
# A FieldInfo Struct contains details about a field's position in the data
|
|
860
872
|
# source it was read from. CSV will pass this Struct to some blocks that make
|
|
@@ -864,19 +876,19 @@ class CSV
|
|
|
864
876
|
# <b><tt>index</tt></b>:: The zero-based index of the field in its row.
|
|
865
877
|
# <b><tt>line</tt></b>:: The line of the data source this row is from.
|
|
866
878
|
# <b><tt>header</tt></b>:: The header for the column, when available.
|
|
879
|
+
# <b><tt>quoted?</tt></b>:: True or false, whether the original value is quoted or not.
|
|
867
880
|
#
|
|
868
|
-
FieldInfo = Struct.new(:index, :line, :header)
|
|
881
|
+
FieldInfo = Struct.new(:index, :line, :header, :quoted?)
|
|
869
882
|
|
|
870
883
|
# A Regexp used to find and convert some common Date formats.
|
|
871
884
|
DateMatcher = / \A(?: (\w+,?\s+)?\w+\s+\d{1,2},?\s+\d{2,4} |
|
|
872
885
|
\d{4}-\d{2}-\d{2} )\z /x
|
|
873
|
-
# A Regexp used to find and convert some common
|
|
886
|
+
# A Regexp used to find and convert some common (Date)Time formats.
|
|
874
887
|
DateTimeMatcher =
|
|
875
888
|
/ \A(?: (\w+,?\s+)?\w+\s+\d{1,2}\s+\d{1,2}:\d{1,2}:\d{1,2},?\s+\d{2,4} |
|
|
876
|
-
|
|
877
|
-
# ISO-8601
|
|
889
|
+
# ISO-8601 and RFC-3339 (space instead of T) recognized by (Date)Time.parse
|
|
878
890
|
\d{4}-\d{2}-\d{2}
|
|
879
|
-
(?:T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?(?:[+-]\d{2}(?::\d{2})|Z)?)?)?
|
|
891
|
+
(?:[T\s]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?(?:[+-]\d{2}(?::\d{2})|Z)?)?)?
|
|
880
892
|
)\z /x
|
|
881
893
|
|
|
882
894
|
# The encoding used by all converters.
|
|
@@ -912,6 +924,14 @@ class CSV
|
|
|
912
924
|
f
|
|
913
925
|
end
|
|
914
926
|
},
|
|
927
|
+
time: lambda { |f|
|
|
928
|
+
begin
|
|
929
|
+
e = f.encode(ConverterEncoding)
|
|
930
|
+
e.match?(DateTimeMatcher) ? Time.parse(e) : f
|
|
931
|
+
rescue # encoding conversion or parse errors
|
|
932
|
+
f
|
|
933
|
+
end
|
|
934
|
+
},
|
|
915
935
|
all: [:date_time, :numeric],
|
|
916
936
|
}
|
|
917
937
|
|
|
@@ -926,7 +946,8 @@ class CSV
|
|
|
926
946
|
symbol: lambda { |h|
|
|
927
947
|
h.encode(ConverterEncoding).downcase.gsub(/[^\s\w]+/, "").strip.
|
|
928
948
|
gsub(/\s+/, "_").to_sym
|
|
929
|
-
}
|
|
949
|
+
},
|
|
950
|
+
symbol_raw: lambda { |h| h.encode(ConverterEncoding).to_sym }
|
|
930
951
|
}
|
|
931
952
|
|
|
932
953
|
# Default values for method options.
|
|
@@ -937,6 +958,7 @@ class CSV
|
|
|
937
958
|
quote_char: '"',
|
|
938
959
|
# For parsing.
|
|
939
960
|
field_size_limit: nil,
|
|
961
|
+
max_field_size: nil,
|
|
940
962
|
converters: nil,
|
|
941
963
|
unconverted_fields: nil,
|
|
942
964
|
headers: false,
|
|
@@ -1004,7 +1026,7 @@ class CSV
|
|
|
1004
1026
|
def instance(data = $stdout, **options)
|
|
1005
1027
|
# create a _signature_ for this method call, data object and options
|
|
1006
1028
|
sig = [data.object_id] +
|
|
1007
|
-
options.values_at(*DEFAULT_OPTIONS.keys
|
|
1029
|
+
options.values_at(*DEFAULT_OPTIONS.keys)
|
|
1008
1030
|
|
|
1009
1031
|
# fetch or create the instance for this signature
|
|
1010
1032
|
@@instances ||= Hash.new
|
|
@@ -1143,7 +1165,7 @@ class CSV
|
|
|
1143
1165
|
# File.read('t.csv') # => "Name,Value\nFOO,0\nBAR,-1\nBAZ,-2\n"
|
|
1144
1166
|
#
|
|
1145
1167
|
# When neither +in_string_or_io+ nor +out_string_or_io+ given,
|
|
1146
|
-
# parses from {ARGF}[
|
|
1168
|
+
# parses from {ARGF}[rdoc-ref:ARGF]
|
|
1147
1169
|
# and generates to STDOUT.
|
|
1148
1170
|
#
|
|
1149
1171
|
# Without headers:
|
|
@@ -1196,12 +1218,49 @@ class CSV
|
|
|
1196
1218
|
# * Argument +in_string_or_io+ must be a \String or an \IO stream.
|
|
1197
1219
|
# * Argument +out_string_or_io+ must be a \String or an \IO stream.
|
|
1198
1220
|
# * Arguments <tt>**options</tt> must be keyword options.
|
|
1199
|
-
#
|
|
1221
|
+
#
|
|
1222
|
+
# - Each option defined as an {option for parsing}[#class-CSV-label-Options+for+Parsing]
|
|
1223
|
+
# is used for parsing the filter input.
|
|
1224
|
+
# - Each option defined as an {option for generating}[#class-CSV-label-Options+for+Generating]
|
|
1225
|
+
# is used for generator the filter input.
|
|
1226
|
+
#
|
|
1227
|
+
# However, there are three options that may be used for both parsing and generating:
|
|
1228
|
+
# +col_sep+, +quote_char+, and +row_sep+.
|
|
1229
|
+
#
|
|
1230
|
+
# Therefore for method +filter+ (and method +filter+ only),
|
|
1231
|
+
# there are special options that allow these parsing and generating options
|
|
1232
|
+
# to be specified separately:
|
|
1233
|
+
#
|
|
1234
|
+
# - Options +input_col_sep+ and +output_col_sep+
|
|
1235
|
+
# (and their aliases +in_col_sep+ and +out_col_sep+)
|
|
1236
|
+
# specify the column separators for parsing and generating.
|
|
1237
|
+
# - Options +input_quote_char+ and +output_quote_char+
|
|
1238
|
+
# (and their aliases +in_quote_char+ and +out_quote_char+)
|
|
1239
|
+
# specify the quote characters for parsing and generting.
|
|
1240
|
+
# - Options +input_row_sep+ and +output_row_sep+
|
|
1241
|
+
# (and their aliases +in_row_sep+ and +out_row_sep+)
|
|
1242
|
+
# specify the row separators for parsing and generating.
|
|
1243
|
+
#
|
|
1244
|
+
# Example options (for column separators):
|
|
1245
|
+
#
|
|
1246
|
+
# CSV.filter # Default for both parsing and generating.
|
|
1247
|
+
# CSV.filter(in_col_sep: ';') # ';' for parsing, default for generating.
|
|
1248
|
+
# CSV.filter(out_col_sep: '|') # Default for parsing, '|' for generating.
|
|
1249
|
+
# CSV.filter(in_col_sep: ';', out_col_sep: '|') # ';' for parsing, '|' for generating.
|
|
1250
|
+
#
|
|
1251
|
+
# Note that for a special option (e.g., +input_col_sep+)
|
|
1252
|
+
# and its corresponding "regular" option (e.g., +col_sep+),
|
|
1253
|
+
# the two are mutually overriding.
|
|
1254
|
+
#
|
|
1255
|
+
# Another example (possibly surprising):
|
|
1256
|
+
#
|
|
1257
|
+
# CSV.filter(in_col_sep: ';', col_sep: '|') # '|' for both parsing(!) and generating.
|
|
1258
|
+
#
|
|
1200
1259
|
def filter(input=nil, output=nil, **options)
|
|
1201
1260
|
# parse options for input, output, or both
|
|
1202
1261
|
in_options, out_options = Hash.new, {row_sep: InputRecordSeparator.value}
|
|
1203
1262
|
options.each do |key, value|
|
|
1204
|
-
case key
|
|
1263
|
+
case key
|
|
1205
1264
|
when /\Ain(?:put)?_(.+)\Z/
|
|
1206
1265
|
in_options[$1.to_sym] = value
|
|
1207
1266
|
when /\Aout(?:put)?_(.+)\Z/
|
|
@@ -1313,8 +1372,8 @@ class CSV
|
|
|
1313
1372
|
#
|
|
1314
1373
|
# Arguments:
|
|
1315
1374
|
# * Argument +path_or_io+ must be a file path or an \IO stream.
|
|
1316
|
-
# * Argument +mode+, if given, must be a \File mode
|
|
1317
|
-
# See {
|
|
1375
|
+
# * Argument +mode+, if given, must be a \File mode.
|
|
1376
|
+
# See {Access Modes}[https://docs.ruby-lang.org/en/master/File.html#class-File-label-Access+Modes].
|
|
1318
1377
|
# * Arguments <tt>**options</tt> must be keyword options.
|
|
1319
1378
|
# See {Options for Parsing}[#class-CSV-label-Options+for+Parsing].
|
|
1320
1379
|
# * This method optionally accepts an additional <tt>:encoding</tt> option
|
|
@@ -1464,12 +1523,50 @@ class CSV
|
|
|
1464
1523
|
(new(str, **options) << row).string
|
|
1465
1524
|
end
|
|
1466
1525
|
|
|
1526
|
+
# :call-seq:
|
|
1527
|
+
# CSV.generate_lines(rows)
|
|
1528
|
+
# CSV.generate_lines(rows, **options)
|
|
1529
|
+
#
|
|
1530
|
+
# Returns the \String created by generating \CSV from
|
|
1531
|
+
# using the specified +options+.
|
|
1532
|
+
#
|
|
1533
|
+
# Argument +rows+ must be an \Array of row. Row is \Array of \String or \CSV::Row.
|
|
1534
|
+
#
|
|
1535
|
+
# Special options:
|
|
1536
|
+
# * Option <tt>:row_sep</tt> defaults to <tt>"\n"</tt> on Ruby 3.0 or later
|
|
1537
|
+
# and <tt>$INPUT_RECORD_SEPARATOR</tt> (<tt>$/</tt>) otherwise.:
|
|
1538
|
+
# $INPUT_RECORD_SEPARATOR # => "\n"
|
|
1539
|
+
# * This method accepts an additional option, <tt>:encoding</tt>, which sets the base
|
|
1540
|
+
# Encoding for the output. This method will try to guess your Encoding from
|
|
1541
|
+
# the first non-+nil+ field in +row+, if possible, but you may need to use
|
|
1542
|
+
# this parameter as a backup plan.
|
|
1543
|
+
#
|
|
1544
|
+
# For other +options+,
|
|
1545
|
+
# see {Options for Generating}[#class-CSV-label-Options+for+Generating].
|
|
1546
|
+
#
|
|
1547
|
+
# ---
|
|
1548
|
+
#
|
|
1549
|
+
# Returns the \String generated from an
|
|
1550
|
+
# CSV.generate_lines([['foo', '0'], ['bar', '1'], ['baz', '2']]) # => "foo,0\nbar,1\nbaz,2\n"
|
|
1551
|
+
#
|
|
1552
|
+
# ---
|
|
1553
|
+
#
|
|
1554
|
+
# Raises an exception
|
|
1555
|
+
# # Raises NoMethodError (undefined method `each' for :foo:Symbol)
|
|
1556
|
+
# CSV.generate_lines(:foo)
|
|
1557
|
+
#
|
|
1558
|
+
def generate_lines(rows, **options)
|
|
1559
|
+
self.generate(**options) do |csv|
|
|
1560
|
+
rows.each do |row|
|
|
1561
|
+
csv << row
|
|
1562
|
+
end
|
|
1563
|
+
end
|
|
1564
|
+
end
|
|
1565
|
+
|
|
1467
1566
|
#
|
|
1468
1567
|
# :call-seq:
|
|
1469
|
-
# open(
|
|
1470
|
-
# open(
|
|
1471
|
-
# open(file_path, mode = "rb", **options ) { |csv| ... } -> object
|
|
1472
|
-
# open(io, mode = "rb", **options ) { |csv| ... } -> object
|
|
1568
|
+
# open(path_or_io, mode = "rb", **options ) -> new_csv
|
|
1569
|
+
# open(path_or_io, mode = "rb", **options ) { |csv| ... } -> object
|
|
1473
1570
|
#
|
|
1474
1571
|
# possible options elements:
|
|
1475
1572
|
# keyword form:
|
|
@@ -1478,10 +1575,10 @@ class CSV
|
|
|
1478
1575
|
# :undef => :replace # replace undefined conversion
|
|
1479
1576
|
# :replace => string # replacement string ("?" or "\uFFFD" if not specified)
|
|
1480
1577
|
#
|
|
1481
|
-
# * Argument +
|
|
1578
|
+
# * Argument +path_or_io+, must be a file path or an \IO stream.
|
|
1482
1579
|
# :include: ../doc/csv/arguments/io.rdoc
|
|
1483
|
-
# * Argument +mode+, if given, must be a \File mode
|
|
1484
|
-
# See {
|
|
1580
|
+
# * Argument +mode+, if given, must be a \File mode.
|
|
1581
|
+
# See {Access Modes}[https://docs.ruby-lang.org/en/master/File.html#class-File-label-Access+Modes].
|
|
1485
1582
|
# * Arguments <tt>**options</tt> must be keyword options.
|
|
1486
1583
|
# See {Options for Generating}[#class-CSV-label-Options+for+Generating].
|
|
1487
1584
|
# * This method optionally accepts an additional <tt>:encoding</tt> option
|
|
@@ -1502,6 +1599,9 @@ class CSV
|
|
|
1502
1599
|
# path = 't.csv'
|
|
1503
1600
|
# File.write(path, string)
|
|
1504
1601
|
#
|
|
1602
|
+
# string_io = StringIO.new
|
|
1603
|
+
# string_io << "foo,0\nbar,1\nbaz,2\n"
|
|
1604
|
+
#
|
|
1505
1605
|
# ---
|
|
1506
1606
|
#
|
|
1507
1607
|
# With no block given, returns a new \CSV object.
|
|
@@ -1514,6 +1614,9 @@ class CSV
|
|
|
1514
1614
|
# csv = CSV.open(File.open(path))
|
|
1515
1615
|
# csv # => #<CSV io_type:File io_path:"t.csv" encoding:UTF-8 lineno:0 col_sep:"," row_sep:"\n" quote_char:"\"">
|
|
1516
1616
|
#
|
|
1617
|
+
# Create a \CSV object using a \StringIO:
|
|
1618
|
+
# csv = CSV.open(string_io)
|
|
1619
|
+
# csv # => #<CSV io_type:StringIO encoding:UTF-8 lineno:0 col_sep:"," row_sep:"\n" quote_char:"\"">
|
|
1517
1620
|
# ---
|
|
1518
1621
|
#
|
|
1519
1622
|
# With a block given, calls the block with the created \CSV object;
|
|
@@ -1531,15 +1634,25 @@ class CSV
|
|
|
1531
1634
|
# Output:
|
|
1532
1635
|
# #<CSV io_type:File io_path:"t.csv" encoding:UTF-8 lineno:0 col_sep:"," row_sep:"\n" quote_char:"\"">
|
|
1533
1636
|
#
|
|
1637
|
+
# Using a \StringIO:
|
|
1638
|
+
# csv = CSV.open(string_io) {|csv| p csv}
|
|
1639
|
+
# csv # => #<CSV io_type:StringIO encoding:UTF-8 lineno:0 col_sep:"," row_sep:"\n" quote_char:"\"">
|
|
1640
|
+
# Output:
|
|
1641
|
+
# #<CSV io_type:StringIO encoding:UTF-8 lineno:0 col_sep:"," row_sep:"\n" quote_char:"\"">
|
|
1534
1642
|
# ---
|
|
1535
1643
|
#
|
|
1536
1644
|
# Raises an exception if the argument is not a \String object or \IO object:
|
|
1537
1645
|
# # Raises TypeError (no implicit conversion of Symbol into String)
|
|
1538
1646
|
# CSV.open(:foo)
|
|
1539
|
-
def open(
|
|
1647
|
+
def open(filename_or_io, mode="r", **options)
|
|
1540
1648
|
# wrap a File opened with the remaining +args+ with no newline
|
|
1541
1649
|
# decorator
|
|
1542
|
-
file_opts =
|
|
1650
|
+
file_opts = {}
|
|
1651
|
+
may_enable_bom_detection_automatically(filename_or_io,
|
|
1652
|
+
mode,
|
|
1653
|
+
options,
|
|
1654
|
+
file_opts)
|
|
1655
|
+
file_opts.merge!(options)
|
|
1543
1656
|
unless file_opts.key?(:newline)
|
|
1544
1657
|
file_opts[:universal_newline] ||= false
|
|
1545
1658
|
end
|
|
@@ -1548,14 +1661,19 @@ class CSV
|
|
|
1548
1661
|
options.delete(:replace)
|
|
1549
1662
|
options.delete_if {|k, _| /newline\z/.match?(k)}
|
|
1550
1663
|
|
|
1551
|
-
|
|
1552
|
-
f =
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1664
|
+
if filename_or_io.is_a?(StringIO)
|
|
1665
|
+
f = create_stringio(filename_or_io.string, mode, **file_opts)
|
|
1666
|
+
else
|
|
1667
|
+
begin
|
|
1668
|
+
f = File.open(filename_or_io, mode, **file_opts)
|
|
1669
|
+
rescue ArgumentError => e
|
|
1670
|
+
raise unless /needs binmode/.match?(e.message) and mode == "r"
|
|
1671
|
+
mode = "rb"
|
|
1672
|
+
file_opts = {encoding: Encoding.default_external}.merge(file_opts)
|
|
1673
|
+
retry
|
|
1674
|
+
end
|
|
1558
1675
|
end
|
|
1676
|
+
|
|
1559
1677
|
begin
|
|
1560
1678
|
csv = new(f, **options)
|
|
1561
1679
|
rescue Exception
|
|
@@ -1687,6 +1805,23 @@ class CSV
|
|
|
1687
1805
|
# Raises an exception if the argument is not a \String object or \IO object:
|
|
1688
1806
|
# # Raises NoMethodError (undefined method `close' for :foo:Symbol)
|
|
1689
1807
|
# CSV.parse(:foo)
|
|
1808
|
+
#
|
|
1809
|
+
# ---
|
|
1810
|
+
#
|
|
1811
|
+
# Please make sure if your text contains \BOM or not. CSV.parse will not remove
|
|
1812
|
+
# \BOM automatically. You might want to remove \BOM before calling CSV.parse :
|
|
1813
|
+
# # remove BOM on calling File.open
|
|
1814
|
+
# File.open(path, encoding: 'bom|utf-8') do |file|
|
|
1815
|
+
# CSV.parse(file, headers: true) do |row|
|
|
1816
|
+
# # you can get value by column name because BOM is removed
|
|
1817
|
+
# p row['Name']
|
|
1818
|
+
# end
|
|
1819
|
+
# end
|
|
1820
|
+
#
|
|
1821
|
+
# Output:
|
|
1822
|
+
# # "foo"
|
|
1823
|
+
# # "bar"
|
|
1824
|
+
# # "baz"
|
|
1690
1825
|
def parse(str, **options, &block)
|
|
1691
1826
|
csv = new(str, **options)
|
|
1692
1827
|
|
|
@@ -1820,6 +1955,42 @@ class CSV
|
|
|
1820
1955
|
options = default_options.merge(options)
|
|
1821
1956
|
read(path, **options)
|
|
1822
1957
|
end
|
|
1958
|
+
|
|
1959
|
+
ON_WINDOWS = /mingw|mswin/.match?(RUBY_PLATFORM)
|
|
1960
|
+
private_constant :ON_WINDOWS
|
|
1961
|
+
|
|
1962
|
+
private
|
|
1963
|
+
def may_enable_bom_detection_automatically(filename_or_io,
|
|
1964
|
+
mode,
|
|
1965
|
+
options,
|
|
1966
|
+
file_opts)
|
|
1967
|
+
if filename_or_io.is_a?(StringIO)
|
|
1968
|
+
# Support to StringIO was dropped for Ruby 2.6 and earlier without BOM support:
|
|
1969
|
+
# https://github.com/ruby/stringio/pull/47
|
|
1970
|
+
return if RUBY_VERSION < "2.7"
|
|
1971
|
+
else
|
|
1972
|
+
# "bom|utf-8" may be buggy on Windows:
|
|
1973
|
+
# https://bugs.ruby-lang.org/issues/20526
|
|
1974
|
+
return if ON_WINDOWS
|
|
1975
|
+
end
|
|
1976
|
+
return unless Encoding.default_external == Encoding::UTF_8
|
|
1977
|
+
return if options.key?(:encoding)
|
|
1978
|
+
return if options.key?(:external_encoding)
|
|
1979
|
+
return if mode.is_a?(String) and mode.include?(":")
|
|
1980
|
+
file_opts[:encoding] = "bom|utf-8"
|
|
1981
|
+
end
|
|
1982
|
+
|
|
1983
|
+
if RUBY_VERSION < "2.7"
|
|
1984
|
+
def create_stringio(str, mode, opts)
|
|
1985
|
+
opts.delete_if {|k, _| k == :universal_newline or DEFAULT_OPTIONS.key?(k)}
|
|
1986
|
+
raise ArgumentError, "Unsupported options parsing StringIO: #{opts.keys}" unless opts.empty?
|
|
1987
|
+
StringIO.new(str, mode)
|
|
1988
|
+
end
|
|
1989
|
+
else
|
|
1990
|
+
def create_stringio(str, mode, opts)
|
|
1991
|
+
StringIO.new(str, mode, **opts)
|
|
1992
|
+
end
|
|
1993
|
+
end
|
|
1823
1994
|
end
|
|
1824
1995
|
|
|
1825
1996
|
# :call-seq:
|
|
@@ -1865,6 +2036,7 @@ class CSV
|
|
|
1865
2036
|
row_sep: :auto,
|
|
1866
2037
|
quote_char: '"',
|
|
1867
2038
|
field_size_limit: nil,
|
|
2039
|
+
max_field_size: nil,
|
|
1868
2040
|
converters: nil,
|
|
1869
2041
|
unconverted_fields: nil,
|
|
1870
2042
|
headers: false,
|
|
@@ -1888,8 +2060,19 @@ class CSV
|
|
|
1888
2060
|
raise ArgumentError.new("Cannot parse nil as CSV") if data.nil?
|
|
1889
2061
|
|
|
1890
2062
|
if data.is_a?(String)
|
|
2063
|
+
if encoding
|
|
2064
|
+
if encoding.is_a?(String)
|
|
2065
|
+
data_external_encoding, data_internal_encoding = encoding.split(":", 2)
|
|
2066
|
+
if data_internal_encoding
|
|
2067
|
+
data = data.encode(data_internal_encoding, data_external_encoding)
|
|
2068
|
+
else
|
|
2069
|
+
data = data.dup.force_encoding(data_external_encoding)
|
|
2070
|
+
end
|
|
2071
|
+
else
|
|
2072
|
+
data = data.dup.force_encoding(encoding)
|
|
2073
|
+
end
|
|
2074
|
+
end
|
|
1891
2075
|
@io = StringIO.new(data)
|
|
1892
|
-
@io.set_encoding(encoding || data.encoding)
|
|
1893
2076
|
else
|
|
1894
2077
|
@io = data
|
|
1895
2078
|
end
|
|
@@ -1907,11 +2090,14 @@ class CSV
|
|
|
1907
2090
|
@initial_header_converters = header_converters
|
|
1908
2091
|
@initial_write_converters = write_converters
|
|
1909
2092
|
|
|
2093
|
+
if max_field_size.nil? and field_size_limit
|
|
2094
|
+
max_field_size = field_size_limit - 1
|
|
2095
|
+
end
|
|
1910
2096
|
@parser_options = {
|
|
1911
2097
|
column_separator: col_sep,
|
|
1912
2098
|
row_separator: row_sep,
|
|
1913
2099
|
quote_character: quote_char,
|
|
1914
|
-
|
|
2100
|
+
max_field_size: max_field_size,
|
|
1915
2101
|
unconverted_fields: unconverted_fields,
|
|
1916
2102
|
headers: headers,
|
|
1917
2103
|
return_headers: return_headers,
|
|
@@ -1943,6 +2129,12 @@ class CSV
|
|
|
1943
2129
|
writer if @writer_options[:write_headers]
|
|
1944
2130
|
end
|
|
1945
2131
|
|
|
2132
|
+
class TSV < CSV
|
|
2133
|
+
def initialize(data, **options)
|
|
2134
|
+
super(data, **({col_sep: "\t"}.merge(options)))
|
|
2135
|
+
end
|
|
2136
|
+
end
|
|
2137
|
+
|
|
1946
2138
|
# :call-seq:
|
|
1947
2139
|
# csv.col_sep -> string
|
|
1948
2140
|
#
|
|
@@ -1979,10 +2171,24 @@ class CSV
|
|
|
1979
2171
|
# Returns the limit for field size; used for parsing;
|
|
1980
2172
|
# see {Option +field_size_limit+}[#class-CSV-label-Option+field_size_limit]:
|
|
1981
2173
|
# CSV.new('').field_size_limit # => nil
|
|
2174
|
+
#
|
|
2175
|
+
# Deprecated since 3.2.3. Use +max_field_size+ instead.
|
|
1982
2176
|
def field_size_limit
|
|
1983
2177
|
parser.field_size_limit
|
|
1984
2178
|
end
|
|
1985
2179
|
|
|
2180
|
+
# :call-seq:
|
|
2181
|
+
# csv.max_field_size -> integer or nil
|
|
2182
|
+
#
|
|
2183
|
+
# Returns the limit for field size; used for parsing;
|
|
2184
|
+
# see {Option +max_field_size+}[#class-CSV-label-Option+max_field_size]:
|
|
2185
|
+
# CSV.new('').max_field_size # => nil
|
|
2186
|
+
#
|
|
2187
|
+
# Since 3.2.3.
|
|
2188
|
+
def max_field_size
|
|
2189
|
+
parser.max_field_size
|
|
2190
|
+
end
|
|
2191
|
+
|
|
1986
2192
|
# :call-seq:
|
|
1987
2193
|
# csv.skip_lines -> regexp or nil
|
|
1988
2194
|
#
|
|
@@ -2481,7 +2687,13 @@ class CSV
|
|
|
2481
2687
|
# p row
|
|
2482
2688
|
# end
|
|
2483
2689
|
def each(&block)
|
|
2484
|
-
|
|
2690
|
+
return to_enum(__method__) unless block_given?
|
|
2691
|
+
begin
|
|
2692
|
+
while true
|
|
2693
|
+
yield(parser_enumerator.next)
|
|
2694
|
+
end
|
|
2695
|
+
rescue StopIteration
|
|
2696
|
+
end
|
|
2485
2697
|
end
|
|
2486
2698
|
|
|
2487
2699
|
# :call-seq:
|