dsv 0.12.5 → 0.12.7

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: d6c8309ce7e75acd45ed55995d9c1ef8f04f1c6e945e56e80cb60d1fe4b7fa2a
4
- data.tar.gz: 2f6a5693d250797bf9e9ce69f2d06c2fd144e5722c7f3e7996c0f4029b71fdea
3
+ metadata.gz: ac547ecbc72cfc0cfb3b4e2a1f9c0c5327ea1f4e064c126a28aa85f8c3a42ded
4
+ data.tar.gz: 5be7f5303ed207575c43dfacac8d7b4e68f259172b48d25aba60104af57b68f7
5
5
  SHA512:
6
- metadata.gz: c91f3fd66f79475ec7c617f47545378a08af49e3a78e7b52b684496b3ab6cb22205e2c26a5c7cd025ca26956ac8730685e2a4344ce7505ea4e46ef737ef0d7e5
7
- data.tar.gz: 3b2c669417438150a4fdfe421358e75362148ac7fa4fe8be1579c79b4c0f7f862283a2797dbef5bd02949cf9218daa61cdc194c0ca84844f443d39242e937245
6
+ metadata.gz: 022fbe8f4caf5baf90c785ba1ef586574d270331e25b6734e1e982766e1a8836a3182f1f92452734ce1d3316e834e0b07da4b8f9b89f6105c45222e4505dab7b
7
+ data.tar.gz: 19cd8f16d5b72349be0e066f63a5dcbac7cc9ea1ab4736e67cd07507415794ce2f9cc47d9bc747358125c252480018b1a0170efd820bdab8056611f46971cfd0
data/CHANGELOG CHANGED
@@ -1,5 +1,19 @@
1
1
  # dsv/CHANGELOG
2
2
 
3
+ ## 20261027
4
+
5
+ 0.12.7: ~ reassemble_quoted_fields(): an escaped quote before a separator no longer closes a quoted field.
6
+
7
+ 1. ~ reassemble_quoted_fields(): a piece closes a field where it ends in an odd run of quotes; + trailing_quotes().
8
+ 2. ~ test/dsv_test.rb: - the skip on a doubled quote before a separator.
9
+ 3. ~ lib/DSV/VERSION.rb: /0.12.6/0.12.7/.
10
+
11
+ 0.12.6: + quoting examples.
12
+
13
+ 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.
14
+ 2. ~ lib/DSV/VERSION.rb: /0.12.5/0.12.6/.
15
+
16
+
3
17
  ## 20261016
4
18
 
5
19
  0.12.5: Regexp separators and quote: :double made to do what the README says.
data/lib/DSV/VERSION.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # DSV::VERSION
3
3
 
4
4
  class DSV
5
- VERSION = '0.12.5'
5
+ VERSION = '0.12.7'
6
6
  end
data/lib/dsv.rb CHANGED
@@ -332,19 +332,19 @@ 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.
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
@@ -353,6 +353,13 @@ class DSV
353
353
  fields
354
354
  end
355
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
361
+ end
362
+
356
363
  # What stood between a piece and the one before it: the separator, or the text a Regexp separator matched there.
357
364
  def separator_before(i, separators)
358
365
  separators ? separators[i - 1] : @column_separator
data/test/dsv_test.rb CHANGED
@@ -242,6 +242,12 @@ describe DSV do
242
242
  _(DSV.parse_line('foo,"""""",baz')).must_equal ['foo', '""', 'baz']
243
243
  end
244
244
 
245
+ it "reads a doubled quote just before a separator inside a quoted field" do
246
+ _(DSV.parse_line('"He said ""no"", then left",x')).must_equal ['He said "no", then left', 'x']
247
+ _(DSV.parse_line('"a"",""b",c')).must_equal ['a","b', 'c']
248
+ _(DSV.read("\"a\"\",\"\"b\",\"c\"\n", quote: :double)).must_equal [{0 => 'a","b', 1 => 'c'}]
249
+ end
250
+
245
251
  it "reads a trailing empty field as an empty string" do
246
252
  _(DSV.parse_line('foo,bar,')).must_equal ['foo', 'bar', '']
247
253
  _(DSV.parse_line(',')).must_equal ['', '']
@@ -548,6 +554,26 @@ describe DSV do
548
554
  end
549
555
  end
550
556
 
557
+ # Input which is not RFC 4180, read to one rule provisionally, until a policy for malformed quoting is decided: a quote is structure only where it opens a field, and a field it opens which does not close raises UnterminatedQuote.
558
+ describe "quoting on read, where the input is malformed" do
559
+ it "reads a quote which does not open a field as data" do
560
+ _(DSV.parse_line('a"b"c,d')).must_equal ['a"b"c', 'd']
561
+ _(DSV.parse_line('a, "b,c"')).must_equal ['a', ' "b', 'c"']
562
+ end
563
+
564
+ it "reads a stray quote in an unquoted field as data, without taking in the next line" do
565
+ 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"
566
+ _(DSV.read("12\" pizza,9.99\nnext,row\n", as_array: true)).must_equal [['12" pizza', '9.99'], ['next', 'row']]
567
+ _(DSV.read("12\" pizza,9.99\nnext,row\n", as_array: true, quote: :double)).must_equal [['12" pizza', '9.99'], ['next', 'row']]
568
+ end
569
+
570
+ it "raises UnterminatedQuote on a quoted field which does not close" do
571
+ skip "Observation: a quoted field which does not close is dropped silently, with the rest of its row"
572
+ _{DSV.read("\"abc,d\n")}.must_raise DSV::UnterminatedQuote
573
+ _{DSV.read("\"ab\"c,d\n")}.must_raise DSV::UnterminatedQuote
574
+ end
575
+ end
576
+
551
577
  describe "#write" do
552
578
  it "writes each row in column order, double-quoted by default" do
553
579
  output = written(columns: [:a, :b]){|csv| csv.rows = [{'a' => 1, 'b' => 2}]; csv.write}
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.5
4
+ version: 0.12.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - thoran