dsv 0.12.10 → 0.12.12

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.
Files changed (8) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +19 -0
  3. data/README.md +10 -1
  4. data/TODO +25 -27
  5. data/lib/DSV/VERSION.rb +1 -1
  6. data/lib/dsv.rb +34 -22
  7. data/test/dsv_test.rb +68 -0
  8. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f88116d5530f25575975779af46933ddd7c67af651b2c61a3894b237b000950a
4
- data.tar.gz: cd03ed4c4b47c12044387544346d8be13ac3f256f1263f135302134bf1ce429d
3
+ metadata.gz: d83c84d7b20fd4f67811f8259313a7c9f7c52cb48430feab0316c96137c3bbd0
4
+ data.tar.gz: 26bf580f0b29c4886796f267a415548bec6354009bede7a12b45b8fbbe3668a5
5
5
  SHA512:
6
- metadata.gz: 0a6e2477a6ff620bba7e5317af9ec1482f0effa853f3ac6f6074e1460170f201b0649db16c1ba67b87621546b986948678d4f6610f2623e29766cb9d948c227d
7
- data.tar.gz: b9ad0b92bcf1ec1caee1604217005250411ac318014ceafb9384ceb70ae276f5d2e457e097a287dcbb5c9d9b668144e8a2c35194d9e7e2a9d28377c5aa3bf8b9
6
+ metadata.gz: 9ee707193d1e740d503ec877f0c079a2868ab3ba848459771760cf96992b002763d6e337e3a9b9ac6f97428c98cf4700b5b32866e00a5729ab3fa50c5ba24094
7
+ data.tar.gz: 007524b8e1cffeb88f7d12b6ebe195b07aaafe85cc060bb1b74c6b372869e944505983ac3629ecabbce84f7f28fecc7e72537133e14ac19e5040dde8020f6344
data/CHANGELOG CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  ## 20261103
4
4
 
5
+ 0.12.12: as_array: with a column selection gives the columns selected.
6
+
7
+ 1. ~ parse_fields(): under as_array:, a selection gives the selected fields; + positions(). Each column not selected had come back as nil, and a selection by position had lost its values.
8
+ 2. ~ DSV#each: under as_array:, selects by position. A selection by name had raised TypeError.
9
+ 3. ~ test/dsv_test.rb: + selections under as_array:, by name, by position, by a repeated name, and from rows already read.
10
+ 4. ~ README.md: + as_array:, and a selection under it.
11
+ 5. ~ TODO: - 1.
12
+ 6. ~ lib/DSV/VERSION.rb: /0.12.11/0.12.12/.
13
+
14
+ 0.12.11: the class methods close the files they open, and return an Enumerator without a block.
15
+
16
+ 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.
17
+ 2. ~ DSV.each: without a block, returns an Enumerator which opens the source afresh on each pass.
18
+ 3. ~ DSV.collect, .select, .reject, .detect: by way of the Enumerator. Without a block each had raised NoMethodError.
19
+ 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.
20
+ 5. ~ README.md: Enumerating from the class, closing and the Enumerator.
21
+ 6. ~ TODO: - 1.
22
+ 7. ~ lib/DSV/VERSION.rb: /0.12.10/0.12.11/.
23
+
5
24
  0.12.10: ~ TODO, now grouped by release; + ROADMAP.md.
6
25
 
7
26
  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.
data/README.md CHANGED
@@ -40,6 +40,13 @@ DSV.read("1,2,3\n4,5,6\n")
40
40
  # => [{0 => "1", 1 => "2", 2 => "3"}, {0 => "4", 1 => "5", 2 => "6"}]
41
41
  ```
42
42
 
43
+ With `as_array: true` each row is an Array of its values in column order, the form stdlib CSV gives:
44
+
45
+ ```ruby
46
+ DSV.read("1,2,3\n4,5,6\n", as_array: true)
47
+ # => [["1", "2", "3"], ["4", "5", "6"]]
48
+ ```
49
+
43
50
  A String that names an existing file is read as a file; any other String is read as text.
44
51
 
45
52
  ### Selecting columns
@@ -53,6 +60,8 @@ DSV.read('people.csv', 'name', headers: true)
53
60
  DSV.read('people.csv', headers: true, selected_columns: ['name'])
54
61
  ```
55
62
 
63
+ Under `as_array:` the Arrays hold the columns selected, in column order.
64
+
56
65
  ### Enumerating from the class
57
66
 
58
67
  The Enumerable calls are available on a source directly, each taking the same options:
@@ -64,7 +73,7 @@ DSV.detect('people.csv', headers: true){|row| row['name'] == 'Ada'}
64
73
  DSV.collect('people.csv', headers: true){|row| row['name'].upcase}
65
74
  ```
66
75
 
67
- `foreach`, `map`, `find_all` and `find` are aliases.
76
+ `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
77
 
69
78
  ### Any delimiter
70
79
 
data/TODO CHANGED
@@ -2,45 +2,43 @@
2
2
 
3
3
  ## 0.12.x, fixes
4
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.
5
+ 1. Reading honours quote: given as a String, as writing does; DSV.parse_line honours quote:.
6
+ 2. A space as the column separator splits on each space, not as String#split(' ') does.
7
+ 3. :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.
8
+ 4. :single read, as :double is with a single quote.
9
+ 5. filter, a class-level alias of select.
10
+ 6. A String under a write mode taken as a path, so that a file not yet there can be written by name.
11
+ 7. The internals private.
14
12
 
15
13
  ## 0.13.0, modes as declarations
16
14
 
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.
15
+ 8. An unknown quote mode, file mode or option raises ArgumentError, where each is ignored today.
16
+ 9. :mixed, the name of the default.
17
+ 10. 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.
20
18
 
21
19
  ## 0.14.0, streaming
22
20
 
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.
21
+ 11. 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
22
 
25
23
  ## 1.0
26
24
 
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.
25
+ 12. 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.
26
+ 13. A benchmark script against stdlib CSV, and the README's speed sentence written from its output.
27
+ 14. README: every mode by example, and a table matching each to stdlib CSV's and Python's settings.
30
28
 
31
29
  ## 1.x, speed, results unchanged
32
30
 
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.
31
+ 15. A per-row fast path for the default: split, the fields bare or wrapped in two quotes unwrapped, any other row read in full.
32
+ 16. as_array: returning the split fields, rather than building a Hash and then an Array from it.
33
+ 17. The splitter chosen once, at construction, as row_renderer is for writing.
34
+ 18. 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
35
 
38
36
  ## 1.x, features
39
37
 
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.
38
+ 19. :rfc_4180, a quote mode reading what RFC 4180 allows as :mixed does and raising on what it forbids.
39
+ 20. field_count:, :header or an Integer, off by default: a row of another width raises.
40
+ 21. 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.
41
+ 22. 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.
42
+ 23. The instance-level header_row renamed headers?, kept as an alias; << to append a row and generate to build a string.
43
+ 24. Symbols and Strings interchangeable for column names, in a selection, a columns: list and a row's keys.
44
+ 25. 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
@@ -2,5 +2,5 @@
2
2
  # DSV::VERSION
3
3
 
4
4
  class DSV
5
- VERSION = '0.12.10'
5
+ VERSION = '0.12.12'
6
6
  end
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
- new(source, options).each(*args, &block)
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
- new_collection = []
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
- new_collection = []
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
- new_collection = []
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){|row| return row if block.call(row)}
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
- new(source, options).read_csv(*args)
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
- new(source, *args).write_csv
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
- new(source, *args).attributes
109
+ opened(source, *args, &:attributes)
116
110
  end
117
111
 
118
112
  def first_row(source, *args)
119
- new(source, *args).first_row
113
+ opened(source, *args, &:first_row)
120
114
  end
121
115
 
122
116
  def attributes(source, *args)
123
- new(source, *args).attributes
117
+ opened(source, *args, &:attributes)
124
118
  end
125
119
 
126
120
  def columns(source, *args)
127
- new(source, *args).columns
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
@@ -384,6 +388,8 @@ class DSV
384
388
  end
385
389
 
386
390
  def parse_fields(fields, *selected_columns)
391
+ selected_columns.flatten!
392
+ return fields.values_at(*positions(selected_columns)) if @as_array && !selected_columns.empty?
387
393
  parsed_row = {}
388
394
  i = -1
389
395
  if selected_columns.empty?
@@ -395,7 +401,6 @@ class DSV
395
401
  fields.each{|column_value| store_field(parsed_row, attributes[i += 1], column_value)}
396
402
  end
397
403
  else
398
- selected_columns.flatten!
399
404
  case selected_columns[0]
400
405
  when Integer
401
406
  fields.each{|column_value| parsed_row[i] = column_value unless !selected_columns.include?(i += 1)}
@@ -414,6 +419,11 @@ class DSV
414
419
  end
415
420
  end
416
421
 
422
+ # The positions of the columns selected, in column order, a repeated name giving each of its own.
423
+ def positions(selected_columns)
424
+ selected_columns.flat_map{|column| column.is_a?(::Integer) ? column : Array((columns || {})[column])}.sort
425
+ end
426
+
417
427
  # With no columns defined, rows keyed by name supply them, and the header, from the first row's keys; rows keyed by position write positionally and have no header to write.
418
428
  def write(*selected_columns)
419
429
  prepare_to_rewrite if @mode == 'r+'
@@ -488,14 +498,16 @@ class DSV
488
498
  if @rows[0]
489
499
  if selected_columns.empty?
490
500
  @rows.each{|row| yield row}
501
+ elsif @as_array
502
+ @rows.each{|row| yield row.values_at(*positions(selected_columns))}
491
503
  else
492
504
  @rows.each do |row|
493
505
  yield selected_columns.inject({}){|hash, column_name| hash[column_name] = row[column_name]; hash}
494
506
  end
495
507
  end
496
508
  else
497
- if selected_columns.empty?
498
- read_csv.each{|row| yield row}
509
+ if selected_columns.empty? || @as_array
510
+ read_csv(selected_columns).each{|row| yield row}
499
511
  else
500
512
  read_csv(selected_columns).each do |row|
501
513
  yield selected_columns.inject({}){|hash, column_name| hash[column_name] = row[column_name]; hash}
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
@@ -110,6 +118,15 @@ describe DSV do
110
118
  _(yielded(:each, DATA, 'a', headers: true)).must_equal [{'a' => '1'}, {'a' => '4'}]
111
119
  _(yielded(:parse, DATA, 'b', headers: true)).must_equal [{'b' => '2'}, {'b' => '5'}]
112
120
  end
121
+
122
+ it "gives only the columns selected under as_array:, in column order" do
123
+ _(DSV.read(DATA, 'a', headers: true, as_array: true)).must_equal [['1'], ['4']]
124
+ _(DSV.read(DATA, ['c', 'a'], headers: true, as_array: true)).must_equal [['1', '3'], ['4', '6']]
125
+ _(DSV.read(DATA, headers: true, as_array: true, selected_columns: ['b'])).must_equal [['2'], ['5']]
126
+ _(DSV.read("1,2,3\n4,5,6\n", 1, as_array: true)).must_equal [['2'], ['5']]
127
+ _(DSV.read("a,a,b\n1,2,3\n", 'a', headers: true, as_array: true)).must_equal [['1', '2']]
128
+ _(yielded(:each, DATA, 'a', headers: true, as_array: true)).must_equal [['1'], ['4']]
129
+ end
113
130
  end
114
131
 
115
132
  describe ".parse" do
@@ -163,6 +180,51 @@ describe DSV do
163
180
  it "detects nil when no row matches" do
164
181
  _(DSV.detect(DATA, headers: true){|row| false}).must_be_nil
165
182
  end
183
+
184
+ it "returns an Enumerator without a block" do
185
+ [:collect, :select, :reject, :detect].each{|method| _(DSV.send(method, DATA, headers: true)).must_be_instance_of Enumerator}
186
+ _(DSV.select(DATA, headers: true).each{|row| row['a'] == '4'}).must_equal [KEYED.last]
187
+ _(DSV.detect(DATA, headers: true).each{|row| row['a'] == '4'}).must_equal KEYED.last
188
+ end
189
+ end
190
+
191
+ describe "the class methods, given a path" do
192
+ it "close the file they open" do
193
+ with_file do |path|
194
+ [:each, :collect, :select, :reject, :detect, :read].each do |method|
195
+ _(files_opened{DSV.send(method, path, headers: true){|row| true}}.map(&:closed?)).must_equal [true]
196
+ end
197
+ [:read, :header_row, :first_row, :attributes, :columns].each do |method|
198
+ _(files_opened{DSV.send(method, path, headers: true)}.map(&:closed?)).must_equal [true]
199
+ end
200
+ _(files_opened{DSV.write(path, mode: 'r+', headers: true)}.map(&:closed?)).must_equal [true]
201
+ end
202
+ end
203
+
204
+ it "close the file they open where the block leaves early" do
205
+ with_file do |path|
206
+ _(files_opened{DSV.each(path, headers: true){|row| break}}.map(&:closed?)).must_equal [true]
207
+ end
208
+ end
209
+
210
+ it "return an Enumerator without a block, opening the file afresh on each pass and closing it" do
211
+ with_file do |path|
212
+ rows = nil
213
+ files = files_opened do
214
+ rows = DSV.each(path, headers: true)
215
+ _(rows).must_be_instance_of Enumerator
216
+ _(rows.to_a).must_equal KEYED
217
+ _(rows.first).must_equal KEYED.first
218
+ end
219
+ _(files.map(&:closed?)).must_equal [true, true]
220
+ end
221
+ end
222
+
223
+ it "leave open an IO given rather than a path" do
224
+ io = StringIO.new(DATA)
225
+ DSV.each(io, headers: true){|row| }
226
+ _(io.closed?).must_equal false
227
+ end
166
228
  end
167
229
 
168
230
  describe ".header_row, .first_row, .attributes and .columns" do
@@ -362,6 +424,12 @@ describe DSV do
362
424
  end
363
425
 
364
426
  describe "#each" do
427
+ it "selects from rows already read under as_array:, in column order" do
428
+ csv = DSV.new(DATA, headers: true, as_array: true)
429
+ csv.read
430
+ _(csv.each(['c', 'a']).to_a).must_equal [['1', '3'], ['4', '6']]
431
+ end
432
+
365
433
  it "yields each row, reading the source first if it has not been read" do
366
434
  rows = []
367
435
  DSV.new(DATA, headers: true).each{|row| rows << row}
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.10
4
+ version: 0.12.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - thoran