dsv 0.12.9 → 0.12.11
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 +1 -1
- data/TODO +38 -18
- data/lib/DSV/VERSION.rb +1 -1
- data/lib/dsv.rb +23 -19
- data/test/dsv_test.rb +53 -0
- 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: 12c41912ef12924ca7177b91aa6d532d4acff23b68073fea18172990fc41cd54
|
|
4
|
+
data.tar.gz: 553305fef6d5b78bea65c39b99f8e72e6dbab070352978c333ce9c277d63dfdc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d89e956eebebc2e15f6e3374bd39e344de12b9999a551db07e15201aefe0921ec4aa62fde5f8f259f5b3b79a6e3a60b9833c9641380913bc1c202036a832ff2a
|
|
7
|
+
data.tar.gz: 2e2f17f71e7651025b05378e90436567ff402870ff815e286c9dd777c11c773d84f249383ff8e4a1b0f817b6d70281c1d90de169970bba2a28b10c5493227af6
|
data/CHANGELOG
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# dsv/CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 20261103
|
|
4
|
+
|
|
5
|
+
0.12.11: the class methods close the files they open, and return an Enumerator without a block.
|
|
6
|
+
|
|
7
|
+
1. ~ DSV.each, .read, .write, .header_row, .first_row, .attributes, .columns: + opened(), so that a path or text given is closed after the call, an early exit included; an IO given is left open. Each had left its file open.
|
|
8
|
+
2. ~ DSV.each: without a block, returns an Enumerator which opens the source afresh on each pass.
|
|
9
|
+
3. ~ DSV.collect, .select, .reject, .detect: by way of the Enumerator. Without a block each had raised NoMethodError.
|
|
10
|
+
4. ~ test/dsv_test.rb: + files_opened(); + the class methods closing what they open, on an early exit too, and leaving an IO open; + the Enumerators.
|
|
11
|
+
5. ~ README.md: Enumerating from the class, closing and the Enumerator.
|
|
12
|
+
6. ~ TODO: - 1.
|
|
13
|
+
7. ~ lib/DSV/VERSION.rb: /0.12.10/0.12.11/.
|
|
14
|
+
|
|
15
|
+
0.12.10: ~ TODO, now grouped by release; + ROADMAP.md.
|
|
16
|
+
|
|
17
|
+
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.
|
|
18
|
+
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.
|
|
19
|
+
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.
|
|
20
|
+
4. ~ lib/DSV/VERSION.rb: /0.12.9/0.12.10/.
|
|
21
|
+
|
|
22
|
+
|
|
3
23
|
## 20261028
|
|
4
24
|
|
|
5
25
|
0.12.9: + DSV::UnterminatedQuote, raised where a quoted field does not close.
|
data/README.md
CHANGED
|
@@ -64,7 +64,7 @@ DSV.detect('people.csv', headers: true){|row| row['name'] == 'Ada'}
|
|
|
64
64
|
DSV.collect('people.csv', headers: true){|row| row['name'].upcase}
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
`foreach`, `map`, `find_all` and `find` are aliases.
|
|
67
|
+
`foreach`, `map`, `find_all` and `find` are aliases. Each closes the file it opens. Without a block, each returns an Enumerator, which opens the file again on each pass, so `DSV.each('people.csv', headers: true).each_slice(100)` works as it would on any Enumerable.
|
|
68
68
|
|
|
69
69
|
### Any delimiter
|
|
70
70
|
|
data/TODO
CHANGED
|
@@ -1,25 +1,45 @@
|
|
|
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. 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.
|
|
6
|
+
2. Reading honours quote: given as a String, as writing does; DSV.parse_line honours quote:.
|
|
7
|
+
3. A space as the column separator splits on each space, not as String#split(' ') does.
|
|
8
|
+
4. :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.
|
|
9
|
+
5. :single read, as :double is with a single quote.
|
|
10
|
+
6. filter, a class-level alias of select.
|
|
11
|
+
7. A String under a write mode taken as a path, so that a file not yet there can be written by name.
|
|
12
|
+
8. The internals private.
|
|
9
13
|
|
|
10
|
-
## 0.13.0,
|
|
14
|
+
## 0.13.0, modes as declarations
|
|
11
15
|
|
|
12
|
-
|
|
16
|
+
9. An unknown quote mode, file mode or option raises ArgumentError, where each is ignored today.
|
|
17
|
+
10. :mixed, the name of the default.
|
|
18
|
+
11. 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
19
|
|
|
14
|
-
##
|
|
20
|
+
## 0.14.0, streaming
|
|
15
21
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
12. 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.
|
|
23
|
+
|
|
24
|
+
## 1.0
|
|
25
|
+
|
|
26
|
+
13. 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.
|
|
27
|
+
14. A benchmark script against stdlib CSV, and the README's speed sentence written from its output.
|
|
28
|
+
15. README: every mode by example, and a table matching each to stdlib CSV's and Python's settings.
|
|
29
|
+
|
|
30
|
+
## 1.x, speed, results unchanged
|
|
31
|
+
|
|
32
|
+
16. A per-row fast path for the default: split, the fields bare or wrapped in two quotes unwrapped, any other row read in full.
|
|
33
|
+
17. as_array: returning the split fields, rather than building a Hash and then an Array from it.
|
|
34
|
+
18. The splitter chosen once, at construction, as row_renderer is for writing.
|
|
35
|
+
19. 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.
|
|
36
|
+
|
|
37
|
+
## 1.x, features
|
|
38
|
+
|
|
39
|
+
20. :rfc_4180, a quote mode reading what RFC 4180 allows as :mixed does and raising on what it forbids.
|
|
40
|
+
21. field_count:, :header or an Integer, off by default: a row of another width raises.
|
|
41
|
+
22. 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.
|
|
42
|
+
23. 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.
|
|
43
|
+
24. The instance-level header_row renamed headers?, kept as an alias; << to append a row and generate to build a string.
|
|
44
|
+
25. Symbols and Strings interchangeable for column names, in a selection, a columns: list and a row's keys.
|
|
45
|
+
26. The duplicate-header modes as an option: keep all as an Array, the default; index, a, a.1; first; last; error.
|
data/lib/DSV/VERSION.rb
CHANGED
data/lib/dsv.rb
CHANGED
|
@@ -53,36 +53,30 @@ class DSV
|
|
|
53
53
|
end
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
# The class-level read, parse and each take a column selection before the options, as the instance-level ones take it as arguments.
|
|
56
|
+
# The class-level read, parse and each take a column selection before the options, as the instance-level ones take it as arguments. Each closes what it opens; without a block, each returns an Enumerator which opens the source afresh on each pass.
|
|
57
57
|
def each(source, *args, &block)
|
|
58
|
+
return to_enum(:each, source, *args) unless block
|
|
58
59
|
options = DSV.extract_options(args)
|
|
59
|
-
|
|
60
|
+
opened(source, options){|dsv| dsv.each(*args, &block)}
|
|
60
61
|
end
|
|
61
62
|
alias_method :foreach, :each
|
|
62
63
|
|
|
63
64
|
def collect(source, *args, &block)
|
|
64
|
-
|
|
65
|
-
each(source, *args){|row| new_collection << block.call(row)}
|
|
66
|
-
new_collection
|
|
65
|
+
each(source, *args).collect(&block)
|
|
67
66
|
end
|
|
68
67
|
alias_method :map, :collect
|
|
69
68
|
|
|
70
69
|
def select(source, *args, &block)
|
|
71
|
-
|
|
72
|
-
each(source, *args){|row| new_collection << row if block.call(row)}
|
|
73
|
-
new_collection
|
|
70
|
+
each(source, *args).select(&block)
|
|
74
71
|
end
|
|
75
72
|
alias_method :find_all, :select
|
|
76
73
|
|
|
77
74
|
def reject(source, *args, &block)
|
|
78
|
-
|
|
79
|
-
each(source, *args){|row| new_collection << row unless block.call(row)}
|
|
80
|
-
new_collection
|
|
75
|
+
each(source, *args).reject(&block)
|
|
81
76
|
end
|
|
82
77
|
|
|
83
78
|
def detect(source, *args, &block)
|
|
84
|
-
each(source, *args)
|
|
85
|
-
nil
|
|
79
|
+
each(source, *args).detect(&block)
|
|
86
80
|
end
|
|
87
81
|
alias_method :find, :detect
|
|
88
82
|
|
|
@@ -91,7 +85,7 @@ class DSV
|
|
|
91
85
|
parse(source, *args, &block)
|
|
92
86
|
else
|
|
93
87
|
options = DSV.extract_options(args)
|
|
94
|
-
|
|
88
|
+
opened(source, options){|dsv| dsv.read_csv(*args)}
|
|
95
89
|
end
|
|
96
90
|
end
|
|
97
91
|
alias_method :read_csv, :read
|
|
@@ -106,25 +100,25 @@ class DSV
|
|
|
106
100
|
alias_method :parse_csv, :parse
|
|
107
101
|
|
|
108
102
|
def write(source, *args)
|
|
109
|
-
|
|
103
|
+
opened(source, *args, &:write_csv)
|
|
110
104
|
end
|
|
111
105
|
alias_method :write_csv, :write
|
|
112
106
|
|
|
113
107
|
# The header row as names, or nil where there is none; first_row is the raw line.
|
|
114
108
|
def header_row(source, *args)
|
|
115
|
-
|
|
109
|
+
opened(source, *args, &:attributes)
|
|
116
110
|
end
|
|
117
111
|
|
|
118
112
|
def first_row(source, *args)
|
|
119
|
-
|
|
113
|
+
opened(source, *args, &:first_row)
|
|
120
114
|
end
|
|
121
115
|
|
|
122
116
|
def attributes(source, *args)
|
|
123
|
-
|
|
117
|
+
opened(source, *args, &:attributes)
|
|
124
118
|
end
|
|
125
119
|
|
|
126
120
|
def columns(source, *args)
|
|
127
|
-
|
|
121
|
+
opened(source, *args, &:columns)
|
|
128
122
|
end
|
|
129
123
|
|
|
130
124
|
def parse_line(raw_row, *args) # For FasterCSV compatibility.
|
|
@@ -135,6 +129,16 @@ class DSV
|
|
|
135
129
|
sc.parse_row(raw_row)
|
|
136
130
|
end
|
|
137
131
|
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
# A DSV on the source, given to the block and closed after it, an early exit included, where the source is a path or text and so was opened here; an IO given is the caller's to close.
|
|
135
|
+
def opened(source, *args)
|
|
136
|
+
dsv = new(source, *args)
|
|
137
|
+
yield dsv
|
|
138
|
+
ensure
|
|
139
|
+
dsv.close if dsv && source.is_a?(::String)
|
|
140
|
+
end
|
|
141
|
+
|
|
138
142
|
end # class << self
|
|
139
143
|
|
|
140
144
|
include Enumerable
|
data/test/dsv_test.rb
CHANGED
|
@@ -60,6 +60,14 @@ describe DSV do
|
|
|
60
60
|
count
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
+
# The files opened while the block runs.
|
|
64
|
+
def files_opened
|
|
65
|
+
files = []
|
|
66
|
+
original = File.method(:new)
|
|
67
|
+
File.stub(:new, ->(*arguments){original.call(*arguments).tap{|file| files << file}}){yield}
|
|
68
|
+
files
|
|
69
|
+
end
|
|
70
|
+
|
|
63
71
|
describe ".read" do
|
|
64
72
|
it "reads rows keyed by position when there is no header row" do
|
|
65
73
|
_(DSV.read("1,2,3\n4,5,6\n")).must_equal POSITIONAL
|
|
@@ -163,6 +171,51 @@ describe DSV do
|
|
|
163
171
|
it "detects nil when no row matches" do
|
|
164
172
|
_(DSV.detect(DATA, headers: true){|row| false}).must_be_nil
|
|
165
173
|
end
|
|
174
|
+
|
|
175
|
+
it "returns an Enumerator without a block" do
|
|
176
|
+
[:collect, :select, :reject, :detect].each{|method| _(DSV.send(method, DATA, headers: true)).must_be_instance_of Enumerator}
|
|
177
|
+
_(DSV.select(DATA, headers: true).each{|row| row['a'] == '4'}).must_equal [KEYED.last]
|
|
178
|
+
_(DSV.detect(DATA, headers: true).each{|row| row['a'] == '4'}).must_equal KEYED.last
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
describe "the class methods, given a path" do
|
|
183
|
+
it "close the file they open" do
|
|
184
|
+
with_file do |path|
|
|
185
|
+
[:each, :collect, :select, :reject, :detect, :read].each do |method|
|
|
186
|
+
_(files_opened{DSV.send(method, path, headers: true){|row| true}}.map(&:closed?)).must_equal [true]
|
|
187
|
+
end
|
|
188
|
+
[:read, :header_row, :first_row, :attributes, :columns].each do |method|
|
|
189
|
+
_(files_opened{DSV.send(method, path, headers: true)}.map(&:closed?)).must_equal [true]
|
|
190
|
+
end
|
|
191
|
+
_(files_opened{DSV.write(path, mode: 'r+', headers: true)}.map(&:closed?)).must_equal [true]
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
it "close the file they open where the block leaves early" do
|
|
196
|
+
with_file do |path|
|
|
197
|
+
_(files_opened{DSV.each(path, headers: true){|row| break}}.map(&:closed?)).must_equal [true]
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
it "return an Enumerator without a block, opening the file afresh on each pass and closing it" do
|
|
202
|
+
with_file do |path|
|
|
203
|
+
rows = nil
|
|
204
|
+
files = files_opened do
|
|
205
|
+
rows = DSV.each(path, headers: true)
|
|
206
|
+
_(rows).must_be_instance_of Enumerator
|
|
207
|
+
_(rows.to_a).must_equal KEYED
|
|
208
|
+
_(rows.first).must_equal KEYED.first
|
|
209
|
+
end
|
|
210
|
+
_(files.map(&:closed?)).must_equal [true, true]
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
it "leave open an IO given rather than a path" do
|
|
215
|
+
io = StringIO.new(DATA)
|
|
216
|
+
DSV.each(io, headers: true){|row| }
|
|
217
|
+
_(io.closed?).must_equal false
|
|
218
|
+
end
|
|
166
219
|
end
|
|
167
220
|
|
|
168
221
|
describe ".header_row, .first_row, .attributes and .columns" do
|