dsv 0.12.8 → 0.12.10
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 +20 -0
- data/README.md +2 -0
- data/TODO +39 -17
- data/lib/DSV/UnterminatedQuote.rb +14 -0
- data/lib/DSV/VERSION.rb +1 -1
- data/lib/dsv.rb +12 -4
- data/test/dsv_test.rb +11 -2
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f88116d5530f25575975779af46933ddd7c67af651b2c61a3894b237b000950a
|
|
4
|
+
data.tar.gz: cd03ed4c4b47c12044387544346d8be13ac3f256f1263f135302134bf1ce429d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0a6e2477a6ff620bba7e5317af9ec1482f0effa853f3ac6f6074e1460170f201b0649db16c1ba67b87621546b986948678d4f6610f2623e29766cb9d948c227d
|
|
7
|
+
data.tar.gz: b9ad0b92bcf1ec1caee1604217005250411ac318014ceafb9384ceb70ae276f5d2e457e097a287dcbb5c9d9b668144e8a2c35194d9e7e2a9d28377c5aa3bf8b9
|
data/CHANGELOG
CHANGED
|
@@ -1,7 +1,27 @@
|
|
|
1
1
|
# dsv/CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 20261103
|
|
4
|
+
|
|
5
|
+
0.12.10: ~ TODO, now grouped by release; + ROADMAP.md.
|
|
6
|
+
|
|
7
|
+
1. ~ TODO: the jobs decided, by release: the fixes left for 0.12.x; modes as declarations in 0.13.0; streaming in 0.14.0; what 1.0 needs; speed and new options in 1.x.
|
|
8
|
+
2. ~ TODO: 11, strict:, and 15, the malformed-quoting option, superseded by field_count: and :rfc_4180; 13, chunks, by a streaming each; 1, 6, 7 and the undecided parts of 2, to ROADMAP.md.
|
|
9
|
+
3. + ROADMAP.md: the design, the boundary for 1.0, the version rule, the release sequence, and the themes after 1.0 with what is undecided in each.
|
|
10
|
+
4. ~ lib/DSV/VERSION.rb: /0.12.9/0.12.10/.
|
|
11
|
+
|
|
12
|
+
|
|
3
13
|
## 20261028
|
|
4
14
|
|
|
15
|
+
0.12.9: + DSV::UnterminatedQuote, raised where a quoted field does not close.
|
|
16
|
+
|
|
17
|
+
1. + lib/DSV/UnterminatedQuote.rb: DSV::UnterminatedQuote, with line, the line on which the field's row began.
|
|
18
|
+
2. ~ row_fields(), parse_row(), columns(): raise it where a quoted field is still open at the end of the source, or of the line or header given. Such a field had been dropped silently, with its row.
|
|
19
|
+
3. + line_of(): counts the lines before the row only when raising.
|
|
20
|
+
4. ~ test/dsv_test.rb: - the skip on a quoted field which does not close; + the line it names; the malformed examples now state the default rule.
|
|
21
|
+
5. ~ README.md: Quoting, the rule for input which isn't RFC 4180.
|
|
22
|
+
6. ~ TODO: 12, as UnterminatedQuote finds its line; + 15, the rule as an option.
|
|
23
|
+
7. ~ lib/DSV/VERSION.rb: /0.12.8/0.12.9/.
|
|
24
|
+
|
|
5
25
|
0.12.8: a stray quote no longer takes in the next line.
|
|
6
26
|
|
|
7
27
|
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.
|
data/README.md
CHANGED
|
@@ -89,6 +89,8 @@ DSV.read('quoted.csv', headers: true, quote: :double) # every field is quoted,
|
|
|
89
89
|
|
|
90
90
|
Under `:double` a row that is wholly unquoted, as a header row often is, is split plainly, and a row that is neither wholly quoted nor wholly unquoted is read as it would be with no mode named, so naming the mode never costs a correct read.
|
|
91
91
|
|
|
92
|
+
Input which isn't RFC 4180 is read to one rule. A quote opens a field only at the start of one, so a quote anywhere else, as in `12" pizza`, is data. A quoted field closes only at a quote followed by the separator or the end of the row, so one which doesn't close by the end of the source, as `"abc` or `"ab"c` doesn't, raises `DSV::UnterminatedQuote`, whose `line` is the line its row began on.
|
|
93
|
+
|
|
92
94
|
### Writing
|
|
93
95
|
|
|
94
96
|
Set the columns, give the rows as Hashes, and write:
|
data/TODO
CHANGED
|
@@ -1,24 +1,46 @@
|
|
|
1
1
|
# TODO
|
|
2
2
|
|
|
3
|
-
## 0.12.x,
|
|
3
|
+
## 0.12.x, fixes
|
|
4
4
|
|
|
5
|
-
1.
|
|
6
|
-
2.
|
|
7
|
-
3.
|
|
8
|
-
4.
|
|
5
|
+
1. The class methods close what they open, an early exit included, and without a block return an Enumerator which opens the source afresh on each pass, as CSV.foreach does; collect, select, reject and detect without a block no longer raise.
|
|
6
|
+
2. as_array: with a column selection: each('name') no longer raises TypeError, and read('name', as_array: true) gives only the columns selected. as_array: in the README, as the way to get Arrays as CSV gives them.
|
|
7
|
+
3. Reading honours quote: given as a String, as writing does; DSV.parse_line honours quote:.
|
|
8
|
+
4. A space as the column separator splits on each space, not as String#split(' ') does.
|
|
9
|
+
5. :double back to 0.12.4's reading, undoing 0.12.5 item 2: a row not starting with a quote split plainly, one starting with a quote split on quote-separator-quote, and one which leaves a lone quote in a field read in full as wholly quoted. The README back to 0.12.3's wording.
|
|
10
|
+
6. :single read, as :double is with a single quote.
|
|
11
|
+
7. filter, a class-level alias of select.
|
|
12
|
+
8. A String under a write mode taken as a path, so that a file not yet there can be written by name.
|
|
13
|
+
9. The internals private.
|
|
9
14
|
|
|
10
|
-
## 0.13.0,
|
|
15
|
+
## 0.13.0, modes as declarations
|
|
11
16
|
|
|
12
|
-
|
|
17
|
+
10. An unknown quote mode, file mode or option raises ArgumentError, where each is ignored today.
|
|
18
|
+
11. :mixed, the name of the default.
|
|
19
|
+
12. A mode raises on a contradiction which costs nothing to see, every row alike, header included: under :double, a row not starting with a quote, and a bare field in a row read in full. Declaring :mixed reads such a row.
|
|
13
20
|
|
|
14
|
-
##
|
|
21
|
+
## 0.14.0, streaming
|
|
15
22
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
13. each yields each row as it is read and returns its receiver, so that detect and break stop reading and memory does not grow with the file; rows is no longer filled by each. Chunks come from each_slice. A source read once, start to end, with no rewind, so that a pipe or $stdin can be read: the header from the first row read, and the line of an error found by seeking only where the source can; a rewind left to r+ rewriting and first_row called alone.
|
|
24
|
+
|
|
25
|
+
## 1.0
|
|
26
|
+
|
|
27
|
+
14. A corpus in the tests, a day of NEMWEB's dispatch files trimmed and the RFC 4180 cases, each checked against stdlib CSV's rows.
|
|
28
|
+
15. A benchmark script against stdlib CSV, and the README's speed sentence written from its output.
|
|
29
|
+
16. README: every mode by example, and a table matching each to stdlib CSV's and Python's settings.
|
|
30
|
+
|
|
31
|
+
## 1.x, speed, results unchanged
|
|
32
|
+
|
|
33
|
+
17. A per-row fast path for the default: split, the fields bare or wrapped in two quotes unwrapped, any other row read in full.
|
|
34
|
+
18. as_array: returning the split fields, rather than building a Hash and then an Array from it.
|
|
35
|
+
19. The splitter chosen once, at construction, as row_renderer is for writing.
|
|
36
|
+
20. Two string predicates in place of four regex matches per piece; rows built by zip.to_h; the scanner-based parser, at the tag scanner-parser-20261006, judged against the rest.
|
|
37
|
+
|
|
38
|
+
## 1.x, features
|
|
39
|
+
|
|
40
|
+
21. :rfc_4180, a quote mode reading what RFC 4180 allows as :mixed does and raising on what it forbids.
|
|
41
|
+
22. field_count:, :header or an Integer, off by default: a row of another width raises.
|
|
42
|
+
23. Writing: minimal quoting, a field quoted only where it holds a separator, a quote or a row terminator; a row terminator inside a field kept, quoted, or folded to a space; the encoding declared and a transcode offered with any loss marked.
|
|
43
|
+
24. A line number in each parse error, found only when the error is raised, as UnterminatedQuote finds its, where the source can seek. line_numbers:, off by default, counts lines as it reads instead, for any source a pipe included: the read loop chosen once, at construction, as row_renderer is for writing, so that without it no row pays for the count.
|
|
44
|
+
25. The instance-level header_row renamed headers?, kept as an alias; << to append a row and generate to build a string.
|
|
45
|
+
26. Symbols and Strings interchangeable for column names, in a selection, a columns: list and a row's keys.
|
|
46
|
+
27. The duplicate-header modes as an option: keep all as an Array, the default; index, a, a.1; first; last; error.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# DSV/UnterminatedQuote.rb
|
|
2
|
+
# DSV::UnterminatedQuote
|
|
3
|
+
|
|
4
|
+
class DSV
|
|
5
|
+
# A quoted field which does not close by the end of the source; line is the one on which its row began.
|
|
6
|
+
class UnterminatedQuote < ::StandardError
|
|
7
|
+
attr_reader :line
|
|
8
|
+
|
|
9
|
+
def initialize(line)
|
|
10
|
+
@line = line
|
|
11
|
+
super("A quoted field in the row beginning on line #{line} does not close.")
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
data/lib/DSV/VERSION.rb
CHANGED
data/lib/dsv.rb
CHANGED
|
@@ -8,6 +8,7 @@ require 'stringio'
|
|
|
8
8
|
require 'DSV/VERSION'
|
|
9
9
|
require 'DSV/File'
|
|
10
10
|
require 'DSV/String'
|
|
11
|
+
require 'DSV/UnterminatedQuote'
|
|
11
12
|
|
|
12
13
|
class DSV
|
|
13
14
|
|
|
@@ -211,7 +212,7 @@ class DSV
|
|
|
211
212
|
@columns = (
|
|
212
213
|
if header_row? && ['r', 'r+', 'a+'].include?(@mode) && (first_row = first_row?)
|
|
213
214
|
columns, i = {}, -1
|
|
214
|
-
(split_row(first_row) ||
|
|
215
|
+
(split_row(first_row) || raise(UnterminatedQuote.new(1))).each do |column_name|
|
|
215
216
|
columns[column_name] = columns.key?(column_name) ? [*columns[column_name], i += 1] : (i += 1)
|
|
216
217
|
end
|
|
217
218
|
columns
|
|
@@ -365,14 +366,21 @@ class DSV
|
|
|
365
366
|
separators ? separators[i - 1] : @column_separator
|
|
366
367
|
end
|
|
367
368
|
|
|
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
|
|
369
|
+
# 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 raises UnterminatedQuote.
|
|
369
370
|
def row_fields(raw_row)
|
|
370
371
|
raw_row << @source.gets(@row_separator) until (fields = split_row(raw_row)) || @source.eof?
|
|
371
|
-
fields ||
|
|
372
|
+
fields || raise(UnterminatedQuote.new(line_of(raw_row)))
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# The line on which a row read up to the source's position began, counted only when an error wants it, by reading the source again up to the row, so that no row which closes pays for it.
|
|
376
|
+
def line_of(raw_row)
|
|
377
|
+
start = @source.pos - raw_row.bytesize
|
|
378
|
+
@source.rewind
|
|
379
|
+
@source.read(start).to_s.scan(@row_separator).length + 1
|
|
372
380
|
end
|
|
373
381
|
|
|
374
382
|
def parse_row(raw_row, *selected_columns)
|
|
375
|
-
parse_fields(split_row(raw_row) ||
|
|
383
|
+
parse_fields(split_row(raw_row) || raise(UnterminatedQuote.new(1)), *selected_columns)
|
|
376
384
|
end
|
|
377
385
|
|
|
378
386
|
def parse_fields(fields, *selected_columns)
|
data/test/dsv_test.rb
CHANGED
|
@@ -554,7 +554,7 @@ describe DSV do
|
|
|
554
554
|
end
|
|
555
555
|
end
|
|
556
556
|
|
|
557
|
-
# Input which is not RFC 4180, read to
|
|
557
|
+
# Input which is not RFC 4180, read to the default rule: a quote is structure only where it opens a field, and a field it opens which does not close raises UnterminatedQuote.
|
|
558
558
|
describe "quoting on read, where the input is malformed" do
|
|
559
559
|
it "reads a quote which does not open a field as data" do
|
|
560
560
|
_(DSV.parse_line('a"b"c,d')).must_equal ['a"b"c', 'd']
|
|
@@ -572,9 +572,18 @@ describe DSV do
|
|
|
572
572
|
end
|
|
573
573
|
|
|
574
574
|
it "raises UnterminatedQuote on a quoted field which does not close" do
|
|
575
|
-
skip "Observation: a quoted field which does not close is dropped silently, with its row"
|
|
576
575
|
_{DSV.read("\"abc,d\n")}.must_raise DSV::UnterminatedQuote
|
|
577
576
|
_{DSV.read("\"ab\"c,d\n")}.must_raise DSV::UnterminatedQuote
|
|
577
|
+
_{DSV.read("x,\"abc\n", quote: :double)}.must_raise DSV::UnterminatedQuote
|
|
578
|
+
_{DSV.parse_line('x,"abc')}.must_raise DSV::UnterminatedQuote
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
it "names the line on which the row holding the unterminated field began" do
|
|
582
|
+
error = _{DSV.read("a,b\n\"c\nd\",e\nf,\"g\nh\n")}.must_raise DSV::UnterminatedQuote
|
|
583
|
+
_(error.line).must_equal 4
|
|
584
|
+
_(error.message).must_include 'line 4'
|
|
585
|
+
error = _{DSV.read("name,note\nAda,\"unclosed\n", headers: true)}.must_raise DSV::UnterminatedQuote
|
|
586
|
+
_(error.line).must_equal 2
|
|
578
587
|
end
|
|
579
588
|
end
|
|
580
589
|
|
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.
|
|
4
|
+
version: 0.12.10
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- thoran
|
|
@@ -67,6 +67,7 @@ files:
|
|
|
67
67
|
- dsv.gemspec
|
|
68
68
|
- lib/DSV/File.rb
|
|
69
69
|
- lib/DSV/String.rb
|
|
70
|
+
- lib/DSV/UnterminatedQuote.rb
|
|
70
71
|
- lib/DSV/VERSION.rb
|
|
71
72
|
- lib/dsv.rb
|
|
72
73
|
- test/VERSION_test.rb
|