csv 3.2.3 → 3.2.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 915b3ed5a51bf4836f08f7bb06efc3b07bdc90e09209a5253092130e2cad2ab6
4
- data.tar.gz: 6bce2e39329afcf200691b4b2f422b6a48d45da66368f5d5e136e0c761cd6217
3
+ metadata.gz: 3ef88fc9b205f8f64c5e817b22f121d5d03534c155cb8d27dc9d87aa62e6b7e0
4
+ data.tar.gz: e12b86cee946837a96ae609314a50246e2135fab855dca58e800de1ddc50e524
5
5
  SHA512:
6
- metadata.gz: 5c1434c8e91c16de40d19d4d1200f193248e786720b67f2bbecf26a481859fe814b8cbaa02d22027668ff02588541266c8ff5d00b9fc1cfc2163b358b8e9ece9
7
- data.tar.gz: 1978e933549049129f0ec99e80a10f2838b3c75a282103aa177d8421fe7589d428308e2786b29a961a4a7a5565ede77e3b1ef44ba8f4bc91b593a5a884ded7aa
6
+ metadata.gz: d663d0917d63315e4fc5802f9538727731f39ea06997adea485f5f137f37639f0791db1f9697476e7d0c4a8048a0339b9e4fb7aecd79f3ec8ee8eb24a4cb676e
7
+ data.tar.gz: f9d22fe50d227b8f8ca0d075ed0fbf1f39a10bf59a1adb68835fede727f3d1dfd1d2f88754caa1098fe9c24df738c71b29ae5fc3080a25d80df484f76530344e
data/NEWS.md CHANGED
@@ -1,5 +1,46 @@
1
1
  # News
2
2
 
3
+ ## 3.2.4 - 2022-08-22
4
+
5
+ ### Improvements
6
+
7
+ * Cleaned up internal implementations.
8
+ [[GitHub#249](https://github.com/ruby/csv/pull/249)]
9
+ [[GitHub#250](https://github.com/ruby/csv/pull/250)]
10
+ [[GitHub#251](https://github.com/ruby/csv/pull/251)]
11
+ [Patch by Mau Magnaguagno]
12
+
13
+ * Added support for RFC 3339 style time.
14
+ [[GitHub#248](https://github.com/ruby/csv/pull/248)]
15
+ [Patch by Thierry Lambert]
16
+
17
+ * Added support for transcoding String CSV. Syntax is
18
+ `from-encoding:to-encoding`.
19
+ [[GitHub#254](https://github.com/ruby/csv/issues/254)]
20
+ [Reported by Richard Stueven]
21
+
22
+ * Added quoted information to `CSV::FieldInfo`.
23
+ [[GitHub#254](https://github.com/ruby/csv/pull/253)]
24
+ [Reported by Hirokazu SUZUKI]
25
+
26
+ ### Fixes
27
+
28
+ * Fixed a link in documents.
29
+ [[GitHub#244](https://github.com/ruby/csv/pull/244)]
30
+ [Patch by Peter Zhu]
31
+
32
+ ### Thanks
33
+
34
+ * Peter Zhu
35
+
36
+ * Mau Magnaguagno
37
+
38
+ * Thierry Lambert
39
+
40
+ * Richard Stueven
41
+
42
+ * Hirokazu SUZUKI
43
+
3
44
  ## 3.2.3 - 2022-04-09
4
45
 
5
46
  ### Improvements
@@ -44,7 +44,7 @@ class CSV
44
44
  @converters.empty?
45
45
  end
46
46
 
47
- def convert(fields, headers, lineno)
47
+ def convert(fields, headers, lineno, quoted_fields)
48
48
  return fields unless need_convert?
49
49
 
50
50
  fields.collect.with_index do |field, index|
@@ -63,7 +63,8 @@ class CSV
63
63
  else
64
64
  header = nil
65
65
  end
66
- field = converter[field, FieldInfo.new(index, lineno, header)]
66
+ quoted = quoted_fields[index]
67
+ field = converter[field, FieldInfo.new(index, lineno, header, quoted)]
67
68
  end
68
69
  break unless field.is_a?(String) # short-circuit pipeline for speed
69
70
  end
data/lib/csv/parser.rb CHANGED
@@ -2,15 +2,10 @@
2
2
 
3
3
  require "strscan"
4
4
 
5
- require_relative "delete_suffix"
6
5
  require_relative "input_record_separator"
7
- require_relative "match_p"
8
6
  require_relative "row"
9
7
  require_relative "table"
10
8
 
11
- using CSV::DeleteSuffix if CSV.const_defined?(:DeleteSuffix)
12
- using CSV::MatchP if CSV.const_defined?(:MatchP)
13
-
14
9
  class CSV
15
10
  # Note: Don't use this class directly. This is an internal class.
16
11
  class Parser
@@ -763,9 +758,10 @@ class CSV
763
758
  case headers
764
759
  when Array
765
760
  @raw_headers = headers
761
+ quoted_fields = [false] * @raw_headers.size
766
762
  @use_headers = true
767
763
  when String
768
- @raw_headers = parse_headers(headers)
764
+ @raw_headers, quoted_fields = parse_headers(headers)
769
765
  @use_headers = true
770
766
  when nil, false
771
767
  @raw_headers = nil
@@ -775,21 +771,28 @@ class CSV
775
771
  @use_headers = true
776
772
  end
777
773
  if @raw_headers
778
- @headers = adjust_headers(@raw_headers)
774
+ @headers = adjust_headers(@raw_headers, quoted_fields)
779
775
  else
780
776
  @headers = nil
781
777
  end
782
778
  end
783
779
 
784
780
  def parse_headers(row)
785
- CSV.parse_line(row,
786
- col_sep: @column_separator,
787
- row_sep: @row_separator,
788
- quote_char: @quote_character)
781
+ quoted_fields = []
782
+ converter = lambda do |field, info|
783
+ quoted_fields << info.quoted?
784
+ field
785
+ end
786
+ headers = CSV.parse_line(row,
787
+ col_sep: @column_separator,
788
+ row_sep: @row_separator,
789
+ quote_char: @quote_character,
790
+ converters: [converter])
791
+ [headers, quoted_fields]
789
792
  end
790
793
 
791
- def adjust_headers(headers)
792
- adjusted_headers = @header_fields_converter.convert(headers, nil, @lineno)
794
+ def adjust_headers(headers, quoted_fields)
795
+ adjusted_headers = @header_fields_converter.convert(headers, nil, @lineno, quoted_fields)
793
796
  adjusted_headers.each {|h| h.freeze if h.is_a? String}
794
797
  adjusted_headers
795
798
  end
@@ -933,9 +936,11 @@ class CSV
933
936
  if line.empty?
934
937
  next if @skip_blanks
935
938
  row = []
939
+ quoted_fields = []
936
940
  else
937
941
  line = strip_value(line)
938
942
  row = line.split(@split_column_separator, -1)
943
+ quoted_fields = [false] * row.size
939
944
  if @max_field_size
940
945
  row.each do |column|
941
946
  validate_field_size(column)
@@ -949,7 +954,7 @@ class CSV
949
954
  end
950
955
  end
951
956
  @last_line = original_line
952
- emit_row(row, &block)
957
+ emit_row(row, quoted_fields, &block)
953
958
  end
954
959
  end
955
960
 
@@ -971,25 +976,30 @@ class CSV
971
976
  next
972
977
  end
973
978
  row = []
979
+ quoted_fields = []
974
980
  elsif line.include?(@cr) or line.include?(@lf)
975
981
  @scanner.keep_back
976
982
  @need_robust_parsing = true
977
983
  return parse_quotable_robust(&block)
978
984
  else
979
985
  row = line.split(@split_column_separator, -1)
986
+ quoted_fields = []
980
987
  n_columns = row.size
981
988
  i = 0
982
989
  while i < n_columns
983
990
  column = row[i]
984
991
  if column.empty?
992
+ quoted_fields << false
985
993
  row[i] = nil
986
994
  else
987
995
  n_quotes = column.count(@quote_character)
988
996
  if n_quotes.zero?
997
+ quoted_fields << false
989
998
  # no quote
990
999
  elsif n_quotes == 2 and
991
1000
  column.start_with?(@quote_character) and
992
1001
  column.end_with?(@quote_character)
1002
+ quoted_fields << true
993
1003
  row[i] = column[1..-2]
994
1004
  else
995
1005
  @scanner.keep_back
@@ -1004,13 +1014,14 @@ class CSV
1004
1014
  @scanner.keep_drop
1005
1015
  @scanner.keep_start
1006
1016
  @last_line = original_line
1007
- emit_row(row, &block)
1017
+ emit_row(row, quoted_fields, &block)
1008
1018
  end
1009
1019
  @scanner.keep_drop
1010
1020
  end
1011
1021
 
1012
1022
  def parse_quotable_robust(&block)
1013
1023
  row = []
1024
+ quoted_fields = []
1014
1025
  skip_needless_lines
1015
1026
  start_row
1016
1027
  while true
@@ -1024,20 +1035,24 @@ class CSV
1024
1035
  end
1025
1036
  if parse_column_end
1026
1037
  row << value
1038
+ quoted_fields << @quoted_column_value
1027
1039
  elsif parse_row_end
1028
1040
  if row.empty? and value.nil?
1029
- emit_row([], &block) unless @skip_blanks
1041
+ emit_row([], [], &block) unless @skip_blanks
1030
1042
  else
1031
1043
  row << value
1032
- emit_row(row, &block)
1044
+ quoted_fields << @quoted_column_value
1045
+ emit_row(row, quoted_fields, &block)
1033
1046
  row = []
1047
+ quoted_fields = []
1034
1048
  end
1035
1049
  skip_needless_lines
1036
1050
  start_row
1037
1051
  elsif @scanner.eos?
1038
1052
  break if row.empty? and value.nil?
1039
1053
  row << value
1040
- emit_row(row, &block)
1054
+ quoted_fields << @quoted_column_value
1055
+ emit_row(row, quoted_fields, &block)
1041
1056
  break
1042
1057
  else
1043
1058
  if @quoted_column_value
@@ -1141,7 +1156,7 @@ class CSV
1141
1156
  if (n_quotes % 2).zero?
1142
1157
  quotes[0, (n_quotes - 2) / 2]
1143
1158
  else
1144
- value = quotes[0, (n_quotes - 1) / 2]
1159
+ value = quotes[0, n_quotes / 2]
1145
1160
  while true
1146
1161
  quoted_value = @scanner.scan_all(@quoted_value)
1147
1162
  value << quoted_value if quoted_value
@@ -1165,11 +1180,9 @@ class CSV
1165
1180
  n_quotes = quotes.size
1166
1181
  if n_quotes == 1
1167
1182
  break
1168
- elsif (n_quotes % 2) == 1
1169
- value << quotes[0, (n_quotes - 1) / 2]
1170
- break
1171
1183
  else
1172
1184
  value << quotes[0, n_quotes / 2]
1185
+ break if (n_quotes % 2) == 1
1173
1186
  end
1174
1187
  end
1175
1188
  value
@@ -1205,18 +1218,15 @@ class CSV
1205
1218
 
1206
1219
  def strip_value(value)
1207
1220
  return value unless @strip
1208
- return nil if value.nil?
1221
+ return value if value.nil?
1209
1222
 
1210
1223
  case @strip
1211
1224
  when String
1212
- size = value.size
1213
- while value.start_with?(@strip)
1214
- size -= 1
1215
- value = value[1, size]
1225
+ while value.delete_prefix!(@strip)
1226
+ # do nothing
1216
1227
  end
1217
- while value.end_with?(@strip)
1218
- size -= 1
1219
- value = value[0, size]
1228
+ while value.delete_suffix!(@strip)
1229
+ # do nothing
1220
1230
  end
1221
1231
  else
1222
1232
  value.strip!
@@ -1239,22 +1249,22 @@ class CSV
1239
1249
  @scanner.keep_start
1240
1250
  end
1241
1251
 
1242
- def emit_row(row, &block)
1252
+ def emit_row(row, quoted_fields, &block)
1243
1253
  @lineno += 1
1244
1254
 
1245
1255
  raw_row = row
1246
1256
  if @use_headers
1247
1257
  if @headers.nil?
1248
- @headers = adjust_headers(row)
1258
+ @headers = adjust_headers(row, quoted_fields)
1249
1259
  return unless @return_headers
1250
1260
  row = Row.new(@headers, row, true)
1251
1261
  else
1252
1262
  row = Row.new(@headers,
1253
- @fields_converter.convert(raw_row, @headers, @lineno))
1263
+ @fields_converter.convert(raw_row, @headers, @lineno, quoted_fields))
1254
1264
  end
1255
1265
  else
1256
1266
  # convert fields, if needed...
1257
- row = @fields_converter.convert(raw_row, nil, @lineno)
1267
+ row = @fields_converter.convert(raw_row, nil, @lineno, quoted_fields)
1258
1268
  end
1259
1269
 
1260
1270
  # inject unconverted fields and accessor, if requested...
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}[https://docs.ruby-lang.org/en/master/dig_methods_rdoc.html].
707
707
  #
708
708
  # Examples:
709
709
  # source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
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.3"
5
+ VERSION = "3.2.4"
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,10 @@ 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
+ quoted_fields = [false] * row.size
44
+ row = @fields_converter.convert(row, nil, lineno, quoted_fields)
45
+ end
46
46
 
47
47
  i = -1
48
48
  converted_row = row.collect do |field|
@@ -97,7 +97,7 @@ class CSV
97
97
  return unless @headers
98
98
 
99
99
  converter = @options[:header_fields_converter]
100
- @headers = converter.convert(@headers, nil, 0)
100
+ @headers = converter.convert(@headers, nil, 0, [])
101
101
  @headers.each do |header|
102
102
  header.freeze if header.is_a?(String)
103
103
  end
data/lib/csv.rb CHANGED
@@ -95,14 +95,11 @@ require "stringio"
95
95
 
96
96
  require_relative "csv/fields_converter"
97
97
  require_relative "csv/input_record_separator"
98
- require_relative "csv/match_p"
99
98
  require_relative "csv/parser"
100
99
  require_relative "csv/row"
101
100
  require_relative "csv/table"
102
101
  require_relative "csv/writer"
103
102
 
104
- using CSV::MatchP if CSV.const_defined?(:MatchP)
105
-
106
103
  # == \CSV
107
104
  #
108
105
  # === In a Hurry?
@@ -866,8 +863,9 @@ class CSV
866
863
  # <b><tt>index</tt></b>:: The zero-based index of the field in its row.
867
864
  # <b><tt>line</tt></b>:: The line of the data source this row is from.
868
865
  # <b><tt>header</tt></b>:: The header for the column, when available.
866
+ # <b><tt>quoted?</tt></b>:: True or false, whether the original value is quoted or not.
869
867
  #
870
- FieldInfo = Struct.new(:index, :line, :header)
868
+ FieldInfo = Struct.new(:index, :line, :header, :quoted?)
871
869
 
872
870
  # A Regexp used to find and convert some common Date formats.
873
871
  DateMatcher = / \A(?: (\w+,?\s+)?\w+\s+\d{1,2},?\s+\d{2,4} |
@@ -875,10 +873,9 @@ class CSV
875
873
  # A Regexp used to find and convert some common DateTime formats.
876
874
  DateTimeMatcher =
877
875
  / \A(?: (\w+,?\s+)?\w+\s+\d{1,2}\s+\d{1,2}:\d{1,2}:\d{1,2},?\s+\d{2,4} |
878
- \d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2} |
879
- # ISO-8601
876
+ # ISO-8601 and RFC-3339 (space instead of T) recognized by DateTime.parse
880
877
  \d{4}-\d{2}-\d{2}
881
- (?:T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?(?:[+-]\d{2}(?::\d{2})|Z)?)?)?
878
+ (?:[T\s]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?(?:[+-]\d{2}(?::\d{2})|Z)?)?)?
882
879
  )\z /x
883
880
 
884
881
  # The encoding used by all converters.
@@ -1893,8 +1890,19 @@ class CSV
1893
1890
  raise ArgumentError.new("Cannot parse nil as CSV") if data.nil?
1894
1891
 
1895
1892
  if data.is_a?(String)
1893
+ if encoding
1894
+ if encoding.is_a?(String)
1895
+ data_external_encoding, data_internal_encoding = encoding.split(":", 2)
1896
+ if data_internal_encoding
1897
+ data = data.encode(data_internal_encoding, data_external_encoding)
1898
+ else
1899
+ data = data.dup.force_encoding(data_external_encoding)
1900
+ end
1901
+ else
1902
+ data = data.dup.force_encoding(encoding)
1903
+ end
1904
+ end
1896
1905
  @io = StringIO.new(data)
1897
- @io.set_encoding(encoding || data.encoding)
1898
1906
  else
1899
1907
  @io = data
1900
1908
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: csv
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.3
4
+ version: 3.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Edward Gray II
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2022-04-08 00:00:00.000000000 Z
12
+ date: 2022-08-22 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -116,10 +116,8 @@ files:
116
116
  - lib/csv.rb
117
117
  - lib/csv/core_ext/array.rb
118
118
  - lib/csv/core_ext/string.rb
119
- - lib/csv/delete_suffix.rb
120
119
  - lib/csv/fields_converter.rb
121
120
  - lib/csv/input_record_separator.rb
122
- - lib/csv/match_p.rb
123
121
  - lib/csv/parser.rb
124
122
  - lib/csv/row.rb
125
123
  - lib/csv/table.rb
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # This provides String#delete_suffix? for Ruby 2.4.
4
- unless String.method_defined?(:delete_suffix)
5
- class CSV
6
- module DeleteSuffix
7
- refine String do
8
- def delete_suffix(suffix)
9
- if end_with?(suffix)
10
- self[0...-suffix.size]
11
- else
12
- self
13
- end
14
- end
15
- end
16
- end
17
- end
18
- end
data/lib/csv/match_p.rb DELETED
@@ -1,20 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # This provides String#match? and Regexp#match? for Ruby 2.3.
4
- unless String.method_defined?(:match?)
5
- class CSV
6
- module MatchP
7
- refine String do
8
- def match?(pattern)
9
- self =~ pattern
10
- end
11
- end
12
-
13
- refine Regexp do
14
- def match?(string)
15
- self =~ string
16
- end
17
- end
18
- end
19
- end
20
- end