csv 0.1.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CSV
4
+ # The version of the installed library.
5
+ VERSION = "3.0.0"
6
+ end