dsv 0.12.4
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 +7 -0
- data/CHANGELOG +951 -0
- data/Gemfile +5 -0
- data/LICENSE +21 -0
- data/README.md +140 -0
- data/Rakefile +22 -0
- data/TODO +24 -0
- data/dsv.gemspec +43 -0
- data/lib/DSV/File.rb +35 -0
- data/lib/DSV/String.rb +19 -0
- data/lib/DSV/VERSION.rb +6 -0
- data/lib/dsv.rb +472 -0
- data/test/VERSION_test.rb +20 -0
- data/test/dsv_test.rb +702 -0
- data/test/gemspec_test.rb +42 -0
- data/test/helper.rb +9 -0
- metadata +97 -0
data/test/dsv_test.rb
ADDED
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
# dsv_test.rb
|
|
2
|
+
|
|
3
|
+
# 20260908, 09
|
|
4
|
+
|
|
5
|
+
# A specification of DSV's interface: what it does today and should go on
|
|
6
|
+
# doing, and what it should do and does not yet. The second kind is skipped, the
|
|
7
|
+
# skip message naming the finding in the Handoff 0 closing note
|
|
8
|
+
# (dsv/closing-note.md) or the observation that it answers, so that the skip
|
|
9
|
+
# count is the fault list and each fix is one skip removed.
|
|
10
|
+
#
|
|
11
|
+
# Four findings have no example, since each waits on a decision that the test
|
|
12
|
+
# would prejudge: 5 (duplicate header names, Decision point 6), 9 (header_row as
|
|
13
|
+
# a class method, Decision point 9), 12 (selected_columns:, Decision point 8) and
|
|
14
|
+
# 17 (writing with no columns defined, Decision point 7). An example whose skip
|
|
15
|
+
# message names a decision point states the option the closing note leans to
|
|
16
|
+
# and is provisional until the decision is recorded.
|
|
17
|
+
#
|
|
18
|
+
# The RFC 4180 cases are taken from stdlib CSV's parsing tests, rewritten here.
|
|
19
|
+
# One convention differs from CSV throughout: an empty field reads as "" rather
|
|
20
|
+
# than nil, as it does in DSV today. Whether that stays is part of the
|
|
21
|
+
# open question of surface compatibility with CSV.
|
|
22
|
+
|
|
23
|
+
require_relative './helper'
|
|
24
|
+
|
|
25
|
+
DATA = "a,b,c\n1,2,3\n4,5,6\n"
|
|
26
|
+
KEYED = [{'a' => '1', 'b' => '2', 'c' => '3'}, {'a' => '4', 'b' => '5', 'c' => '6'}]
|
|
27
|
+
POSITIONAL = [{0 => '1', 1 => '2', 2 => '3'}, {0 => '4', 1 => '5', 2 => '6'}]
|
|
28
|
+
ARRAYS = [['1', '2', '3'], ['4', '5', '6']]
|
|
29
|
+
|
|
30
|
+
describe DSV do
|
|
31
|
+
# A writer over a StringIO, returning what was written.
|
|
32
|
+
def written(*arguments)
|
|
33
|
+
io = StringIO.new
|
|
34
|
+
csv = DSV.new(io, *arguments)
|
|
35
|
+
yield csv
|
|
36
|
+
io.string
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The rows a block-taking class method yields.
|
|
40
|
+
def yielded(method, *arguments)
|
|
41
|
+
rows = []
|
|
42
|
+
DSV.send(method, *arguments){|row| rows << row}
|
|
43
|
+
rows
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# A temporary file holding DATA, given to the block by path.
|
|
47
|
+
def with_file(contents = DATA)
|
|
48
|
+
Dir.mktmpdir('DSV') do |directory|
|
|
49
|
+
path = File.join(directory, 'data.csv')
|
|
50
|
+
File.write(path, contents)
|
|
51
|
+
yield path, directory
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# How many times a class's new is called while the block runs.
|
|
56
|
+
def constructions(klass)
|
|
57
|
+
count = 0
|
|
58
|
+
original = klass.method(:new)
|
|
59
|
+
klass.stub(:new, ->(*arguments, &block){count += 1; original.call(*arguments, &block)}){yield}
|
|
60
|
+
count
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
describe ".read" do
|
|
64
|
+
it "reads rows keyed by position when there is no header row" do
|
|
65
|
+
_(DSV.read("1,2,3\n4,5,6\n")).must_equal POSITIONAL
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "reads rows keyed by column name when headers: is true" do
|
|
69
|
+
_(DSV.read(DATA, headers: true)).must_equal KEYED
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it "accepts header_row: and header: as spellings of headers:" do
|
|
73
|
+
_(DSV.read(DATA, header_row: true)).must_equal KEYED
|
|
74
|
+
_(DSV.read(DATA, header: true)).must_equal KEYED
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it "keys by string, so header names are strings" do
|
|
78
|
+
_(DSV.read(DATA, headers: true).first.keys).must_equal ['a', 'b', 'c']
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "reads rows as arrays when as_array: is true" do
|
|
82
|
+
_(DSV.read(DATA, headers: true, as_array: true)).must_equal ARRAYS
|
|
83
|
+
_(DSV.read("1,2,3\n4,5,6\n", as_array: true)).must_equal ARRAYS
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "names the columns of a headerless source from columns: given as an array" do
|
|
87
|
+
_(DSV.read("1,2,3\n4,5,6\n", columns: [:x, :y, :z])).must_equal [{'x' => '1', 'y' => '2', 'z' => '3'}, {'x' => '4', 'y' => '5', 'z' => '6'}]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it "maps names to positions from columns: given as a hash" do
|
|
91
|
+
rows = DSV.read("1,2,3\n4,5,6\n", columns: {x: 0, z: 2})
|
|
92
|
+
_(rows.map{|row| row.values_at('x', 'z')}).must_equal [['1', '2'], ['4', '5']]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "reads an empty source as no rows" do
|
|
96
|
+
_(DSV.read('', headers: true)).must_equal []
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it "yields each row instead of returning them when given a block" do
|
|
100
|
+
_(yielded(:read, DATA, headers: true)).must_equal KEYED
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it "is aliased read_csv" do
|
|
104
|
+
_(DSV.read_csv(DATA, headers: true)).must_equal KEYED
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it "selects columns given as arguments, as #read does" do
|
|
108
|
+
_(DSV.read(DATA, 'a', headers: true)).must_equal [{'a' => '1'}, {'a' => '4'}]
|
|
109
|
+
_(DSV.read(DATA, ['a', 'c'], headers: true)).must_equal [{'a' => '1', 'c' => '3'}, {'a' => '4', 'c' => '6'}]
|
|
110
|
+
_(yielded(:each, DATA, 'a', headers: true)).must_equal [{'a' => '1'}, {'a' => '4'}]
|
|
111
|
+
_(yielded(:parse, DATA, 'b', headers: true)).must_equal [{'b' => '2'}, {'b' => '5'}]
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
describe ".parse" do
|
|
116
|
+
it "reads like .read without a block" do
|
|
117
|
+
_(DSV.parse(DATA, headers: true)).must_equal KEYED
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it "yields each row with a block" do
|
|
121
|
+
_(yielded(:parse, DATA, headers: true)).must_equal KEYED
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it "is aliased parse_csv" do
|
|
125
|
+
_(DSV.parse_csv(DATA, headers: true)).must_equal KEYED
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
describe ".each" do
|
|
130
|
+
it "yields each row" do
|
|
131
|
+
_(yielded(:each, DATA, headers: true)).must_equal KEYED
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it "returns the rows" do
|
|
135
|
+
_(DSV.each(DATA, headers: true){|row| }).must_equal KEYED
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it "is aliased foreach" do
|
|
139
|
+
_(yielded(:foreach, DATA, headers: true)).must_equal KEYED
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
describe ".collect, .select, .reject and .detect" do
|
|
144
|
+
it "collects the block value for each row, also as map" do
|
|
145
|
+
_(DSV.collect(DATA, headers: true){|row| row['a'].to_i}).must_equal [1, 4]
|
|
146
|
+
_(DSV.map(DATA, headers: true){|row| row['a'].to_i}).must_equal [1, 4]
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it "selects the rows for which the block is true, also as find_all" do
|
|
150
|
+
_(DSV.select(DATA, headers: true){|row| row['a'] == '4'}).must_equal [KEYED.last]
|
|
151
|
+
_(DSV.find_all(DATA, headers: true){|row| row['a'] == '4'}).must_equal [KEYED.last]
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
it "rejects the rows for which the block is true" do
|
|
155
|
+
_(DSV.reject(DATA, headers: true){|row| row['a'] == '4'}).must_equal [KEYED.first]
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
it "detects the first row for which the block is true, also as find" do
|
|
159
|
+
_(DSV.detect(DATA, headers: true){|row| row['a'] == '4'}).must_equal KEYED.last
|
|
160
|
+
_(DSV.find(DATA, headers: true){|row| row['a'] == '4'}).must_equal KEYED.last
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
it "detects nil when no row matches" do
|
|
164
|
+
_(DSV.detect(DATA, headers: true){|row| false}).must_be_nil
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
describe ".header_row, .first_row, .attributes and .columns" do
|
|
169
|
+
it "header_row returns the header row as names, or nil without one" do
|
|
170
|
+
_(DSV.header_row(DATA, headers: true)).must_equal ['a', 'b', 'c']
|
|
171
|
+
_(DSV.header_row(DATA)).must_be_nil
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
it "first_row returns the first line, separator included" do
|
|
175
|
+
_(DSV.first_row(DATA, headers: true)).must_equal "a,b,c\n"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
it "attributes returns the column names in order, or nil without a header row" do
|
|
179
|
+
_(DSV.attributes(DATA, headers: true)).must_equal ['a', 'b', 'c']
|
|
180
|
+
_(DSV.attributes(DATA)).must_be_nil
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
it "columns returns the column names mapped to positions, or nil without a header row" do
|
|
184
|
+
_(DSV.columns(DATA, headers: true)).must_equal({'a' => 0, 'b' => 1, 'c' => 2})
|
|
185
|
+
_(DSV.columns(DATA)).must_be_nil
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
describe ".parse_line" do
|
|
190
|
+
it "splits one line into an array of fields" do
|
|
191
|
+
_(DSV.parse_line("1,2,3\n")).must_equal ['1', '2', '3']
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
it "strips quotes from quoted fields" do
|
|
195
|
+
_(DSV.parse_line("\"1\",\"2\"\n")).must_equal ['1', '2']
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
it "takes col_sep: and row_sep:" do
|
|
199
|
+
_(DSV.parse_line("1\t2\t3\n", col_sep: "\t")).must_equal ['1', '2', '3']
|
|
200
|
+
_(DSV.parse_line("1,2,3\r\n", row_sep: "\r\n")).must_equal ['1', '2', '3']
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
it "leaves the caller's string as it was" do
|
|
204
|
+
line = "1,2,3\n"
|
|
205
|
+
DSV.parse_line(line)
|
|
206
|
+
_(line).must_equal "1,2,3\n"
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
describe "parsing per RFC 4180" do
|
|
211
|
+
it "reads unquoted fields, quoted fields, an empty quoted field and a lone field" do
|
|
212
|
+
_(DSV.parse_line("\t")).must_equal ["\t"]
|
|
213
|
+
_(DSV.parse_line('foo')).must_equal ['foo']
|
|
214
|
+
_(DSV.parse_line('""')).must_equal ['']
|
|
215
|
+
_(DSV.parse_line('foo,"",baz')).must_equal ['foo', '', 'baz']
|
|
216
|
+
_(DSV.parse_line('"1997","Ford","E350"')).must_equal ['1997', 'Ford', 'E350']
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
it "keeps spaces around a field, since they are part of it" do
|
|
220
|
+
_(DSV.parse_line('1997, Ford , E350')).must_equal ['1997', ' Ford ', ' E350']
|
|
221
|
+
_(DSV.parse_line('1997,Ford,E350," Super luxurious truck "')).must_equal ['1997', 'Ford', 'E350', ' Super luxurious truck ']
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
it "reads an empty field between or before others as an empty string" do
|
|
225
|
+
_(DSV.parse_line('foo,,baz')).must_equal ['foo', '', 'baz']
|
|
226
|
+
_(DSV.parse_line(',foo,bar')).must_equal ['', 'foo', 'bar']
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
it "reads a separator inside a quoted field" do
|
|
230
|
+
_(DSV.parse_line('"a, b",c')).must_equal ['a, b', 'c']
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
it "reads a line break inside a quoted field of one line" do
|
|
234
|
+
_(DSV.parse_line("foo,\"\r\n\",baz")).must_equal ['foo', "\r\n", 'baz']
|
|
235
|
+
_(DSV.parse_line("foo,\"\n\",baz")).must_equal ['foo', "\n", 'baz']
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
it "reads a doubled quote inside a quoted field as one quote" do
|
|
239
|
+
_(DSV.parse_line('"say ""hi""",2')).must_equal ['say "hi"', '2']
|
|
240
|
+
_(DSV.parse_line('foo,"""bar""",baz')).must_equal ['foo', '"bar"', 'baz']
|
|
241
|
+
_(DSV.parse_line('foo,"""",baz')).must_equal ['foo', '"', 'baz']
|
|
242
|
+
_(DSV.parse_line('foo,"""""",baz')).must_equal ['foo', '""', 'baz']
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
it "reads a trailing empty field as an empty string" do
|
|
246
|
+
_(DSV.parse_line('foo,bar,')).must_equal ['foo', 'bar', '']
|
|
247
|
+
_(DSV.parse_line(',')).must_equal ['', '']
|
|
248
|
+
_(DSV.parse_line(',,')).must_equal ['', '', '']
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
it "reads more than one separator inside a quoted field, and a field that is only a separator" do
|
|
252
|
+
_(DSV.parse_line('"a, b, c",d')).must_equal ['a, b, c', 'd']
|
|
253
|
+
_(DSV.parse_line('","')).must_equal [',']
|
|
254
|
+
_(DSV.parse_line('",",","')).must_equal [',', ',']
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
it "reads a quoted field holding the row separator as one field across lines" do
|
|
258
|
+
_(DSV.read("a,b\n\"x\ny\",2\n", headers: true)).must_equal [{'a' => "x\ny", 'b' => '2'}]
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
describe ".open" do
|
|
263
|
+
it "returns an instance without a block" do
|
|
264
|
+
csv = DSV.open(DATA, headers: true)
|
|
265
|
+
_(csv).must_be_instance_of DSV
|
|
266
|
+
_(csv.read).must_equal KEYED
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
it "yields the instance, closes it and returns it with a block" do
|
|
270
|
+
given = nil
|
|
271
|
+
returned = DSV.open(DATA, headers: true){|csv| given = csv}
|
|
272
|
+
_(returned).must_be_same_as given
|
|
273
|
+
_(given.instance_variable_get(:@source).closed?).must_equal true
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
it "keeps no instance in the class between calls" do
|
|
277
|
+
DSV.open(DATA)
|
|
278
|
+
_(DSV.instance_variable_defined?(:@csv_file)).must_equal false
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
describe ".source_type" do
|
|
283
|
+
it "is DSV::File for the path of an existing file and DSV::String for text" do
|
|
284
|
+
with_file{|path| _(DSV.source_type(path)).must_equal DSV::File}
|
|
285
|
+
_(DSV.source_type(DATA)).must_equal DSV::String
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
describe ".new" do
|
|
290
|
+
it "takes an IO as the source directly" do
|
|
291
|
+
_(DSV.new(StringIO.new(DATA), headers: true).read).must_equal KEYED
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
it "reads only the selected_columns: given at construction when a call selects none, a call's own selection winning" do
|
|
295
|
+
csv = DSV.new(DATA, headers: true, selected_columns: ['a'])
|
|
296
|
+
_(csv.read).must_equal [{'a' => '1'}, {'a' => '4'}]
|
|
297
|
+
_(csv.read('b')).must_equal [{'b' => '2'}, {'b' => '5'}]
|
|
298
|
+
_(DSV.new(DATA, headers: true, selected_columns: 'c').each.to_a).must_equal [{'c' => '3'}, {'c' => '6'}]
|
|
299
|
+
_(DSV.read(DATA, headers: true, selected_columns: [0, 2])).must_equal [{0 => '1', 2 => '3'}, {0 => '4', 2 => '6'}]
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
it "exposes the options as accessors, quote defaulting to nil" do
|
|
303
|
+
csv = DSV.new(DATA, headers: true, mode: 'r', quote: :none, row_separator: "\n", as_array: false)
|
|
304
|
+
_(csv.header_row).must_equal true
|
|
305
|
+
_(csv.header_row?).must_equal true
|
|
306
|
+
_(csv.mode).must_equal 'r'
|
|
307
|
+
_(csv.quote).must_equal :none
|
|
308
|
+
_(csv.row_separator).must_equal "\n"
|
|
309
|
+
_(csv.as_array).must_equal false
|
|
310
|
+
_(DSV.new(DATA).header_row?).must_equal false
|
|
311
|
+
_(DSV.new(DATA).quote).must_be_nil
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
it "reads the header row under mode: given as a symbol or a long name" do
|
|
315
|
+
_(DSV.new(DATA, headers: true, mode: :r).read).must_equal KEYED
|
|
316
|
+
with_file{|path| _(DSV.new(path, headers: true, mode: :read_only).read).must_equal KEYED}
|
|
317
|
+
with_file{|path| _(DSV.new(path, headers: true, mode: 'rw').read).must_equal KEYED}
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
describe "#read" do
|
|
322
|
+
it "returns the rows and keeps them in rows" do
|
|
323
|
+
csv = DSV.new(DATA, headers: true)
|
|
324
|
+
_(csv.rows).must_equal []
|
|
325
|
+
_(csv.read).must_equal KEYED
|
|
326
|
+
_(csv.rows).must_equal KEYED
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
it "selects columns by name, given singly or as an array" do
|
|
330
|
+
_(DSV.new(DATA, headers: true).read('a')).must_equal [{'a' => '1'}, {'a' => '4'}]
|
|
331
|
+
_(DSV.new(DATA, headers: true).read('a', 'c')).must_equal [{'a' => '1', 'c' => '3'}, {'a' => '4', 'c' => '6'}]
|
|
332
|
+
_(DSV.new(DATA, headers: true).read(['a', 'c'])).must_equal [{'a' => '1', 'c' => '3'}, {'a' => '4', 'c' => '6'}]
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
it "selects columns by position, keyed by position even under a header row" do
|
|
336
|
+
_(DSV.new("1,2,3\n4,5,6\n").read(0, 2)).must_equal [{0 => '1', 2 => '3'}, {0 => '4', 2 => '6'}]
|
|
337
|
+
_(DSV.new(DATA, headers: true).read(0, 2)).must_equal [{0 => '1', 2 => '3'}, {0 => '4', 2 => '6'}]
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
it "yields each row with a block, as does #parse" do
|
|
341
|
+
rows = []
|
|
342
|
+
DSV.new(DATA, headers: true).read{|row| rows << row}
|
|
343
|
+
_(rows).must_equal KEYED
|
|
344
|
+
_(DSV.new(DATA, headers: true).parse).must_equal KEYED
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
it "gives a short row only the columns it has" do
|
|
348
|
+
_(DSV.read("a,b,c\n1,2\n", headers: true)).must_equal [{'a' => '1', 'b' => '2'}]
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
it "returns the same rows when read again" do
|
|
352
|
+
csv = DSV.new(DATA, headers: true)
|
|
353
|
+
csv.read
|
|
354
|
+
_(csv.read).must_equal KEYED
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
describe "#each" do
|
|
359
|
+
it "yields each row, reading the source first if it has not been read" do
|
|
360
|
+
rows = []
|
|
361
|
+
DSV.new(DATA, headers: true).each{|row| rows << row}
|
|
362
|
+
_(rows).must_equal KEYED
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
it "yields only the selected columns, before or after a read" do
|
|
366
|
+
rows = []
|
|
367
|
+
DSV.new(DATA, headers: true).each('a'){|row| rows << row}
|
|
368
|
+
_(rows).must_equal [{'a' => '1'}, {'a' => '4'}]
|
|
369
|
+
csv = DSV.new(DATA, headers: true)
|
|
370
|
+
csv.read
|
|
371
|
+
rows = []
|
|
372
|
+
csv.each('a', 'c'){|row| rows << row}
|
|
373
|
+
_(rows).must_equal [{'a' => '1', 'c' => '3'}, {'a' => '4', 'c' => '6'}]
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
it "is aliased each_row and backs Enumerable" do
|
|
377
|
+
_(DSV.include?(Enumerable)).must_equal true
|
|
378
|
+
_(DSV.new(DATA, headers: true).map{|row| row['a']}).must_equal ['1', '4']
|
|
379
|
+
_(DSV.new(DATA, headers: true).count).must_equal 2
|
|
380
|
+
_(DSV.new(DATA, headers: true).first).must_equal KEYED.first
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
it "returns an enumerator without a block" do
|
|
384
|
+
_(DSV.new(DATA, headers: true).each).must_be_kind_of Enumerator
|
|
385
|
+
_(DSV.new(DATA, headers: true).each.to_a).must_equal KEYED
|
|
386
|
+
_(DSV.new(DATA, headers: true).each('a').to_a).must_equal [{'a' => '1'}, {'a' => '4'}]
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
describe "#to_a" do
|
|
391
|
+
it "returns the rows as arrays in column order" do
|
|
392
|
+
_(DSV.new(DATA, headers: true).to_a).must_equal ARRAYS
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
it "returns rows as read when as_array: is true" do
|
|
396
|
+
_(DSV.new(DATA, headers: true, as_array: true).to_a).must_equal ARRAYS
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
it "returns the rows as arrays in position order when there are no columns" do
|
|
400
|
+
_(DSV.new("1,2\n3,4\n").to_a).must_equal [['1', '2'], ['3', '4']]
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
describe "#columns= and #attributes=" do
|
|
405
|
+
it "columns= takes names in order, or a hash of name to position, as strings" do
|
|
406
|
+
csv = DSV.new("1,2\n")
|
|
407
|
+
csv.columns = [:p, :q]
|
|
408
|
+
_(csv.columns).must_equal({'p' => 0, 'q' => 1})
|
|
409
|
+
_(csv.attributes).must_equal ['p', 'q']
|
|
410
|
+
_(csv.read).must_equal [{'p' => '1', 'q' => '2'}]
|
|
411
|
+
csv = DSV.new("1,2\n")
|
|
412
|
+
csv.columns = {p: 0, q: 1}
|
|
413
|
+
_(csv.columns).must_equal({'p' => 0, 'q' => 1})
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
it "attributes= sets the names without setting columns, so rows stay positional" do
|
|
417
|
+
csv = DSV.new("1,2\n")
|
|
418
|
+
csv.attributes = ['p', 'q']
|
|
419
|
+
_(csv.attributes).must_equal ['p', 'q']
|
|
420
|
+
_(csv.columns).must_be_nil
|
|
421
|
+
_(csv.read).must_equal [{0 => '1', 1 => '2'}]
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
describe "column separators on read" do
|
|
426
|
+
it "reads a tab, given as column_separator: or col_sep:" do
|
|
427
|
+
_(DSV.read("a\tb\n1\t2\n", headers: true, column_separator: "\t")).must_equal [{'a' => '1', 'b' => '2'}]
|
|
428
|
+
_(DSV.read("a\tb\n1\t2\n", headers: true, col_sep: "\t")).must_equal [{'a' => '1', 'b' => '2'}]
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
it "reads a pipe and a semicolon" do
|
|
432
|
+
_(DSV.read("a|b\n1|2\n", headers: true, column_separator: '|')).must_equal [{'a' => '1', 'b' => '2'}]
|
|
433
|
+
_(DSV.read("a;b\n1;2\n", headers: true, column_separator: ';')).must_equal [{'a' => '1', 'b' => '2'}]
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
it "reads a multi-character separator" do
|
|
437
|
+
_(DSV.read("a::b\n1::2\n", headers: true, column_separator: '::')).must_equal [{'a' => '1', 'b' => '2'}]
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
it "reads a regular expression separator" do
|
|
441
|
+
_(DSV.read("a, b\n1,2\n", headers: true, column_separator: /,\s*/)).must_equal [{'a' => '1', 'b' => '2'}]
|
|
442
|
+
end
|
|
443
|
+
|
|
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
|
+
_(DSV.read("a,b\n\"x, y\",2\n", headers: true, column_separator: /,\s*/)).must_equal [{'a' => 'x, y', 'b' => '2'}]
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
describe "row separators on read" do
|
|
451
|
+
it "reads CRLF, given as row_separator: or row_sep:, and by default" do
|
|
452
|
+
_(DSV.read("a,b\r\n1,2\r\n", headers: true, row_separator: "\r\n")).must_equal [{'a' => '1', 'b' => '2'}]
|
|
453
|
+
_(DSV.read("a,b\r\n1,2\r\n", headers: true, row_sep: "\r\n")).must_equal [{'a' => '1', 'b' => '2'}]
|
|
454
|
+
_(DSV.read("a,b\r\n1,2\r\n", headers: true)).must_equal [{'a' => '1', 'b' => '2'}]
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
it "reads any other row separator" do
|
|
458
|
+
_(DSV.read('a,b|1,2|', headers: true, row_separator: '|')).must_equal [{'a' => '1', 'b' => '2'}]
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
it "reads a blank line as an empty row" do
|
|
462
|
+
_(DSV.read("a,b\n\n1,2\n", headers: true)).must_equal [{}, {'a' => '1', 'b' => '2'}]
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
describe "quoting on read" do
|
|
467
|
+
it "strips quotes from quoted fields and headers by default" do
|
|
468
|
+
_(DSV.read("a,b\n\"x\",\"2\"\n", headers: true)).must_equal [{'a' => 'x', 'b' => '2'}]
|
|
469
|
+
_(DSV.read("\"a\",\"b\"\n1,2\n", headers: true)).must_equal [{'a' => '1', 'b' => '2'}]
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
it "keeps a separator inside a quoted field by default" do
|
|
473
|
+
_(DSV.read("a,b\n\"x, y\",2\n", headers: true)).must_equal [{'a' => 'x, y', 'b' => '2'}]
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
it "keeps the quotes under quote: :none" do
|
|
477
|
+
_(DSV.read("a,b\n\"x\",\"2\"\n", headers: true, quote: :none)).must_equal [{'a' => '"x"', 'b' => '"2"'}]
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
it "strips the quotes under quote: :double, a row being wholly quoted or, as a header row often is, wholly unquoted" do
|
|
481
|
+
_(DSV.read("a,b\n\"x\",\"2\"\n", headers: true, quote: :double)).must_equal [{'a' => 'x', 'b' => '2'}]
|
|
482
|
+
_(DSV.read("\"a\",\"b\"\n\"say \"\"hi\"\"\",\"2\"\n", headers: true, quote: :double)).must_equal [{'a' => 'say "hi"', 'b' => '2'}]
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
it "keeps every position of a repeated header name in columns, the name at each position in attributes" do
|
|
486
|
+
_(DSV.columns("a,a,b\n1,2,3\n", headers: true)).must_equal({'a' => [0, 1], 'b' => 2})
|
|
487
|
+
_(DSV.attributes("a,a,b\n1,2,3\n", headers: true)).must_equal ['a', 'a', 'b']
|
|
488
|
+
_(DSV.columns(",,b\n1,2,3\n", headers: true)).must_equal({'' => [0, 1], 'b' => 2})
|
|
489
|
+
_(DSV.attributes(",,b\n1,2,3\n", headers: true)).must_equal ['', '', 'b']
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
it "reads the values under a repeated header name as an Array, one per position, in order" do
|
|
493
|
+
_(DSV.read("a,a,b\n1,2,3\n4,5,6\n", headers: true)).must_equal [{'a' => ['1', '2'], 'b' => '3'}, {'a' => ['4', '5'], 'b' => '6'}]
|
|
494
|
+
_(DSV.read(",,b\n1,2,3\n", headers: true)).must_equal [{'' => ['1', '2'], 'b' => '3'}]
|
|
495
|
+
_(DSV.new("a,a,b\n1,2,3\n", headers: true).read('a')).must_equal [{'a' => ['1', '2']}]
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
it "spreads a repeated name's Array back over its positions in to_a, as_array and write" do
|
|
499
|
+
_(DSV.new("a,a,b\n1,2,3\n", headers: true).to_a).must_equal [['1', '2', '3']]
|
|
500
|
+
_(DSV.read("a,a,b\n1,2,3\n", headers: true, as_array: true)).must_equal [['1', '2', '3']]
|
|
501
|
+
output = written(headers: true, columns: ['a', 'a', 'b'], quote: :none){|csv| csv.rows = [{'a' => ['1', '2'], 'b' => '3'}]; csv.write}
|
|
502
|
+
_(output).must_equal "a,a,b\n1,2,3\n"
|
|
503
|
+
_(written(columns: ['a', 'a', 'b'], quote: :none){|csv| csv.write_row('a' => 'x', 'b' => '3')}).must_equal "x,x,3\n"
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
it "reads a doubled quote as one quote in a row" do
|
|
507
|
+
_(DSV.read("a,b\n\"say \"\"hi\"\"\",2\n", headers: true)).must_equal [{'a' => 'say "hi"', 'b' => '2'}]
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
it "reads a trailing empty field as an empty string under its column" do
|
|
511
|
+
_(DSV.read("a,b,c\n1,2,\n", headers: true)).must_equal [{'a' => '1', 'b' => '2', 'c' => ''}]
|
|
512
|
+
_(DSV.read("1,2,\n")).must_equal [{0 => '1', 1 => '2', 2 => ''}]
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
it "keeps every separator inside a quoted field in a row" do
|
|
516
|
+
_(DSV.read("\"x, y, z\",2\n")).must_equal [{0 => 'x, y, z', 1 => '2'}]
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
it "keeps a separator inside a quoted field under quote: :double" do
|
|
520
|
+
_(DSV.read("a,b\n\"x, y\",\"2\"\n", headers: true, quote: :double)).must_equal [{'a' => 'x, y', 'b' => '2'}]
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
describe "#write" do
|
|
525
|
+
it "writes each row in column order, double-quoted by default" do
|
|
526
|
+
output = written(columns: [:a, :b]){|csv| csv.rows = [{'a' => 1, 'b' => 2}]; csv.write}
|
|
527
|
+
_(output).must_equal "\"1\",\"2\"\n"
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
it "writes the header row first when headers: is true, and is aliased write_csv" do
|
|
531
|
+
output = written(headers: true, columns: [:a, :b]){|csv| csv.rows = [{'a' => 1, 'b' => 2}]; csv.write_csv}
|
|
532
|
+
_(output).must_equal "\"a\",\"b\"\n\"1\",\"2\"\n"
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
it "writes unquoted under quote: :none, :unquoted or the string none, and single-quoted under :single" do
|
|
536
|
+
_(written(headers: true, columns: [:a, :b], quote: :none){|csv| csv.rows = [{'a' => 1, 'b' => 2}]; csv.write}).must_equal "a,b\n1,2\n"
|
|
537
|
+
_(written(columns: [:a, :b], quote: :unquoted){|csv| csv.rows = [{'a' => 1, 'b' => 2}]; csv.write}).must_equal "1,2\n"
|
|
538
|
+
_(written(columns: [:a, :b], quote: 'none'){|csv| csv.rows = [{'a' => 1, 'b' => 2}]; csv.write}).must_equal "1,2\n"
|
|
539
|
+
_(written(columns: [:a, :b], quote: :single){|csv| csv.rows = [{'a' => 1, 'b' => 2}]; csv.write}).must_equal "'1','2'\n"
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
it "writes only the selected columns" do
|
|
543
|
+
output = written(columns: [:a, :b, :c]){|csv| csv.rows = [{'a' => 1, 'b' => 2, 'c' => 3}]; csv.write('a', 'c')}
|
|
544
|
+
_(output).must_equal "\"1\",\"3\"\n"
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
it "writes what it read back, so a source round-trips" do
|
|
548
|
+
output = written(headers: true, columns: [:a, :b]){|csv| csv.rows = DSV.read("a,b\n1,2\n", headers: true); csv.write}
|
|
549
|
+
_(DSV.read(output, headers: true)).must_equal [{'a' => '1', 'b' => '2'}]
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
it "write_row and write_header write one row and the header row" do
|
|
553
|
+
_(written(columns: [:a, :b]){|csv| csv.write_row('a' => 'x', 'b' => 'y')}).must_equal "\"x\",\"y\"\n"
|
|
554
|
+
_(written(columns: [:a, :b]){|csv| csv.write_header}).must_equal "\"a\",\"b\"\n"
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
it "quotes a value holding a separator, and leaves it bare under quote: :none" do
|
|
558
|
+
_(written(columns: [:a, :b]){|csv| csv.write_row('a' => 'x, y', 'b' => 'z')}).must_equal "\"x, y\",\"z\"\n"
|
|
559
|
+
_(written(columns: [:a, :b], quote: :none){|csv| csv.write_row('a' => 'x, y', 'b' => 'z')}).must_equal "x, y,z\n"
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
it "doubles a quote inside a quoted value, per RFC 4180" do
|
|
563
|
+
_(written(columns: [:a, :b]){|csv| csv.write_row('a' => 'say "hi"', 'b' => 'z')}).must_equal "\"say \"\"hi\"\"\",\"z\"\n"
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
it "writes the selected column names as the header row" do
|
|
567
|
+
output = written(headers: true, columns: [:a, :b, :c], quote: :none){|csv| csv.rows = [{'a' => 1, 'b' => 2, 'c' => 3}]; csv.write('a', 'c')}
|
|
568
|
+
_(output).must_equal "a,c\n1,3\n"
|
|
569
|
+
output = written(headers: true, columns: [:a, :b, :c]){|csv| csv.rows = [{'a' => 1, 'b' => 2, 'c' => 3}]; csv.write('a', 'c')}
|
|
570
|
+
_(output).must_equal "\"a\",\"c\"\n\"1\",\"3\"\n"
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
it "writes a nil value as an empty field, keeping the columns in place" do
|
|
574
|
+
output = written(columns: [:a, :b, :c]){|csv| csv.rows = [{'a' => 1, 'b' => nil, 'c' => 3}]; csv.write}
|
|
575
|
+
_(output).must_equal "\"1\",\"\",\"3\"\n"
|
|
576
|
+
_(written(columns: [:a, :b, :c], quote: :none){|csv| csv.rows = [{'a' => 1, 'b' => nil, 'c' => 3}]; csv.write}).must_equal "1,,3\n"
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
it "writes rows keyed by position in their own order when no columns are defined, with no header" do
|
|
580
|
+
_(written(quote: :none){|csv| csv.rows = [{0 => 1, 1 => 2}]; csv.write}).must_equal "1,2\n"
|
|
581
|
+
_(written(headers: true, quote: :none){|csv| csv.rows = DSV.read("1,2\n3,4\n"); csv.write}).must_equal "1,2\n3,4\n"
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
it "takes the columns, and the header, from the first row's keys when rows are keyed by name and no columns are defined" do
|
|
585
|
+
_(written(headers: true, quote: :none){|csv| csv.rows = [{'a' => 1, 'b' => 2}, {'a' => 3, 'b' => 4}]; csv.write}).must_equal "a,b\n1,2\n3,4\n"
|
|
586
|
+
_(written(quote: :none){|csv| csv.rows = [{'a' => 1, 'b' => 2}]; csv.write}).must_equal "1,2\n"
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
it "writes with the column and row separators given, as it reads with them, and round-trips them" do
|
|
590
|
+
_(written(columns: [:a, :b], column_separator: "\t", quote: :none){|csv| csv.rows = [{'a' => 1, 'b' => 2}]; csv.write}).must_equal "1\t2\n"
|
|
591
|
+
_(written(columns: [:a, :b], row_separator: "\r\n", quote: :none){|csv| csv.rows = [{'a' => 1, 'b' => 2}]; csv.write}).must_equal "1,2\r\n"
|
|
592
|
+
output = written(headers: true, columns: [:a, :b], column_separator: '|', row_separator: "\r\n"){|csv| csv.rows = [{'a' => 'x|y', 'b' => '2'}]; csv.write}
|
|
593
|
+
_(DSV.read(output, headers: true, column_separator: '|', row_separator: "\r\n")).must_equal [{'a' => 'x|y', 'b' => '2'}]
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
it "writes the spacey modes with a space after each separator" do
|
|
597
|
+
_(written(columns: [:a, :b], quote: :spacey_none){|csv| csv.write_row('a' => 1, 'b' => 2)}).must_equal "1, 2\n"
|
|
598
|
+
_(written(columns: [:a, :b], quote: :spacey_double){|csv| csv.write_row('a' => 1, 'b' => 2)}).must_equal "\"1\", \"2\"\n"
|
|
599
|
+
end
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
describe "DSV::String" do
|
|
603
|
+
it "reads a string, and .open behaves as DSV.open" do
|
|
604
|
+
_(DSV::String.new(DATA, headers: true).read).must_equal KEYED
|
|
605
|
+
_(DSV::String.open(DATA, headers: true){|csv| csv.read}).must_be_instance_of DSV::String
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
it ".open constructs one instance" do
|
|
609
|
+
_(constructions(DSV::String){DSV::String.open(DATA){|csv| }}).must_equal 1
|
|
610
|
+
end
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
describe "DSV::File" do
|
|
614
|
+
it "reads a file by path" do
|
|
615
|
+
with_file{|path| _(DSV.read(path, headers: true)).must_equal KEYED}
|
|
616
|
+
with_file{|path| _(DSV::File.new(path, headers: true).read).must_equal KEYED}
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
it "defaults the mode to r, reports it as Ruby's File spells it, and takes permissions:" do
|
|
620
|
+
with_file do |path|
|
|
621
|
+
_(DSV::File.new(path).mode).must_equal 'r'
|
|
622
|
+
_(DSV::File.new(path, mode: :read_only).mode).must_equal 'r'
|
|
623
|
+
_(DSV::File.new(path, mode: 'rw').mode).must_equal 'r+'
|
|
624
|
+
_(DSV.new(path, mode: 'append').mode).must_equal 'a'
|
|
625
|
+
_(DSV::File.new(path, permissions: 0644).permissions).must_equal 0644
|
|
626
|
+
end
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
it "reads the header row under a+ and has no columns under w or w+" do
|
|
630
|
+
with_file{|path| _(DSV.new(path, mode: 'a+', headers: true).columns).must_equal({'a' => 0, 'b' => 1, 'c' => 2})}
|
|
631
|
+
with_file{|path| _(DSV.new(path, mode: 'w', headers: true).columns).must_be_nil}
|
|
632
|
+
with_file{|path| _(DSV.new(path, mode: 'w+', headers: true).columns).must_be_nil}
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
it "writes an existing file under w, and appends under a" do
|
|
636
|
+
with_file do |path|
|
|
637
|
+
DSV.open(path, mode: 'w', headers: true, columns: [:a, :b], quote: :none){|csv| csv.rows = [{'a' => 1, 'b' => 2}]; csv.write}
|
|
638
|
+
_(File.read(path)).must_equal "a,b\n1,2\n"
|
|
639
|
+
end
|
|
640
|
+
with_file do |path|
|
|
641
|
+
DSV.open(path, mode: 'a', columns: [:a, :b, :c], quote: :none){|csv| csv.rows = [{'a' => 7, 'b' => 8, 'c' => 9}]; csv.write}
|
|
642
|
+
_(File.read(path)).must_equal "a,b,c\n1,2,3\n4,5,6\n7,8,9\n"
|
|
643
|
+
end
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
it "rewrites a file in place under r+ when the rows are read, changed and written" do
|
|
647
|
+
with_file do |path|
|
|
648
|
+
DSV.open(path, mode: 'r+', headers: true, quote: :none){|csv| csv.rows = csv.read.map{|row| row.merge('a' => 'X')}; csv.write}
|
|
649
|
+
_(File.read(path)).must_equal "a,b,c\nX,2,3\nX,5,6\n"
|
|
650
|
+
end
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
it "DSV.open yields the instance and closes the file" do
|
|
654
|
+
with_file do |path|
|
|
655
|
+
given = nil
|
|
656
|
+
DSV.open(path, headers: true){|csv| given = csv}
|
|
657
|
+
_(given.instance_variable_get(:@source).closed?).must_equal true
|
|
658
|
+
end
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
it "writes a new file by name under a write mode" do
|
|
662
|
+
skip "Decision point 4: Finding 14, a path that does not exist yet is taken as text; the note leans to a write mode making it a path"
|
|
663
|
+
with_file do |path, directory|
|
|
664
|
+
new_path = File.join(directory, 'new.csv')
|
|
665
|
+
DSV.open(new_path, mode: 'w', headers: true, columns: [:a, :b], quote: :none){|csv| csv.rows = [{'a' => 1, 'b' => 2}]; csv.write}
|
|
666
|
+
_(File.read(new_path)).must_equal "a,b\n1,2\n"
|
|
667
|
+
end
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
it "leaves a file as it was when read under r+ without a write, whichever way it is read" do
|
|
671
|
+
with_file do |path|
|
|
672
|
+
DSV.open(path, mode: 'r+', headers: true){|csv| csv.read}
|
|
673
|
+
_(File.read(path)).must_equal DATA
|
|
674
|
+
end
|
|
675
|
+
with_file do |path|
|
|
676
|
+
_(DSV.new(path, mode: 'r+', headers: true).count).must_equal 2
|
|
677
|
+
_(File.read(path)).must_equal DATA
|
|
678
|
+
end
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
it "replaces the file under r+ on write, reading the rows first if they have not been read" do
|
|
682
|
+
with_file do |path|
|
|
683
|
+
DSV.open(path, mode: 'r+', headers: true, quote: :none){|csv| csv.write}
|
|
684
|
+
_(File.read(path)).must_equal DATA
|
|
685
|
+
end
|
|
686
|
+
with_file do |path|
|
|
687
|
+
DSV.open(path, mode: 'r+', headers: true, quote: :none){|csv| csv.rows = [{'a' => 'X', 'b' => 'Y', 'c' => 'Z'}]; csv.write}
|
|
688
|
+
_(File.read(path)).must_equal "a,b,c\nX,Y,Z\n"
|
|
689
|
+
end
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
it ".open constructs one instance, opening the file once" do
|
|
693
|
+
with_file{|path| _(constructions(DSV::File){DSV::File.open(path){|csv| }}).must_equal 1}
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
it "expands the path it is given" do
|
|
697
|
+
with_file do |path, directory|
|
|
698
|
+
_(DSV::File.new(File.join(directory, '.', 'data.csv')).filename).must_equal path
|
|
699
|
+
end
|
|
700
|
+
end
|
|
701
|
+
end
|
|
702
|
+
end
|