dsv 0.12.6 → 0.12.8
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/CHANGELOG +18 -0
- data/lib/DSV/VERSION.rb +1 -1
- data/lib/dsv.rb +30 -19
- data/test/dsv_test.rb +6 -3
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 190e66c0b25b64c18dac0b955ae59fe64591d14d369cafa950d31b1aa56a2b30
|
|
4
|
+
data.tar.gz: 6a7ea034b8f8da40716efb89a555951307ee312c38142b84c71f2712c57e0622
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e6f6cb3c8f04131f26bf26be33f7dc1e000a4e39a58537f3c327c250837d880477b07e478be8688a57b124dfd85f24f92d569109de15e95ec61b95631652e493
|
|
7
|
+
data.tar.gz: f0787b59e38dbac938fcd7474b815e24d2660c33438289a9720ca239b439a3d7435ce650db9dd5279b2ae2266ccfe763d1dfe8005d92cdb2dd1a8c1369f16cb8
|
data/CHANGELOG
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
# dsv/CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 20261028
|
|
4
|
+
|
|
5
|
+
0.12.8: a stray quote no longer takes in the next line.
|
|
6
|
+
|
|
7
|
+
1. /complete_quoted_row()/row_fields()/: takes the next line into a row while a quoted field in it is open, rather than while its quotes are odd in number. A stray quote had made the count odd, and one beside an open field even.
|
|
8
|
+
2. ~ reassemble_quoted_fields(), split_row(): nil where a quoted field is still open at the end of the row.
|
|
9
|
+
3. ~ split_double_quoted(): takes a row as wholly quoted only where it ends in a closing quote.
|
|
10
|
+
4. + parse_fields(), from parse_row(), so that the read loop splits each row once.
|
|
11
|
+
5. ~ test/dsv_test.rb: - the skip on a stray quote; + a quoted field across lines after a stray quote.
|
|
12
|
+
6. ~ lib/DSV/VERSION.rb: /0.12.7/0.12.8/.
|
|
13
|
+
|
|
14
|
+
|
|
3
15
|
## 20261027
|
|
4
16
|
|
|
17
|
+
0.12.7: ~ reassemble_quoted_fields(): an escaped quote before a separator no longer closes a quoted field.
|
|
18
|
+
|
|
19
|
+
1. ~ reassemble_quoted_fields(): a piece closes a field where it ends in an odd run of quotes; + trailing_quotes().
|
|
20
|
+
2. ~ test/dsv_test.rb: - the skip on a doubled quote before a separator.
|
|
21
|
+
3. ~ lib/DSV/VERSION.rb: /0.12.6/0.12.7/.
|
|
22
|
+
|
|
5
23
|
0.12.6: + quoting examples.
|
|
6
24
|
|
|
7
25
|
1. ~ test/dsv_test.rb: + a doubled quote before a separator, skipped; + a quote not opening a field read as data; + a stray quote not taking in the next line, skipped; + an unclosed quoted field raising UnterminatedQuote, skipped.
|
data/lib/DSV/VERSION.rb
CHANGED
data/lib/dsv.rb
CHANGED
|
@@ -180,7 +180,7 @@ class DSV
|
|
|
180
180
|
else
|
|
181
181
|
read_header
|
|
182
182
|
@rows = []
|
|
183
|
-
@source.each(@row_separator){|raw_row| @rows <<
|
|
183
|
+
@source.each(@row_separator){|raw_row| @rows << parse_fields(row_fields(raw_row), *selected_columns)}
|
|
184
184
|
@rows
|
|
185
185
|
end
|
|
186
186
|
end
|
|
@@ -211,7 +211,7 @@ class DSV
|
|
|
211
211
|
@columns = (
|
|
212
212
|
if header_row? && ['r', 'r+', 'a+'].include?(@mode) && (first_row = first_row?)
|
|
213
213
|
columns, i = {}, -1
|
|
214
|
-
split_row(first_row).each do |column_name|
|
|
214
|
+
(split_row(first_row) || []).each do |column_name|
|
|
215
215
|
columns[column_name] = columns.key?(column_name) ? [*columns[column_name], i += 1] : (i += 1)
|
|
216
216
|
end
|
|
217
217
|
columns
|
|
@@ -263,7 +263,7 @@ class DSV
|
|
|
263
263
|
value.nil? || value.empty?
|
|
264
264
|
end
|
|
265
265
|
|
|
266
|
-
# One row into its fields, by the quote mode: :none splits; :double splits a row as it promises, wholly quoted or wholly unquoted, and one that is neither as unspecified would; unspecified splits and reassembles a quoted field that held the separator, a doubled quote inside it reading as one. Direct string operations throughout; the scanner-based parser is on the branch scanner-parser.
|
|
266
|
+
# One row into its fields, by the quote mode: :none splits; :double splits a row as it promises, wholly quoted or wholly unquoted, and one that is neither as unspecified would; unspecified splits and reassembles a quoted field that held the separator, a doubled quote inside it reading as one. nil where a quoted field in the row is still open at its end. Direct string operations throughout; the scanner-based parser is on the branch scanner-parser.
|
|
267
267
|
def split_row(raw_row)
|
|
268
268
|
row = raw_row.chomp(@row_separator)
|
|
269
269
|
case @quote
|
|
@@ -302,10 +302,10 @@ class DSV
|
|
|
302
302
|
end
|
|
303
303
|
end
|
|
304
304
|
|
|
305
|
-
# The :double contract: a wholly quoted row split on quote-separator-quote, a wholly unquoted one plainly; nil for a row that is neither.
|
|
305
|
+
# The :double contract: a wholly quoted row split on quote-separator-quote, a wholly unquoted one plainly; nil for a row that is neither, or whose last field does not close.
|
|
306
306
|
def split_double_quoted(row)
|
|
307
307
|
if row.start_with?('"')
|
|
308
|
-
unescaped_fields(split_on(row.delete_prefix('"').delete_suffix('"'), quoted_separator))
|
|
308
|
+
unescaped_fields(split_on(row.delete_prefix('"').delete_suffix('"'), quoted_separator)) if trailing_quotes(row, 0).odd?
|
|
309
309
|
else
|
|
310
310
|
split_on(row, @column_separator) unless row.include?('"')
|
|
311
311
|
end
|
|
@@ -332,25 +332,32 @@ class DSV
|
|
|
332
332
|
)
|
|
333
333
|
end
|
|
334
334
|
|
|
335
|
-
# Pieces of a row split on the separator, the pieces of a quoted field that held the separator joined back with it, or with the text it matched where the separator is a Regexp; a piece that is a lone quote opens or closes a field.
|
|
335
|
+
# Pieces of a row split on the separator, the pieces of a quoted field that held the separator joined back with it, or with the text it matched where the separator is a Regexp; a piece closes a field where it ends in an odd run of quotes, a doubled quote being escaped, and a piece that is a lone quote opens or closes one; nil where a field is still open after the last piece.
|
|
336
336
|
def reassemble_quoted_fields(pieces, separators = nil)
|
|
337
337
|
fields = []
|
|
338
338
|
buffer = nil
|
|
339
339
|
pieces.each_with_index do |piece, i|
|
|
340
|
-
if buffer &&
|
|
340
|
+
if buffer && trailing_quotes(piece, -1).even?
|
|
341
341
|
buffer << separator_before(i, separators) << piece
|
|
342
342
|
elsif buffer
|
|
343
343
|
fields << (buffer << separator_before(i, separators) << piece.delete_suffix('"')).gsub('""', '"')
|
|
344
344
|
buffer = nil
|
|
345
|
-
elsif piece.start_with?('"') && (piece
|
|
345
|
+
elsif piece.start_with?('"') && trailing_quotes(piece, 0).even?
|
|
346
346
|
buffer = +piece.delete_prefix('"')
|
|
347
|
-
elsif piece.start_with?('"')
|
|
347
|
+
elsif piece.start_with?('"')
|
|
348
348
|
fields << piece.delete_prefix('"').delete_suffix('"').gsub('""', '"')
|
|
349
349
|
else
|
|
350
350
|
fields << piece
|
|
351
351
|
end
|
|
352
352
|
end
|
|
353
|
-
fields
|
|
353
|
+
fields unless buffer
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# How many quotes a piece ends with, counting back to but not including the byte given, so that a field's opening quote is not taken for part of its closing. Read by byte, a quote being 34 in any ASCII-compatible encoding, so that nothing is allocated per character.
|
|
357
|
+
def trailing_quotes(piece, floor)
|
|
358
|
+
i = piece.bytesize - 1
|
|
359
|
+
i -= 1 while i > floor && piece.getbyte(i) == 34
|
|
360
|
+
piece.bytesize - 1 - i
|
|
354
361
|
end
|
|
355
362
|
|
|
356
363
|
# What stood between a piece and the one before it: the separator, or the text a Regexp separator matched there.
|
|
@@ -358,30 +365,34 @@ class DSV
|
|
|
358
365
|
separators ? separators[i - 1] : @column_separator
|
|
359
366
|
end
|
|
360
367
|
|
|
361
|
-
# A
|
|
362
|
-
def
|
|
363
|
-
raw_row << @source.gets(@row_separator)
|
|
364
|
-
|
|
368
|
+
# A row's fields, the next line taken into the row while a quoted field in it is open, a quoted field being able to hold the row separator; a field still open at the end of the source is dropped, with its row.
|
|
369
|
+
def row_fields(raw_row)
|
|
370
|
+
raw_row << @source.gets(@row_separator) until (fields = split_row(raw_row)) || @source.eof?
|
|
371
|
+
fields || []
|
|
365
372
|
end
|
|
366
373
|
|
|
367
374
|
def parse_row(raw_row, *selected_columns)
|
|
375
|
+
parse_fields(split_row(raw_row) || [], *selected_columns)
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def parse_fields(fields, *selected_columns)
|
|
368
379
|
parsed_row = {}
|
|
369
380
|
i = -1
|
|
370
381
|
if selected_columns.empty?
|
|
371
382
|
if blank?(@columns)
|
|
372
|
-
|
|
383
|
+
fields.each{|column_value| parsed_row[i += 1] = column_value}
|
|
373
384
|
elsif repeated_names.empty?
|
|
374
|
-
|
|
385
|
+
fields.each{|column_value| parsed_row[attributes[i += 1]] = column_value}
|
|
375
386
|
else
|
|
376
|
-
|
|
387
|
+
fields.each{|column_value| store_field(parsed_row, attributes[i += 1], column_value)}
|
|
377
388
|
end
|
|
378
389
|
else
|
|
379
390
|
selected_columns.flatten!
|
|
380
391
|
case selected_columns[0]
|
|
381
392
|
when Integer
|
|
382
|
-
|
|
393
|
+
fields.each{|column_value| parsed_row[i] = column_value unless !selected_columns.include?(i += 1)}
|
|
383
394
|
else
|
|
384
|
-
|
|
395
|
+
fields.each{|column_value| store_field(parsed_row, attributes[i], column_value) unless !selected_columns.include?(attributes[i += 1])}
|
|
385
396
|
end
|
|
386
397
|
end
|
|
387
398
|
if @as_array
|
data/test/dsv_test.rb
CHANGED
|
@@ -243,7 +243,6 @@ describe DSV do
|
|
|
243
243
|
end
|
|
244
244
|
|
|
245
245
|
it "reads a doubled quote just before a separator inside a quoted field" do
|
|
246
|
-
skip "Observation: a piece of a quoted field ending in a quote is taken as the field's end, an escaped quote ending in one as a closing quote does"
|
|
247
246
|
_(DSV.parse_line('"He said ""no"", then left",x')).must_equal ['He said "no", then left', 'x']
|
|
248
247
|
_(DSV.parse_line('"a"",""b",c')).must_equal ['a","b', 'c']
|
|
249
248
|
_(DSV.read("\"a\"\",\"\"b\",\"c\"\n", quote: :double)).must_equal [{0 => 'a","b', 1 => 'c'}]
|
|
@@ -563,13 +562,17 @@ describe DSV do
|
|
|
563
562
|
end
|
|
564
563
|
|
|
565
564
|
it "reads a stray quote in an unquoted field as data, without taking in the next line" do
|
|
566
|
-
skip "Observation: the quotes in a row are counted, so a stray quote leaves them unbalanced and the next line is taken into the row; unspecified since 0.11.4, :double since 0.12.5"
|
|
567
565
|
_(DSV.read("12\" pizza,9.99\nnext,row\n", as_array: true)).must_equal [['12" pizza', '9.99'], ['next', 'row']]
|
|
568
566
|
_(DSV.read("12\" pizza,9.99\nnext,row\n", as_array: true, quote: :double)).must_equal [['12" pizza', '9.99'], ['next', 'row']]
|
|
569
567
|
end
|
|
570
568
|
|
|
569
|
+
it "reads a quoted field across lines after a stray quote in the same row" do
|
|
570
|
+
_(DSV.read("a\"b,\"c\nd\"\n", as_array: true)).must_equal [['a"b', "c\nd"]]
|
|
571
|
+
_(DSV.read("a\"b,\"c\nd\"\n", as_array: true, quote: :double)).must_equal [['a"b', "c\nd"]]
|
|
572
|
+
end
|
|
573
|
+
|
|
571
574
|
it "raises UnterminatedQuote on a quoted field which does not close" do
|
|
572
|
-
skip "Observation: a quoted field which does not close is dropped silently, with
|
|
575
|
+
skip "Observation: a quoted field which does not close is dropped silently, with its row"
|
|
573
576
|
_{DSV.read("\"abc,d\n")}.must_raise DSV::UnterminatedQuote
|
|
574
577
|
_{DSV.read("\"ab\"c,d\n")}.must_raise DSV::UnterminatedQuote
|
|
575
578
|
end
|