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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7322674ac0843543bf2f813312290dd2c4d460c7c3d0e8e8d604ccfcb680a196
4
- data.tar.gz: 866e0a95d7854265444854111c0f1b61ed5e0faf9eb1f55ba20061f60fe52447
3
+ metadata.gz: 190e66c0b25b64c18dac0b955ae59fe64591d14d369cafa950d31b1aa56a2b30
4
+ data.tar.gz: 6a7ea034b8f8da40716efb89a555951307ee312c38142b84c71f2712c57e0622
5
5
  SHA512:
6
- metadata.gz: 219226e29fa873569b197d731813421aa478d18719bb25ede6501b84ab42efd574112a1131c7e3526933fe5099c8c1c3fffb8345d1fcdb26392dd8ef9fe445c9
7
- data.tar.gz: e57b19a5fc7ce1855f4fd40cca7a496bd946464c43f84e00e13f8a3b031d2e8b7c104e6995e749d2955a968c941896d80951f8190f557ff17ea92734c65aba66
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
@@ -2,5 +2,5 @@
2
2
  # DSV::VERSION
3
3
 
4
4
  class DSV
5
- VERSION = '0.12.6'
5
+ VERSION = '0.12.8'
6
6
  end
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 << parse_row(complete_quoted_row(raw_row), *selected_columns)}
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 && !piece.end_with?('"')
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.length == 1 || !piece.end_with?('"'))
345
+ elsif piece.start_with?('"') && trailing_quotes(piece, 0).even?
346
346
  buffer = +piece.delete_prefix('"')
347
- elsif piece.start_with?('"') && piece.end_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 quoted field may hold the row separator: while the quotes in a row are unbalanced the next line belongs to it, in every quote mode which reads quotes as structure, :none and :unquoted reading them as data.
362
- def complete_quoted_row(raw_row)
363
- raw_row << @source.gets(@row_separator).to_s while @quote != :none && @quote != :unquoted && raw_row.count('"').odd? && !@source.eof?
364
- raw_row
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
- split_row(raw_row).each{|column_value| parsed_row[i += 1] = column_value}
383
+ fields.each{|column_value| parsed_row[i += 1] = column_value}
373
384
  elsif repeated_names.empty?
374
- split_row(raw_row).each{|column_value| parsed_row[attributes[i += 1]] = column_value}
385
+ fields.each{|column_value| parsed_row[attributes[i += 1]] = column_value}
375
386
  else
376
- split_row(raw_row).each{|column_value| store_field(parsed_row, attributes[i += 1], column_value)}
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
- split_row(raw_row).each{|column_value| parsed_row[i] = column_value unless !selected_columns.include?(i += 1)}
393
+ fields.each{|column_value| parsed_row[i] = column_value unless !selected_columns.include?(i += 1)}
383
394
  else
384
- split_row(raw_row).each{|column_value| store_field(parsed_row, attributes[i], column_value) unless !selected_columns.include?(attributes[i += 1])}
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 the rest of its row"
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dsv
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.6
4
+ version: 0.12.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - thoran