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.
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}[https://docs.ruby-lang.org/en/master/doc/dig_methods_rdoc.html].
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
- deleted << delete(header) if yield([header, self[header]])
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
- def to_csv(write_headers: true, **options)
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.each do |row|
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}>".encode("US-ASCII")
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
@@ -2,5 +2,5 @@
2
2
 
3
3
  class CSV
4
4
  # The version of the installed library.
5
- VERSION = "3.2.2"
5
+ VERSION = "3.3.5"
6
6
  end
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
- row = @fields_converter.convert(row, nil, lineno) if @fields_converter
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}[http://www.ietf.org/rfc/rfc4180.txt]. I relax the rules in only one
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 DateTime formats.
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
- \d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2} |
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.sort_by { |sym| sym.to_s })
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}[https://docs.ruby-lang.org/en/master/ARGF.html]
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
- # See {Options for Parsing}[#class-CSV-label-Options+for+Parsing].
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.to_s
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 {Open Mode}[https://ruby-doc.org/core/IO.html#method-c-new-label-Open+Mode].
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(file_path, mode = "rb", **options ) -> new_csv
1470
- # open(io, mode = "rb", **options ) -> new_csv
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 +path+, if given, must be the path to a file.
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 {Open Mode}[IO.html#method-c-new-label-Open+Mode].
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(filename, mode="r", **options)
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 = options.dup
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
- begin
1552
- f = File.open(filename, mode, **file_opts)
1553
- rescue ArgumentError => e
1554
- raise unless /needs binmode/.match?(e.message) and mode == "r"
1555
- mode = "rb"
1556
- file_opts = {encoding: Encoding.default_external}.merge(file_opts)
1557
- retry
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
- field_size_limit: field_size_limit,
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
- parser_enumerator.each(&block)
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: