csv 0.1.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/LICENSE.txt +33 -0
- data/README.md +52 -0
- data/lib/csv/core_ext/array.rb +9 -0
- data/lib/csv/core_ext/string.rb +9 -0
- data/lib/csv/row.rb +388 -0
- data/lib/csv/table.rb +378 -0
- data/lib/csv/version.rb +6 -0
- data/lib/csv.rb +227 -804
- data/news.md +123 -0
- metadata +40 -14
data/lib/csv/table.rb
ADDED
@@ -0,0 +1,378 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
|
5
|
+
class CSV
|
6
|
+
#
|
7
|
+
# A CSV::Table is a two-dimensional data structure for representing CSV
|
8
|
+
# documents. Tables allow you to work with the data by row or column,
|
9
|
+
# manipulate the data, and even convert the results back to CSV, if needed.
|
10
|
+
#
|
11
|
+
# All tables returned by CSV will be constructed from this class, if header
|
12
|
+
# row processing is activated.
|
13
|
+
#
|
14
|
+
class Table
|
15
|
+
#
|
16
|
+
# Construct a new CSV::Table from +array_of_rows+, which are expected
|
17
|
+
# to be CSV::Row objects. All rows are assumed to have the same headers.
|
18
|
+
#
|
19
|
+
# A CSV::Table object supports the following Array methods through
|
20
|
+
# delegation:
|
21
|
+
#
|
22
|
+
# * empty?()
|
23
|
+
# * length()
|
24
|
+
# * size()
|
25
|
+
#
|
26
|
+
def initialize(array_of_rows)
|
27
|
+
@table = array_of_rows
|
28
|
+
@mode = :col_or_row
|
29
|
+
end
|
30
|
+
|
31
|
+
# The current access mode for indexing and iteration.
|
32
|
+
attr_reader :mode
|
33
|
+
|
34
|
+
# Internal data format used to compare equality.
|
35
|
+
attr_reader :table
|
36
|
+
protected :table
|
37
|
+
|
38
|
+
### Array Delegation ###
|
39
|
+
|
40
|
+
extend Forwardable
|
41
|
+
def_delegators :@table, :empty?, :length, :size
|
42
|
+
|
43
|
+
#
|
44
|
+
# Returns a duplicate table object, in column mode. This is handy for
|
45
|
+
# chaining in a single call without changing the table mode, but be aware
|
46
|
+
# that this method can consume a fair amount of memory for bigger data sets.
|
47
|
+
#
|
48
|
+
# This method returns the duplicate table for chaining. Don't chain
|
49
|
+
# destructive methods (like []=()) this way though, since you are working
|
50
|
+
# with a duplicate.
|
51
|
+
#
|
52
|
+
def by_col
|
53
|
+
self.class.new(@table.dup).by_col!
|
54
|
+
end
|
55
|
+
|
56
|
+
#
|
57
|
+
# Switches the mode of this table to column mode. All calls to indexing and
|
58
|
+
# iteration methods will work with columns until the mode is changed again.
|
59
|
+
#
|
60
|
+
# This method returns the table and is safe to chain.
|
61
|
+
#
|
62
|
+
def by_col!
|
63
|
+
@mode = :col
|
64
|
+
|
65
|
+
self
|
66
|
+
end
|
67
|
+
|
68
|
+
#
|
69
|
+
# Returns a duplicate table object, in mixed mode. This is handy for
|
70
|
+
# chaining in a single call without changing the table mode, but be aware
|
71
|
+
# that this method can consume a fair amount of memory for bigger data sets.
|
72
|
+
#
|
73
|
+
# This method returns the duplicate table for chaining. Don't chain
|
74
|
+
# destructive methods (like []=()) this way though, since you are working
|
75
|
+
# with a duplicate.
|
76
|
+
#
|
77
|
+
def by_col_or_row
|
78
|
+
self.class.new(@table.dup).by_col_or_row!
|
79
|
+
end
|
80
|
+
|
81
|
+
#
|
82
|
+
# Switches the mode of this table to mixed mode. All calls to indexing and
|
83
|
+
# iteration methods will use the default intelligent indexing system until
|
84
|
+
# the mode is changed again. In mixed mode an index is assumed to be a row
|
85
|
+
# reference while anything else is assumed to be column access by headers.
|
86
|
+
#
|
87
|
+
# This method returns the table and is safe to chain.
|
88
|
+
#
|
89
|
+
def by_col_or_row!
|
90
|
+
@mode = :col_or_row
|
91
|
+
|
92
|
+
self
|
93
|
+
end
|
94
|
+
|
95
|
+
#
|
96
|
+
# Returns a duplicate table object, in row mode. This is handy for chaining
|
97
|
+
# in a single call without changing the table mode, but be aware that this
|
98
|
+
# method can consume a fair amount of memory for bigger data sets.
|
99
|
+
#
|
100
|
+
# This method returns the duplicate table for chaining. Don't chain
|
101
|
+
# destructive methods (like []=()) this way though, since you are working
|
102
|
+
# with a duplicate.
|
103
|
+
#
|
104
|
+
def by_row
|
105
|
+
self.class.new(@table.dup).by_row!
|
106
|
+
end
|
107
|
+
|
108
|
+
#
|
109
|
+
# Switches the mode of this table to row mode. All calls to indexing and
|
110
|
+
# iteration methods will work with rows until the mode is changed again.
|
111
|
+
#
|
112
|
+
# This method returns the table and is safe to chain.
|
113
|
+
#
|
114
|
+
def by_row!
|
115
|
+
@mode = :row
|
116
|
+
|
117
|
+
self
|
118
|
+
end
|
119
|
+
|
120
|
+
#
|
121
|
+
# Returns the headers for the first row of this table (assumed to match all
|
122
|
+
# other rows). An empty Array is returned for empty tables.
|
123
|
+
#
|
124
|
+
def headers
|
125
|
+
if @table.empty?
|
126
|
+
Array.new
|
127
|
+
else
|
128
|
+
@table.first.headers
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
#
|
133
|
+
# In the default mixed mode, this method returns rows for index access and
|
134
|
+
# columns for header access. You can force the index association by first
|
135
|
+
# calling by_col!() or by_row!().
|
136
|
+
#
|
137
|
+
# Columns are returned as an Array of values. Altering that Array has no
|
138
|
+
# effect on the table.
|
139
|
+
#
|
140
|
+
def [](index_or_header)
|
141
|
+
if @mode == :row or # by index
|
142
|
+
(@mode == :col_or_row and (index_or_header.is_a?(Integer) or index_or_header.is_a?(Range)))
|
143
|
+
@table[index_or_header]
|
144
|
+
else # by header
|
145
|
+
@table.map { |row| row[index_or_header] }
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
#
|
150
|
+
# In the default mixed mode, this method assigns rows for index access and
|
151
|
+
# columns for header access. You can force the index association by first
|
152
|
+
# calling by_col!() or by_row!().
|
153
|
+
#
|
154
|
+
# Rows may be set to an Array of values (which will inherit the table's
|
155
|
+
# headers()) or a CSV::Row.
|
156
|
+
#
|
157
|
+
# Columns may be set to a single value, which is copied to each row of the
|
158
|
+
# column, or an Array of values. Arrays of values are assigned to rows top
|
159
|
+
# to bottom in row major order. Excess values are ignored and if the Array
|
160
|
+
# does not have a value for each row the extra rows will receive a +nil+.
|
161
|
+
#
|
162
|
+
# Assigning to an existing column or row clobbers the data. Assigning to
|
163
|
+
# new columns creates them at the right end of the table.
|
164
|
+
#
|
165
|
+
def []=(index_or_header, value)
|
166
|
+
if @mode == :row or # by index
|
167
|
+
(@mode == :col_or_row and index_or_header.is_a? Integer)
|
168
|
+
if value.is_a? Array
|
169
|
+
@table[index_or_header] = Row.new(headers, value)
|
170
|
+
else
|
171
|
+
@table[index_or_header] = value
|
172
|
+
end
|
173
|
+
else # set column
|
174
|
+
if value.is_a? Array # multiple values
|
175
|
+
@table.each_with_index do |row, i|
|
176
|
+
if row.header_row?
|
177
|
+
row[index_or_header] = index_or_header
|
178
|
+
else
|
179
|
+
row[index_or_header] = value[i]
|
180
|
+
end
|
181
|
+
end
|
182
|
+
else # repeated value
|
183
|
+
@table.each do |row|
|
184
|
+
if row.header_row?
|
185
|
+
row[index_or_header] = index_or_header
|
186
|
+
else
|
187
|
+
row[index_or_header] = value
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
#
|
195
|
+
# The mixed mode default is to treat a list of indices as row access,
|
196
|
+
# returning the rows indicated. Anything else is considered columnar
|
197
|
+
# access. For columnar access, the return set has an Array for each row
|
198
|
+
# with the values indicated by the headers in each Array. You can force
|
199
|
+
# column or row mode using by_col!() or by_row!().
|
200
|
+
#
|
201
|
+
# You cannot mix column and row access.
|
202
|
+
#
|
203
|
+
def values_at(*indices_or_headers)
|
204
|
+
if @mode == :row or # by indices
|
205
|
+
( @mode == :col_or_row and indices_or_headers.all? do |index|
|
206
|
+
index.is_a?(Integer) or
|
207
|
+
( index.is_a?(Range) and
|
208
|
+
index.first.is_a?(Integer) and
|
209
|
+
index.last.is_a?(Integer) )
|
210
|
+
end )
|
211
|
+
@table.values_at(*indices_or_headers)
|
212
|
+
else # by headers
|
213
|
+
@table.map { |row| row.values_at(*indices_or_headers) }
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
#
|
218
|
+
# Adds a new row to the bottom end of this table. You can provide an Array,
|
219
|
+
# which will be converted to a CSV::Row (inheriting the table's headers()),
|
220
|
+
# or a CSV::Row.
|
221
|
+
#
|
222
|
+
# This method returns the table for chaining.
|
223
|
+
#
|
224
|
+
def <<(row_or_array)
|
225
|
+
if row_or_array.is_a? Array # append Array
|
226
|
+
@table << Row.new(headers, row_or_array)
|
227
|
+
else # append Row
|
228
|
+
@table << row_or_array
|
229
|
+
end
|
230
|
+
|
231
|
+
self # for chaining
|
232
|
+
end
|
233
|
+
|
234
|
+
#
|
235
|
+
# A shortcut for appending multiple rows. Equivalent to:
|
236
|
+
#
|
237
|
+
# rows.each { |row| self << row }
|
238
|
+
#
|
239
|
+
# This method returns the table for chaining.
|
240
|
+
#
|
241
|
+
def push(*rows)
|
242
|
+
rows.each { |row| self << row }
|
243
|
+
|
244
|
+
self # for chaining
|
245
|
+
end
|
246
|
+
|
247
|
+
#
|
248
|
+
# Removes and returns the indicated columns or rows. In the default mixed
|
249
|
+
# mode indices refer to rows and everything else is assumed to be a column
|
250
|
+
# headers. Use by_col!() or by_row!() to force the lookup.
|
251
|
+
#
|
252
|
+
def delete(*indexes_or_headers)
|
253
|
+
if indexes_or_headers.empty?
|
254
|
+
raise ArgumentError, "wrong number of arguments (given 0, expected 1+)"
|
255
|
+
end
|
256
|
+
deleted_values = indexes_or_headers.map do |index_or_header|
|
257
|
+
if @mode == :row or # by index
|
258
|
+
(@mode == :col_or_row and index_or_header.is_a? Integer)
|
259
|
+
@table.delete_at(index_or_header)
|
260
|
+
else # by header
|
261
|
+
@table.map { |row| row.delete(index_or_header).last }
|
262
|
+
end
|
263
|
+
end
|
264
|
+
if indexes_or_headers.size == 1
|
265
|
+
deleted_values[0]
|
266
|
+
else
|
267
|
+
deleted_values
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
#
|
272
|
+
# Removes any column or row for which the block returns +true+. In the
|
273
|
+
# default mixed mode or row mode, iteration is the standard row major
|
274
|
+
# walking of rows. In column mode, iteration will +yield+ two element
|
275
|
+
# tuples containing the column name and an Array of values for that column.
|
276
|
+
#
|
277
|
+
# This method returns the table for chaining.
|
278
|
+
#
|
279
|
+
# If no block is given, an Enumerator is returned.
|
280
|
+
#
|
281
|
+
def delete_if(&block)
|
282
|
+
return enum_for(__method__) { @mode == :row or @mode == :col_or_row ? size : headers.size } unless block_given?
|
283
|
+
|
284
|
+
if @mode == :row or @mode == :col_or_row # by index
|
285
|
+
@table.delete_if(&block)
|
286
|
+
else # by header
|
287
|
+
deleted = []
|
288
|
+
headers.each do |header|
|
289
|
+
deleted << delete(header) if yield([header, self[header]])
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
self # for chaining
|
294
|
+
end
|
295
|
+
|
296
|
+
include Enumerable
|
297
|
+
|
298
|
+
#
|
299
|
+
# In the default mixed mode or row mode, iteration is the standard row major
|
300
|
+
# walking of rows. In column mode, iteration will +yield+ two element
|
301
|
+
# tuples containing the column name and an Array of values for that column.
|
302
|
+
#
|
303
|
+
# This method returns the table for chaining.
|
304
|
+
#
|
305
|
+
# If no block is given, an Enumerator is returned.
|
306
|
+
#
|
307
|
+
def each(&block)
|
308
|
+
return enum_for(__method__) { @mode == :col ? headers.size : size } unless block_given?
|
309
|
+
|
310
|
+
if @mode == :col
|
311
|
+
headers.each { |header| yield([header, self[header]]) }
|
312
|
+
else
|
313
|
+
@table.each(&block)
|
314
|
+
end
|
315
|
+
|
316
|
+
self # for chaining
|
317
|
+
end
|
318
|
+
|
319
|
+
# Returns +true+ if all rows of this table ==() +other+'s rows.
|
320
|
+
def ==(other)
|
321
|
+
return @table == other.table if other.is_a? CSV::Table
|
322
|
+
@table == other
|
323
|
+
end
|
324
|
+
|
325
|
+
#
|
326
|
+
# Returns the table as an Array of Arrays. Headers will be the first row,
|
327
|
+
# then all of the field rows will follow.
|
328
|
+
#
|
329
|
+
def to_a
|
330
|
+
array = [headers]
|
331
|
+
@table.each do |row|
|
332
|
+
array.push(row.fields) unless row.header_row?
|
333
|
+
end
|
334
|
+
|
335
|
+
array
|
336
|
+
end
|
337
|
+
|
338
|
+
#
|
339
|
+
# Returns the table as a complete CSV String. Headers will be listed first,
|
340
|
+
# then all of the field rows.
|
341
|
+
#
|
342
|
+
# This method assumes you want the Table.headers(), unless you explicitly
|
343
|
+
# pass <tt>:write_headers => false</tt>.
|
344
|
+
#
|
345
|
+
def to_csv(write_headers: true, **options)
|
346
|
+
array = write_headers ? [headers.to_csv(options)] : []
|
347
|
+
@table.each do |row|
|
348
|
+
array.push(row.fields.to_csv(options)) unless row.header_row?
|
349
|
+
end
|
350
|
+
|
351
|
+
array.join("")
|
352
|
+
end
|
353
|
+
alias_method :to_s, :to_csv
|
354
|
+
|
355
|
+
#
|
356
|
+
# Extracts the nested value specified by the sequence of +index+ or +header+ objects by calling dig at each step,
|
357
|
+
# returning nil if any intermediate step is nil.
|
358
|
+
#
|
359
|
+
def dig(index_or_header, *index_or_headers)
|
360
|
+
value = self[index_or_header]
|
361
|
+
if value.nil?
|
362
|
+
nil
|
363
|
+
elsif index_or_headers.empty?
|
364
|
+
value
|
365
|
+
else
|
366
|
+
unless value.respond_to?(:dig)
|
367
|
+
raise TypeError, "#{value.class} does not have \#dig method"
|
368
|
+
end
|
369
|
+
value.dig(*index_or_headers)
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
# Shows the mode and size of this table in a US-ASCII String.
|
374
|
+
def inspect
|
375
|
+
"#<#{self.class} mode:#{@mode} row_count:#{to_a.size}>".encode("US-ASCII")
|
376
|
+
end
|
377
|
+
end
|
378
|
+
end
|