dsv 0.12.4 → 0.12.5
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 +16 -2
- data/README.md +3 -1
- data/TODO +1 -1
- data/lib/DSV/VERSION.rb +1 -1
- data/lib/dsv.rb +80 -17
- data/test/dsv_test.rb +33 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d6c8309ce7e75acd45ed55995d9c1ef8f04f1c6e945e56e80cb60d1fe4b7fa2a
|
|
4
|
+
data.tar.gz: 2f6a5693d250797bf9e9ce69f2d06c2fd144e5722c7f3e7996c0f4029b71fdea
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c91f3fd66f79475ec7c617f47545378a08af49e3a78e7b52b684496b3ab6cb22205e2c26a5c7cd025ca26956ac8730685e2a4344ce7505ea4e46ef737ef0d7e5
|
|
7
|
+
data.tar.gz: 3b2c669417438150a4fdfe421358e75362148ac7fa4fe8be1579c79b4c0f7f862283a2797dbef5bd02949cf9218daa61cdc194c0ca84844f443d39242e937245
|
data/CHANGELOG
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# dsv/CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 20261016
|
|
4
|
+
|
|
5
|
+
0.12.5: Regexp separators and quote: :double made to do what the README says.
|
|
6
|
+
|
|
7
|
+
1. ~ split_row(): a Regexp separator reads a quoted field holding it, rejoined with the text matched; + pieces_and_separators(), + separator_before(), + split_on(). It had raised TypeError.
|
|
8
|
+
2. ~ split_row() under :double: + split_double_quoted(), wholly quoted or wholly unquoted rows split fast, any other row read as with no mode named; + split_quoted(), + unescaped_fields(), + quoted_separator. "a",b,"c" had been read as one field.
|
|
9
|
+
3. ~ complete_quoted_row(): carries a quoted field across lines under :double too.
|
|
10
|
+
4. ~ writing with a Regexp separator raises ArgumentError; + writable_column_separator. The row renderer is chosen at the first write.
|
|
11
|
+
5. ~ test/dsv_test.rb: - the skip on a Regexp separator with a quoted field; + examples of the above.
|
|
12
|
+
6. ~ README.md: writing with a Regexp raises; how :double reads a row it does not promise.
|
|
13
|
+
7. ~ TODO: 5, - the Regexp separator as a reason for the scanner parser.
|
|
14
|
+
8. ~ lib/DSV/VERSION.rb: /0.12.4/0.12.5/.
|
|
15
|
+
|
|
16
|
+
|
|
3
17
|
## 20260913, 1005
|
|
4
18
|
|
|
5
19
|
0.12.4: + dsv(.rb).gemspec, Gemfile, Rakefile, .gitignore and LICENSE.
|
|
@@ -41,7 +55,7 @@
|
|
|
41
55
|
|
|
42
56
|
0.11.18: + TODO, and the header's Todo, Ideas and Bugs sections retired into it.
|
|
43
57
|
|
|
44
|
-
1. + TODO: what survives of the header's three sections and of
|
|
58
|
+
1. + TODO: what survives of the header's three sections and of SimpleCSV's notes, triaged: what is done or fixed is dropped with its version named in the reconciliation, and the rest is grouped as 0.12.0, decided; 0.13.0, speed; and features after, the guessing mode (separator, header, and a header made from the data where there is none), templated ingestion as a whole-row Regexp, the duplicate-header modes, the writing options, symbols and strings interchangeably, a strict: option, and a line number on parse errors.
|
|
45
59
|
2. ~ lib/SimpleCSV.rb: - the Todo, Ideas and Bugs sections; the header is the name, the date, the version and the description. /0.11.17/0.11.18/.
|
|
46
60
|
|
|
47
61
|
0.11.17: ~ SimpleCSV.header_row, the header row as names.
|
|
@@ -168,7 +182,7 @@
|
|
|
168
182
|
|
|
169
183
|
1. + test/SimpleCSV_test.rb: MiniTest spec style, 102 examples. 76 pass and state the interface as it stands; 26 are skipped and state what the library should do and does not yet, each skip message naming its finding in the Handoff 0 closing note, so that the skip count is the fault list and each fix from here removes one skip. The RFC 4180 cases are rewritten from stdlib CSV's parsing tests. Run as `ruby test/SimpleCSV_test.rb`.
|
|
170
184
|
2. ~ lib/SimpleCSV.rb: - the Changes section of the header, its entries being here from 0.10.0 on; the date and version lines stay. From here each entry is written by hand, one per commit.
|
|
171
|
-
3. 0.10.4 was the last
|
|
185
|
+
3. 0.10.4 was the last imported version; from here every change is a commit.
|
|
172
186
|
|
|
173
187
|
### Note that prior to 0.10.4 predates this being in git.
|
|
174
188
|
|
data/README.md
CHANGED
|
@@ -76,7 +76,7 @@ DSV.read('people.txt', headers: true, column_separator: /,\s*/)
|
|
|
76
76
|
DSV.read('people.csv', headers: true, row_separator: "\r\n")
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
`col_sep:` and `row_sep:` are accepted as spellings of those. Writing takes the same two options, so a file round-trips through whatever it was read with.
|
|
79
|
+
`col_sep:` and `row_sep:` are accepted as spellings of those. Writing takes the same two options, so a file round-trips through whatever it was read with. A Regexp says what may be read as a separator, not what to write, so writing with one raises `ArgumentError`; give a String to write.
|
|
80
80
|
|
|
81
81
|
### Quoting
|
|
82
82
|
|
|
@@ -87,6 +87,8 @@ DSV.read('plain.csv', headers: true, quote: :none) # no field is quoted; quot
|
|
|
87
87
|
DSV.read('quoted.csv', headers: true, quote: :double) # every field is quoted, as DSV writes them
|
|
88
88
|
```
|
|
89
89
|
|
|
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
|
+
|
|
90
92
|
### Writing
|
|
91
93
|
|
|
92
94
|
Set the columns, give the rows as Hashes, and write:
|
data/TODO
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
## 0.13.0, speed
|
|
11
11
|
|
|
12
|
-
5. The saved-up speed change, with the evidence in the reconciliation: two string predicates in place of four regex matches per piece in split_csv; rows built by zip.to_h; a benchmark script in the repository against stdlib CSV, and the README's speed sentence written from its output. The scanner-based parser on the branch scanner-parser is judged here
|
|
12
|
+
5. The saved-up speed change, with the evidence in the reconciliation: two string predicates in place of four regex matches per piece in split_csv; rows built by zip.to_h; a benchmark script in the repository against stdlib CSV, and the README's speed sentence written from its output. The scanner-based parser on the branch scanner-parser is judged here.
|
|
13
13
|
|
|
14
14
|
## Features, after
|
|
15
15
|
|
data/lib/DSV/VERSION.rb
CHANGED
data/lib/dsv.rb
CHANGED
|
@@ -156,7 +156,6 @@ class DSV
|
|
|
156
156
|
@column_separator = options[:column_separator] || options[:col_sep] || ','
|
|
157
157
|
@selected_columns = options[:selected_columns]
|
|
158
158
|
@as_array = options[:as_array] || false
|
|
159
|
-
@render_row = row_renderer
|
|
160
159
|
if options[:columns]
|
|
161
160
|
self.columns = options[:columns]
|
|
162
161
|
else
|
|
@@ -264,32 +263,84 @@ class DSV
|
|
|
264
263
|
value.nil? || value.empty?
|
|
265
264
|
end
|
|
266
265
|
|
|
267
|
-
# One row into its fields, by the quote mode: :none splits; :double splits a
|
|
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.
|
|
268
267
|
def split_row(raw_row)
|
|
269
268
|
row = raw_row.chomp(@row_separator)
|
|
270
269
|
case @quote
|
|
271
270
|
when :none, :unquoted
|
|
272
|
-
row
|
|
271
|
+
split_on(row, @column_separator)
|
|
273
272
|
when :double, :double_quoted, :double_quotes
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
273
|
+
split_double_quoted(row) || split_quoted(row)
|
|
274
|
+
else
|
|
275
|
+
split_quoted(row)
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# A row split on a separator, String or Regexp; a Regexp's matches divide it, since String#split would also return any groups it captures.
|
|
280
|
+
def split_on(row, separator)
|
|
281
|
+
separator.is_a?(::Regexp) ? pieces_and_separators(row, separator).first : row.split(separator, -1)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# A row's pieces between a Regexp's matches, with the text of each match, so that a quoted field may be joined back with the separator it held.
|
|
285
|
+
def pieces_and_separators(row, separator)
|
|
286
|
+
pieces, separators, position = [], [], 0
|
|
287
|
+
row.scan(separator) do
|
|
288
|
+
match = ::Regexp.last_match
|
|
289
|
+
pieces << row[position...match.begin(0)]
|
|
290
|
+
separators << match[0]
|
|
291
|
+
position = match.end(0)
|
|
292
|
+
end
|
|
293
|
+
[pieces << row[position..], separators]
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Unspecified quoting: the row split on the separator, a quoted field that held it joined back with the separator's own text.
|
|
297
|
+
def split_quoted(row)
|
|
298
|
+
if @column_separator.is_a?(::Regexp)
|
|
299
|
+
reassemble_quoted_fields(*pieces_and_separators(row, @column_separator))
|
|
279
300
|
else
|
|
280
301
|
reassemble_quoted_fields(row.split(@column_separator, -1))
|
|
281
302
|
end
|
|
282
303
|
end
|
|
283
304
|
|
|
284
|
-
#
|
|
285
|
-
def
|
|
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.
|
|
306
|
+
def split_double_quoted(row)
|
|
307
|
+
if row.start_with?('"')
|
|
308
|
+
unescaped_fields(split_on(row.delete_prefix('"').delete_suffix('"'), quoted_separator))
|
|
309
|
+
else
|
|
310
|
+
split_on(row, @column_separator) unless row.include?('"')
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# The fields of a row split as wholly quoted, a doubled quote read as one; nil where a field holds a lone quote, the mark of a row that was not. A field has twice the quotes it has unescaped exactly when every quote in it is one of a doubled pair.
|
|
315
|
+
def unescaped_fields(fields)
|
|
316
|
+
fields.collect do |field|
|
|
317
|
+
next field unless field.include?('"')
|
|
318
|
+
unescaped = field.gsub('""', '"')
|
|
319
|
+
return nil unless field.count('"') == 2 * unescaped.count('"')
|
|
320
|
+
unescaped
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Quote, separator, quote: what divides the fields of a wholly quoted row.
|
|
325
|
+
def quoted_separator
|
|
326
|
+
@quoted_separator ||= (
|
|
327
|
+
if @column_separator.is_a?(::Regexp)
|
|
328
|
+
::Regexp.new('"(?:' + @column_separator.source + ')"', @column_separator.options)
|
|
329
|
+
else
|
|
330
|
+
'"' + @column_separator + '"'
|
|
331
|
+
end
|
|
332
|
+
)
|
|
333
|
+
end
|
|
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.
|
|
336
|
+
def reassemble_quoted_fields(pieces, separators = nil)
|
|
286
337
|
fields = []
|
|
287
338
|
buffer = nil
|
|
288
|
-
pieces.
|
|
339
|
+
pieces.each_with_index do |piece, i|
|
|
289
340
|
if buffer && !piece.end_with?('"')
|
|
290
|
-
buffer <<
|
|
341
|
+
buffer << separator_before(i, separators) << piece
|
|
291
342
|
elsif buffer
|
|
292
|
-
fields << (buffer <<
|
|
343
|
+
fields << (buffer << separator_before(i, separators) << piece.delete_suffix('"')).gsub('""', '"')
|
|
293
344
|
buffer = nil
|
|
294
345
|
elsif piece.start_with?('"') && (piece.length == 1 || !piece.end_with?('"'))
|
|
295
346
|
buffer = +piece.delete_prefix('"')
|
|
@@ -302,9 +353,14 @@ class DSV
|
|
|
302
353
|
fields
|
|
303
354
|
end
|
|
304
355
|
|
|
305
|
-
#
|
|
356
|
+
# What stood between a piece and the one before it: the separator, or the text a Regexp separator matched there.
|
|
357
|
+
def separator_before(i, separators)
|
|
358
|
+
separators ? separators[i - 1] : @column_separator
|
|
359
|
+
end
|
|
360
|
+
|
|
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.
|
|
306
362
|
def complete_quoted_row(raw_row)
|
|
307
|
-
raw_row << @source.gets(@row_separator).to_s while @quote
|
|
363
|
+
raw_row << @source.gets(@row_separator).to_s while @quote != :none && @quote != :unquoted && raw_row.count('"').odd? && !@source.eof?
|
|
308
364
|
raw_row
|
|
309
365
|
end
|
|
310
366
|
|
|
@@ -377,10 +433,17 @@ class DSV
|
|
|
377
433
|
alias_method :write_csv_row, :write_row
|
|
378
434
|
|
|
379
435
|
def write_values(values)
|
|
436
|
+
@render_row ||= row_renderer
|
|
380
437
|
@source.write(@render_row.call(values))
|
|
381
438
|
end
|
|
382
439
|
|
|
383
|
-
#
|
|
440
|
+
# The column separator a row is written with: a Regexp says what may be read as one, not what to write.
|
|
441
|
+
def writable_column_separator
|
|
442
|
+
raise ::ArgumentError, "a Regexp column separator can be read with, not written with; give a String to write" if @column_separator.is_a?(::Regexp)
|
|
443
|
+
@column_separator
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# Chosen once, at the first write, from the quote mode: how a row of values is rendered, with the instance's separators, a quote inside a quoted value doubled as RFC 4180 has it. The spacey modes put a space after each separator, as Array#to_csv_row did.
|
|
384
447
|
def row_renderer
|
|
385
448
|
quote, spacey = case @quote&.to_sym
|
|
386
449
|
when :none, :unquoted then [nil, false]
|
|
@@ -390,7 +453,7 @@ class DSV
|
|
|
390
453
|
when :spacey_double then ['"', true]
|
|
391
454
|
else ['"', false]
|
|
392
455
|
end
|
|
393
|
-
joiner = spacey ?
|
|
456
|
+
joiner = spacey ? writable_column_separator + ' ' : writable_column_separator
|
|
394
457
|
if quote
|
|
395
458
|
doubled = quote * 2
|
|
396
459
|
->(values){values.collect{|value| quote + value.to_s.gsub(quote, doubled) + quote}.join(joiner) + @row_separator}
|
data/test/dsv_test.rb
CHANGED
|
@@ -442,8 +442,21 @@ describe DSV do
|
|
|
442
442
|
end
|
|
443
443
|
|
|
444
444
|
it "reads a regular expression separator with a quoted field holding the separator" do
|
|
445
|
-
skip "Observation: reassembling a quoted field concatenates the separator back in, which a Regexp cannot be; the parked scanner parser handles it"
|
|
446
445
|
_(DSV.read("a,b\n\"x, y\",2\n", headers: true, column_separator: /,\s*/)).must_equal [{'a' => 'x, y', 'b' => '2'}]
|
|
446
|
+
_(DSV.read("\"x, y\",2\n", column_separator: /,\s*/)).must_equal [{0 => 'x, y', 1 => '2'}]
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
it "reads a regular expression separator under quote: :double" do
|
|
450
|
+
_(DSV.read("\"a\"; \"b\"\n\"x; y\";\"2\"\n", headers: true, quote: :double, column_separator: /;\s*/)).must_equal [{'a' => 'x; y', 'b' => '2'}]
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
it "reads a regular expression separator that captures, without the captures becoming fields" do
|
|
454
|
+
_(DSV.read("1;2,3\n", quote: :none, column_separator: /(;|,)/)).must_equal [{0 => '1', 1 => '2', 2 => '3'}]
|
|
455
|
+
_(DSV.read("\"x;y\",2\n", column_separator: /(;|,)/)).must_equal [{0 => 'x;y', 1 => '2'}]
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
it "reads with a regular expression separator under the spacey quote modes" do
|
|
459
|
+
_(DSV.read("1, 2\n", quote: :spacey_none, column_separator: /,\s*/)).must_equal [{0 => '1', 1 => '2'}]
|
|
447
460
|
end
|
|
448
461
|
end
|
|
449
462
|
|
|
@@ -519,6 +532,20 @@ describe DSV do
|
|
|
519
532
|
it "keeps a separator inside a quoted field under quote: :double" do
|
|
520
533
|
_(DSV.read("a,b\n\"x, y\",\"2\"\n", headers: true, quote: :double)).must_equal [{'a' => 'x, y', 'b' => '2'}]
|
|
521
534
|
end
|
|
535
|
+
|
|
536
|
+
it "reads a row that is neither wholly quoted nor wholly unquoted under quote: :double as it would unspecified" do
|
|
537
|
+
_(DSV.read("\"a\",b,\"c\"\n", quote: :double)).must_equal [{0 => 'a', 1 => 'b', 2 => 'c'}]
|
|
538
|
+
_(DSV.read("1,\"x\"\n", quote: :double)).must_equal [{0 => '1', 1 => 'x'}]
|
|
539
|
+
_(DSV.read("\"a\",\"b\",c\n", quote: :double)).must_equal [{0 => 'a', 1 => 'b', 2 => 'c'}]
|
|
540
|
+
_(DSV.read("a,b\n1,\"x, y\"\n", headers: true, quote: :double)).must_equal [{'a' => '1', 'b' => 'x, y'}]
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
it "reads a quoted field holding the row separator as one field across lines under quote: :double, as DSV writes it" do
|
|
544
|
+
_(DSV.read("\"x\ny\",\"2\"\n\"z\",\"3\"\n", quote: :double)).must_equal [{0 => "x\ny", 1 => '2'}, {0 => 'z', 1 => '3'}]
|
|
545
|
+
_(DSV.read("1,\"x\ny\"\n", quote: :double)).must_equal [{0 => '1', 1 => "x\ny"}]
|
|
546
|
+
output = written(headers: true, columns: ['a', 'b']){|csv| csv.rows = [{'a' => "x\ny", 'b' => '2'}]; csv.write}
|
|
547
|
+
_(DSV.read(output, headers: true, quote: :double)).must_equal [{'a' => "x\ny", 'b' => '2'}]
|
|
548
|
+
end
|
|
522
549
|
end
|
|
523
550
|
|
|
524
551
|
describe "#write" do
|
|
@@ -597,6 +624,11 @@ describe DSV do
|
|
|
597
624
|
_(written(columns: [:a, :b], quote: :spacey_none){|csv| csv.write_row('a' => 1, 'b' => 2)}).must_equal "1, 2\n"
|
|
598
625
|
_(written(columns: [:a, :b], quote: :spacey_double){|csv| csv.write_row('a' => 1, 'b' => 2)}).must_equal "\"1\", \"2\"\n"
|
|
599
626
|
end
|
|
627
|
+
|
|
628
|
+
it "refuses to write with a regular expression separator, there being no one string to write between fields" do
|
|
629
|
+
error = _{written(columns: [:a, :b], column_separator: /,\s*/){|csv| csv.write_row('a' => 1, 'b' => 2)}}.must_raise ArgumentError
|
|
630
|
+
_(error.message).must_match(/Regexp/)
|
|
631
|
+
end
|
|
600
632
|
end
|
|
601
633
|
|
|
602
634
|
describe "DSV::String" do
|
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.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- thoran
|
|
@@ -91,7 +91,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
91
91
|
- !ruby/object:Gem::Version
|
|
92
92
|
version: '0'
|
|
93
93
|
requirements: []
|
|
94
|
-
rubygems_version: 4.0.
|
|
94
|
+
rubygems_version: 4.0.23
|
|
95
95
|
specification_version: 4
|
|
96
96
|
summary: Read and write delimiter-separated values.
|
|
97
97
|
test_files: []
|