csv 3.2.3 → 3.2.4

Sign up to get free protection for your applications and to get access to all the features.
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