csv_madness 0.0.6 → 0.0.10
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.
Potentially problematic release.
This version of csv_madness might be problematic. Click here for more details.
- checksums.yaml +5 -13
- data/.document +5 -0
- data/CHANGELOG.markdown +4 -0
- data/Gemfile +2 -8
- data/README.rdoc +55 -47
- data/Rakefile +10 -49
- data/VERSION +1 -1
- data/lib/csv_madness/builder.rb +95 -10
- data/lib/csv_madness/column.rb +30 -0
- data/lib/csv_madness/column_methods/core_methods.rb +6 -0
- data/lib/csv_madness/column_methods/default_value_methods.rb +7 -0
- data/lib/csv_madness/column_methods/error_methods.rb +19 -0
- data/lib/csv_madness/column_methods/fetch_methods.rb +11 -0
- data/lib/csv_madness/column_methods/typing_methods.rb +24 -0
- data/lib/csv_madness/column_set.rb +5 -0
- data/lib/csv_madness/column_type.rb +74 -0
- data/lib/csv_madness/column_types/date.rb +14 -0
- data/lib/csv_madness/column_types/float.rb +25 -0
- data/lib/csv_madness/column_types/integer.rb +17 -0
- data/lib/csv_madness/column_types/string.rb +9 -0
- data/lib/csv_madness/data_accessor_module.rb +79 -44
- data/lib/csv_madness/exceptions.rb +16 -0
- data/lib/csv_madness/gem_api.rb +11 -2
- data/lib/csv_madness/index_set.rb +36 -0
- data/lib/csv_madness/indexer.rb +105 -0
- data/lib/csv_madness/record.rb +68 -40
- data/lib/csv_madness/record_filter.rb +68 -0
- data/lib/csv_madness/record_methods/method_methods.rb +57 -0
- data/lib/csv_madness/sheet.rb +172 -442
- data/lib/csv_madness/sheet_methods/base.rb +6 -0
- data/lib/csv_madness/sheet_methods/class_methods.rb +84 -0
- data/lib/csv_madness/sheet_methods/column_methods.rb +175 -0
- data/lib/csv_madness/sheet_methods/file_methods.rb +44 -0
- data/lib/csv_madness/sheet_methods/indexing.rb +133 -0
- data/lib/csv_madness/sheet_methods/record_methods.rb +175 -0
- data/lib/csv_madness/sheet_methods/sorting_methods.rb +10 -0
- data/lib/csv_madness/utils/const_proc.rb +19 -0
- data/lib/csv_madness/utils/counter.rb +24 -0
- data/lib/csv_madness/utils/counter_proc.rb +31 -0
- data/lib/csv_madness/utils/looker_upper.rb +36 -0
- data/lib/csv_madness.rb +2 -2
- data/test/csv/nil_headers.csv +5 -0
- data/test/csv/out/.gitkeep +0 -0
- data/test/csv/pokemon.csv +9 -0
- data/test/csv/test_column_types.csv +2 -2
- data/test/helper.rb +13 -5
- data/test/test_builder.rb +43 -3
- data/test/test_csv_madness.rb +34 -25
- data/test/test_index_set.rb +30 -0
- data/test/test_indexer.rb +50 -0
- data/test/test_reloading_spreadsheet.rb +5 -2
- data/test/test_sheet.rb +184 -37
- data/test/test_utils.rb +99 -0
- metadata +82 -36
data/lib/csv_madness/sheet.rb
CHANGED
|
@@ -1,144 +1,29 @@
|
|
|
1
1
|
module CsvMadness
|
|
2
2
|
class Sheet
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
rval = Integer(cell)
|
|
11
|
-
rescue
|
|
12
|
-
# do nothing
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
unless rval.is_a?(Integer)
|
|
16
|
-
begin
|
|
17
|
-
rval = Float(cell)
|
|
18
|
-
rescue
|
|
19
|
-
# do nothing
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
rval
|
|
25
|
-
end,
|
|
26
|
-
|
|
27
|
-
integer: Proc.new do |cell, record|
|
|
28
|
-
begin
|
|
29
|
-
Integer(cell)
|
|
30
|
-
rescue
|
|
31
|
-
cell
|
|
32
|
-
end
|
|
33
|
-
end,
|
|
34
|
-
|
|
35
|
-
float: Proc.new do |cell, record|
|
|
36
|
-
begin
|
|
37
|
-
Float(cell)
|
|
38
|
-
rescue
|
|
39
|
-
cell
|
|
40
|
-
end
|
|
41
|
-
end,
|
|
42
|
-
|
|
43
|
-
date: Proc.new do |cell, record|
|
|
44
|
-
begin
|
|
45
|
-
parse = Time.parse( cell || "" )
|
|
46
|
-
rescue ArgumentError
|
|
47
|
-
if cell =~ /^Invalid Time Format: /
|
|
48
|
-
parse = cell
|
|
49
|
-
else
|
|
50
|
-
parse = "Invalid Time Format: <#{cell}>"
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
parse
|
|
55
|
-
end
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
FORBIDDEN_COLUMN_NAMES = [:to_s] # breaks things hard when you use them. Probably not comprehensive, sadly.
|
|
59
|
-
|
|
60
|
-
# Used to make getter/setter names out of the original header strings.
|
|
61
|
-
# " hello;: world! " => :hello_world
|
|
62
|
-
def self.getter_name( name )
|
|
63
|
-
name = name.strip.gsub( /\s+/, "_" ).gsub( /(\W|_)+/, "_" ).downcase
|
|
64
|
-
name = name.gsub( /_+$/, "" )
|
|
65
|
-
if name.match( /^\d/ )
|
|
66
|
-
name = "_#{name}"
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
name.to_sym
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
# Paths to be searched when CsvMadness.load( "filename.csv" ) is called.
|
|
74
|
-
def self.add_search_path( path )
|
|
75
|
-
@search_paths ||= []
|
|
76
|
-
path = Pathname.new( path ).expand_path
|
|
77
|
-
unless path.directory?
|
|
78
|
-
raise "The given path does not exist"
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
@search_paths << path unless @search_paths.include?( path )
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def self.search_paths
|
|
85
|
-
@search_paths
|
|
86
|
-
end
|
|
3
|
+
extend SheetMethods::ClassMethods
|
|
4
|
+
include SheetMethods::Base
|
|
5
|
+
# include SheetMethods::Indexing # TODO: May never re-implement
|
|
6
|
+
include SheetMethods::FileMethods
|
|
7
|
+
include SheetMethods::ColumnMethods
|
|
8
|
+
include SheetMethods::RecordMethods
|
|
9
|
+
include SheetMethods::SortingMethods
|
|
87
10
|
|
|
88
|
-
def
|
|
89
|
-
|
|
90
|
-
Sheet.new( f, opts )
|
|
91
|
-
else
|
|
92
|
-
raise "File not found."
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
# Search absolute/relative-to-current-dir before checking
|
|
97
|
-
# search paths.
|
|
98
|
-
def self.find_spreadsheet_in_filesystem( name )
|
|
99
|
-
@search_paths ||= []
|
|
100
|
-
|
|
101
|
-
expanded_path = Pathname.new( name ).expand_path
|
|
102
|
-
if expanded_path.exist?
|
|
103
|
-
return expanded_path
|
|
104
|
-
else # look for it in the search paths
|
|
105
|
-
@search_paths.each do |p|
|
|
106
|
-
file = p.join( name )
|
|
107
|
-
if file.exist? && file.file?
|
|
108
|
-
return p.join( name )
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
nil
|
|
11
|
+
def indexing_enabled?
|
|
12
|
+
false
|
|
114
13
|
end
|
|
115
14
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
out = spreadsheet.columns.to_csv( opts )
|
|
119
|
-
spreadsheet.records.inject( out ) do |output, record|
|
|
120
|
-
output << record.to_csv( opts )
|
|
121
|
-
end
|
|
122
|
-
end
|
|
15
|
+
attr_reader :columns, :column_mapping, :opts, :extra_methods
|
|
16
|
+
attr_accessor :spreadsheet_file
|
|
123
17
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def to_csv( opts = {} )
|
|
136
|
-
self.records.inject( self.columns.to_csv( opts ) ) do |output, record|
|
|
137
|
-
output << record.to_csv( opts )
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
attr_reader :columns, :index_columns, :records, :spreadsheet_file, :record_class, :opts
|
|
18
|
+
# new( <optional non-hash first argument>, <optional hash of options> )
|
|
19
|
+
# firstarg:
|
|
20
|
+
# nil or no first argument given: this Spreadsheet object isn't associated with a .csv file
|
|
21
|
+
#
|
|
22
|
+
# string or path: create Spreadsheet by reading contents of a CSV file
|
|
23
|
+
#
|
|
24
|
+
# array: sets the columns. Overlaps with opts[:columns], and should likely be removed.
|
|
25
|
+
#
|
|
26
|
+
#
|
|
142
27
|
# opts:
|
|
143
28
|
# index: ( [:id, :id2 ] )
|
|
144
29
|
# columns you want mapped for quick
|
|
@@ -154,16 +39,25 @@ module CsvMadness
|
|
|
154
39
|
# header: false
|
|
155
40
|
# anything else, we assume the csv file has a header row
|
|
156
41
|
def initialize( *args )
|
|
42
|
+
@csv = nil
|
|
43
|
+
@extra_methods = {} # key = method name as symbol, val: a proc that takes a record as an argument, and possibly a block
|
|
44
|
+
if indexing_enabled?
|
|
45
|
+
@index_columns = []
|
|
46
|
+
@indexes = {}
|
|
47
|
+
end
|
|
48
|
+
|
|
157
49
|
if args.last.is_a?(Hash)
|
|
158
50
|
@opts = args.pop
|
|
159
51
|
else
|
|
160
52
|
@opts = {}
|
|
161
53
|
end
|
|
162
54
|
|
|
55
|
+
@column_types = {}
|
|
56
|
+
|
|
163
57
|
firstarg = args.shift
|
|
164
58
|
|
|
165
59
|
case firstarg
|
|
166
|
-
when NilClass
|
|
60
|
+
when NilClass # Blank sheet, no affiliation with an existing csv file
|
|
167
61
|
@spreadsheet_file = nil
|
|
168
62
|
@opts[:columns] ||= []
|
|
169
63
|
when String, FunWith::Files::FilePath, Pathname
|
|
@@ -178,146 +72,102 @@ module CsvMadness
|
|
|
178
72
|
reload_spreadsheet
|
|
179
73
|
end
|
|
180
74
|
|
|
181
|
-
def <<( record )
|
|
182
|
-
self.add_record( record )
|
|
183
|
-
end
|
|
184
75
|
|
|
185
|
-
def
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
record = CSV::Row.new( self.columns, record )
|
|
190
|
-
when Hash
|
|
191
|
-
header = []
|
|
192
|
-
fields = []
|
|
193
|
-
|
|
194
|
-
for col in self.columns
|
|
195
|
-
header << col
|
|
196
|
-
fields << record[col]
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
record = CSV::Row.new( header, fields )
|
|
200
|
-
when CSV::Row
|
|
201
|
-
# do nothing
|
|
76
|
+
def call_record_method( record, method, args )
|
|
77
|
+
if extra_method?( method )
|
|
78
|
+
# call the decorated method
|
|
79
|
+
call_extra_method( method, record, *args )
|
|
202
80
|
else
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
@records << record
|
|
209
|
-
add_to_indexes( record )
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
# record can be the row number (integer from 0...@records.length)
|
|
213
|
-
# record can be the record itself (anonymous class)
|
|
214
|
-
def remove_record( record )
|
|
215
|
-
record = @records[record] if record.is_a?(Integer)
|
|
216
|
-
return if record.nil?
|
|
217
|
-
|
|
218
|
-
self.remove_from_index( record )
|
|
219
|
-
@records.delete( record )
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
# Here's the deal: you hand us a block, and we'll remove the records for which
|
|
223
|
-
# it yields _true_.
|
|
224
|
-
def remove_records( records = nil, &block )
|
|
225
|
-
if block_given?
|
|
226
|
-
for record in @records
|
|
227
|
-
remove_record( record ) if yield( record ) == true
|
|
228
|
-
end
|
|
229
|
-
else # records should be an array
|
|
230
|
-
for record in records
|
|
231
|
-
self.remove_record( record )
|
|
81
|
+
if method[-1] == '='
|
|
82
|
+
key = method[0..-2]
|
|
83
|
+
set_cell( record, key, args.first )
|
|
84
|
+
else
|
|
85
|
+
get_cell( record, method )
|
|
232
86
|
end
|
|
233
87
|
end
|
|
234
88
|
end
|
|
235
|
-
|
|
236
|
-
def
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
89
|
+
|
|
90
|
+
def get_cell( record, key )
|
|
91
|
+
record = self[record] if record.is_a?( Integer )
|
|
92
|
+
|
|
93
|
+
index = if key.is_a?( Integer )
|
|
94
|
+
key
|
|
95
|
+
elsif has_column?( key )
|
|
96
|
+
index_of_column( key )
|
|
97
|
+
else
|
|
98
|
+
nil
|
|
99
|
+
end
|
|
241
100
|
|
|
242
|
-
|
|
243
|
-
|
|
101
|
+
raise ColumnError.new( "Column #{key} does not exist. Column mapping: #{self.column_mapping.inspect}" ) if index.nil?
|
|
102
|
+
|
|
103
|
+
record.data[ index ]
|
|
244
104
|
end
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# TODO: Also update the index if necessary
|
|
108
|
+
def set_cell( record, key, val )
|
|
109
|
+
record = self[record] if record.is_a?( Integer )
|
|
245
110
|
|
|
246
|
-
|
|
247
|
-
@index_columns = case index_columns
|
|
248
|
-
when NilClass
|
|
249
|
-
[]
|
|
250
|
-
when Symbol
|
|
251
|
-
[ index_columns ]
|
|
252
|
-
when Array
|
|
253
|
-
index_columns
|
|
254
|
-
end
|
|
111
|
+
record.data[ index_of_column( key ) ] = val
|
|
255
112
|
end
|
|
256
113
|
|
|
257
|
-
def
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
# Fetches an indexed record based on the column indexed and the keying object.
|
|
262
|
-
# If key is an array of keying objects, returns an array of records in the
|
|
263
|
-
# same order that the keying objects appear.
|
|
264
|
-
# Index column should yield a different, unique value for each record.
|
|
265
|
-
def fetch( index_col, key )
|
|
266
|
-
if key.is_a?(Array)
|
|
267
|
-
key.map{ |key| @indexes[index_col][key] }
|
|
268
|
-
else
|
|
269
|
-
@indexes[index_col][key]
|
|
114
|
+
def to_csv( hash_opts = {}, **keyword_opts )
|
|
115
|
+
opts = hash_opts.merge( keyword_opts )
|
|
116
|
+
self.records.inject( self.columns.to_csv( ** opts ) ) do |output, record|
|
|
117
|
+
output << record.to_csv( ** opts )
|
|
270
118
|
end
|
|
271
119
|
end
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
# meat grinder
|
|
276
|
-
def filter( &block )
|
|
277
|
-
rval = []
|
|
278
|
-
@records.each do |record|
|
|
279
|
-
rval << record if ( yield record )
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
rval
|
|
120
|
+
|
|
121
|
+
def [] offset
|
|
122
|
+
@records[offset]
|
|
283
123
|
end
|
|
284
124
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
@records = self.filter( &block )
|
|
288
|
-
reindex
|
|
289
|
-
@records
|
|
125
|
+
def records
|
|
126
|
+
@records ||= []
|
|
290
127
|
end
|
|
291
|
-
|
|
128
|
+
|
|
292
129
|
|
|
293
130
|
# give a copy of the current spreadsheet, but with no records
|
|
294
131
|
def blanked()
|
|
295
132
|
sheet = self.class.new
|
|
296
133
|
sheet.columns = @columns.clone
|
|
297
|
-
sheet.
|
|
134
|
+
sheet.set_column_mapping
|
|
135
|
+
|
|
136
|
+
sheet.index_columns = @index_columns.clone if indexing_enabled?
|
|
137
|
+
|
|
298
138
|
sheet.records = []
|
|
299
139
|
sheet.spreadsheet_file = nil
|
|
300
|
-
sheet.create_data_accessor_module
|
|
301
|
-
sheet.create_record_class
|
|
140
|
+
# sheet.create_data_accessor_module
|
|
141
|
+
# sheet.create_record_class
|
|
302
142
|
sheet.opts = @opts.clone
|
|
303
|
-
sheet.reindex
|
|
143
|
+
sheet.reindex if indexing_enabled?
|
|
304
144
|
|
|
305
145
|
sheet
|
|
306
146
|
end
|
|
307
147
|
|
|
308
|
-
#
|
|
309
|
-
#
|
|
310
|
-
#
|
|
311
|
-
#
|
|
312
|
-
|
|
313
|
-
|
|
148
|
+
# block form:
|
|
149
|
+
# give a block, and get back a hash.
|
|
150
|
+
# The hash keys are the results of the block.
|
|
151
|
+
# The hash values are copies of the spreadsheets, with only the records
|
|
152
|
+
# which caused the block to return the key.
|
|
153
|
+
#
|
|
154
|
+
# arg form:
|
|
155
|
+
# assumes the given arg is a column name
|
|
156
|
+
def split( col = nil, &block )
|
|
157
|
+
if block_given?
|
|
158
|
+
sheets = Hash.new
|
|
314
159
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
160
|
+
for record in @records
|
|
161
|
+
result_key = yield record
|
|
162
|
+
( sheets[result_key] ||= self.blanked() ) << record
|
|
163
|
+
end
|
|
319
164
|
|
|
320
|
-
|
|
165
|
+
sheets
|
|
166
|
+
elsif has_column?( col )
|
|
167
|
+
self.split(&col)
|
|
168
|
+
else
|
|
169
|
+
raise ColumnError.new( "No block given and column doesn't exist: #{col.inspect}" )
|
|
170
|
+
end
|
|
321
171
|
# sheet_args = self.blanked
|
|
322
172
|
# for key, record_set in records
|
|
323
173
|
# sheet = self.clone
|
|
@@ -329,18 +179,6 @@ module CsvMadness
|
|
|
329
179
|
# records
|
|
330
180
|
end
|
|
331
181
|
|
|
332
|
-
def column col
|
|
333
|
-
@records.map(&col)
|
|
334
|
-
end
|
|
335
|
-
|
|
336
|
-
# retrieve multiple columns. Returns an array of the form
|
|
337
|
-
# [ [record1:col1, record1:col2...], [record2:col1, record2:col2...] [...] ]
|
|
338
|
-
def multiple_columns(*args)
|
|
339
|
-
@records.inject([]){ |memo, record|
|
|
340
|
-
memo << args.map{ |arg| record.send(arg) }
|
|
341
|
-
memo
|
|
342
|
-
}
|
|
343
|
-
end
|
|
344
182
|
|
|
345
183
|
# if blank is defined, only the records which are non-blank in that
|
|
346
184
|
# column will actually be yielded. The rest will be set to the provided
|
|
@@ -350,102 +188,6 @@ module CsvMadness
|
|
|
350
188
|
alter_column( column, blank, &block )
|
|
351
189
|
end
|
|
352
190
|
end
|
|
353
|
-
|
|
354
|
-
def alter_column( column, blank = :undefined, &block )
|
|
355
|
-
raise "Column does not exist: #{column}" unless @columns.include?( column )
|
|
356
|
-
|
|
357
|
-
if cindex = @columns.index( column )
|
|
358
|
-
for record in @records
|
|
359
|
-
if record.blank?(column) && blank != :undefined
|
|
360
|
-
record[cindex] = blank
|
|
361
|
-
else
|
|
362
|
-
record[cindex] = yield( record[cindex], record )
|
|
363
|
-
end
|
|
364
|
-
end
|
|
365
|
-
end
|
|
366
|
-
end
|
|
367
|
-
|
|
368
|
-
# If no block given, adds an empty column
|
|
369
|
-
def add_column( column, &block )
|
|
370
|
-
raise "Column already exists: #{column}" if @columns.include?( column )
|
|
371
|
-
raise "#{column} is in the list FORBIDDEN_COLUMN_NAMES" if FORBIDDEN_COLUMN_NAMES.include?(column)
|
|
372
|
-
@columns << column
|
|
373
|
-
|
|
374
|
-
# add empty column to each row
|
|
375
|
-
@records.map{ |r|
|
|
376
|
-
r.csv_data << {column => ""}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
update_data_accessor_module
|
|
380
|
-
|
|
381
|
-
if block_given?
|
|
382
|
-
alter_column( column ) do |val, record|
|
|
383
|
-
yield val, record
|
|
384
|
-
end
|
|
385
|
-
end
|
|
386
|
-
end
|
|
387
|
-
|
|
388
|
-
def drop_column( column )
|
|
389
|
-
raise "Column does not exist" unless @columns.include?( column )
|
|
390
|
-
|
|
391
|
-
@columns.delete( column )
|
|
392
|
-
|
|
393
|
-
key = column.to_s
|
|
394
|
-
|
|
395
|
-
@records.map{ |r|
|
|
396
|
-
r.csv_data.delete( key )
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
update_data_accessor_module
|
|
400
|
-
end
|
|
401
|
-
|
|
402
|
-
def rename_column( column, new_name )
|
|
403
|
-
@columns[@columns.index(column)] = new_name
|
|
404
|
-
rename_index_column( column, new_name ) if @index_columns.include?( column )
|
|
405
|
-
update_data_accessor_module
|
|
406
|
-
end
|
|
407
|
-
|
|
408
|
-
def set_column_type( column, type, blank = :undefined )
|
|
409
|
-
alter_column( column, blank, &COLUMN_TYPES[type] )
|
|
410
|
-
end
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
# If :reverse_merge is true, then the dest column is only overwritten for records where :dest is blank
|
|
414
|
-
def merge_columns( source, dest, opts = {} )
|
|
415
|
-
opts = { :drop_source => true, :reverse_merge => false, :default => "" }.merge( opts )
|
|
416
|
-
column_must_exist( source, dest )
|
|
417
|
-
|
|
418
|
-
self.records.each do |record|
|
|
419
|
-
if opts[:reverse_merge] == false || record.blank?( dest )
|
|
420
|
-
record[dest] = record.blank?(source) ? opts[:default] : record[source]
|
|
421
|
-
end
|
|
422
|
-
end
|
|
423
|
-
|
|
424
|
-
self.drop_column( source ) if opts[:drop_source]
|
|
425
|
-
end
|
|
426
|
-
|
|
427
|
-
# By default, the
|
|
428
|
-
def concat_columns( col1, col2, opts = {} )
|
|
429
|
-
opts = {:separator => '', :out => col1}.merge( opts )
|
|
430
|
-
|
|
431
|
-
column_must_exist( col1, col2 )
|
|
432
|
-
self.add_column( opts[:out] ) unless self.columns.include?( opts[:out] )
|
|
433
|
-
|
|
434
|
-
for record in self.records
|
|
435
|
-
record[ opts[:out] ] = "#{record[col1]}#{opts[:separator]}#{record[col2]}"
|
|
436
|
-
end
|
|
437
|
-
end
|
|
438
|
-
|
|
439
|
-
alias :concatenate :concat_columns
|
|
440
|
-
|
|
441
|
-
# Note: If a block is given, the mod arg will be ignored.
|
|
442
|
-
def add_record_methods( mod = nil, &block )
|
|
443
|
-
if block_given?
|
|
444
|
-
mod = Module.new( &block )
|
|
445
|
-
end
|
|
446
|
-
@record_class.send( :include, mod )
|
|
447
|
-
self
|
|
448
|
-
end
|
|
449
191
|
|
|
450
192
|
# Note: If implementation of Record[] changes, so must this.
|
|
451
193
|
def nils_are_blank_strings
|
|
@@ -459,74 +201,50 @@ module CsvMadness
|
|
|
459
201
|
end
|
|
460
202
|
|
|
461
203
|
protected
|
|
462
|
-
attr_writer :columns, :
|
|
204
|
+
attr_writer :columns, :records, :record_class, :opts
|
|
463
205
|
|
|
464
206
|
def load_csv
|
|
207
|
+
puts "Options: #{@opts.inspect}"
|
|
465
208
|
|
|
466
209
|
# encoding seems to solve a specific problem with a specific spreadsheet, at an unknown cost.
|
|
467
|
-
|
|
468
|
-
{ write_headers: true,
|
|
469
|
-
headers: ( @opts[:header] ? :first_row : false ) } )
|
|
470
|
-
end
|
|
210
|
+
data = File.read(@spreadsheet_file).force_encoding("ISO-8859-1").encode("UTF-8")
|
|
471
211
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
end
|
|
475
|
-
|
|
476
|
-
def add_to_indexes( records )
|
|
477
|
-
if records.is_a?( Array )
|
|
478
|
-
for record in records
|
|
479
|
-
add_to_indexes( record )
|
|
480
|
-
end
|
|
481
|
-
else
|
|
482
|
-
record = records
|
|
483
|
-
for col in @index_columns
|
|
484
|
-
add_to_index( col, record.send(col), record )
|
|
485
|
-
end
|
|
486
|
-
end
|
|
487
|
-
end
|
|
488
|
-
|
|
489
|
-
def remove_from_index( record )
|
|
490
|
-
for col in @index_columns
|
|
491
|
-
@indexes[col].delete( record.send(col) )
|
|
492
|
-
end
|
|
493
|
-
end
|
|
494
|
-
|
|
495
|
-
# Reindexes the record lookup tables.
|
|
496
|
-
def reindex
|
|
497
|
-
@indexes = {}
|
|
498
|
-
add_to_indexes( @records )
|
|
499
|
-
end
|
|
500
|
-
|
|
501
|
-
# shouldn't require reindex
|
|
502
|
-
def rename_index_column( column, new_name )
|
|
503
|
-
@index_columns[ @index_columns.index( column ) ] = new_name
|
|
504
|
-
@indexes[new_name] = @indexes[column]
|
|
505
|
-
@indexes.delete(column)
|
|
506
|
-
end
|
|
507
|
-
|
|
508
|
-
# Each spreadsheet has its own anonymous record class, and each CSV row instantiates
|
|
509
|
-
# a record of this class. This is where the getters and setters come from.
|
|
510
|
-
def create_record_class
|
|
511
|
-
create_data_accessor_module
|
|
512
|
-
@record_class = Class.new( CsvMadness::Record )
|
|
513
|
-
@record_class.spreadsheet = self
|
|
514
|
-
@record_class.send( :include, @module )
|
|
212
|
+
header_flag = @opts[:header] ? :first_row : false
|
|
213
|
+
@csv = CSV.new( data, write_headers: true, headers: header_flag ) # , { write_headers: true, headers: header_flag } ) API changed with CSV 3.x
|
|
515
214
|
end
|
|
215
|
+
|
|
216
|
+
# No longer needed.
|
|
217
|
+
# # Each spreadsheet has its own anonymous record class, and each CSV row instantiates
|
|
218
|
+
# # a record of this class. This is where the getters and setters come from.
|
|
219
|
+
# def create_record_class
|
|
220
|
+
# create_data_accessor_module
|
|
221
|
+
# @record_class = Class.new( CsvMadness::Record )
|
|
222
|
+
# @record_class.spreadsheet = self
|
|
223
|
+
# @record_class.send( :include, @module )
|
|
224
|
+
# end
|
|
516
225
|
|
|
517
226
|
# fetch the original headers from the CSV file. If opts[:headers] is false,
|
|
518
227
|
# or the CSV file isn't loaded yet, returns an empty array.
|
|
228
|
+
#
|
|
229
|
+
#
|
|
230
|
+
# I think that somewhere between v2 and v3 of CSV, they changed the way headers get loaded.
|
|
231
|
+
# For whatever reason, something's broken here.
|
|
519
232
|
def fetch_csv_headers
|
|
520
233
|
if @csv && @opts[:header]
|
|
521
|
-
if @csv.headers
|
|
522
|
-
@csv.
|
|
523
|
-
|
|
234
|
+
if @csv.headers.is_a?(Array)
|
|
235
|
+
headers = @csv.headers # already loaded
|
|
236
|
+
elsif @csv.header_row?
|
|
237
|
+
# WARNING: This is probably a really expensive way to get the headers...
|
|
238
|
+
headers = @csv.read.headers.map(&:to_sym)
|
|
524
239
|
@csv.rewind # shift/rewind, else @csv.headers only returns 'true'
|
|
525
240
|
else
|
|
526
|
-
|
|
241
|
+
warn( "NO HEADERS in CSV" )
|
|
242
|
+
headers = @csv.headers || []
|
|
527
243
|
end
|
|
244
|
+
|
|
528
245
|
headers
|
|
529
246
|
else
|
|
247
|
+
warn( "THE ELSE HAPPENED")
|
|
530
248
|
[]
|
|
531
249
|
end
|
|
532
250
|
end
|
|
@@ -537,25 +255,35 @@ module CsvMadness
|
|
|
537
255
|
# otherwise, you end up with accessors like record.col1, record.col2, record.col3...
|
|
538
256
|
# If the columns given doesn't match the number of columns in the spreadsheet
|
|
539
257
|
# prints a warning and a comparison of the columns to the headers.
|
|
540
|
-
def
|
|
258
|
+
def set_columns( columns = nil )
|
|
541
259
|
if columns.nil?
|
|
542
260
|
if @opts[:header] == false
|
|
543
|
-
columns = (0...csv_column_count).map{ |i|
|
|
261
|
+
columns = (0...csv_column_count).map{ |i| self.class.default_column_name(i) }
|
|
544
262
|
else
|
|
545
|
-
|
|
263
|
+
header_list = fetch_csv_headers
|
|
264
|
+
columns = header_list.each_with_index.map do |name, index|
|
|
265
|
+
self.class.getter_name( name || self.class.default_column_name( index ) )
|
|
266
|
+
end
|
|
546
267
|
end
|
|
547
268
|
else
|
|
548
269
|
unless !@csv || columns.length == csv_column_count
|
|
549
270
|
$stderr.puts "Warning <#{@spreadsheet_file}>: columns array does not match the number of columns in the spreadsheet."
|
|
271
|
+
@columns = columns
|
|
550
272
|
compare_columns_to_headers
|
|
551
273
|
end
|
|
552
274
|
end
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
end
|
|
557
|
-
|
|
275
|
+
|
|
276
|
+
raise_on_forbidden_column_names( columns )
|
|
277
|
+
|
|
558
278
|
@columns = columns
|
|
279
|
+
set_column_mapping
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def set_column_mapping
|
|
283
|
+
@column_mapping = @columns.each_with_index.inject( {} ) do |memo, col_with_index|
|
|
284
|
+
memo[col_with_index.first.to_sym] = col_with_index.last
|
|
285
|
+
memo
|
|
286
|
+
end
|
|
559
287
|
end
|
|
560
288
|
|
|
561
289
|
# Printout so the user can see which CSV columns are being matched to which
|
|
@@ -563,18 +291,19 @@ module CsvMadness
|
|
|
563
291
|
# column list.
|
|
564
292
|
def compare_columns_to_headers
|
|
565
293
|
headers = fetch_csv_headers
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
294
|
+
max_index = ([@columns, headers].map(&:length).max)
|
|
295
|
+
|
|
296
|
+
for i in 0 ... max_index
|
|
297
|
+
$stdout.puts "\t#{i}: #{@columns[i].inspect} ==> #{headers[i].inspect}"
|
|
569
298
|
end
|
|
570
299
|
end
|
|
571
300
|
|
|
572
301
|
|
|
573
302
|
# Create objects that respond to the recipe-named methods
|
|
574
|
-
def
|
|
303
|
+
def package_each_csv_row
|
|
575
304
|
@records = []
|
|
576
305
|
(@csv || []).each do |row|
|
|
577
|
-
@records <<
|
|
306
|
+
@records << Record.new( row, self )
|
|
578
307
|
end
|
|
579
308
|
end
|
|
580
309
|
|
|
@@ -583,27 +312,28 @@ module CsvMadness
|
|
|
583
312
|
fetch_csv_headers.length
|
|
584
313
|
end
|
|
585
314
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
315
|
+
# # returns a mapping based off the current ordered list of columns.
|
|
316
|
+
# # A hash where the first column
|
|
317
|
+
# def columns_to_mapping
|
|
318
|
+
# @columns.each_with_index.inject({}){ |memo, item|
|
|
319
|
+
# memo[item.first] = item.last
|
|
320
|
+
# memo
|
|
321
|
+
# }
|
|
322
|
+
# end
|
|
323
|
+
#
|
|
324
|
+
# def create_data_accessor_module
|
|
325
|
+
# # columns = @columns # yes, this line is necessary. Module.new has its own @vars.
|
|
326
|
+
#
|
|
327
|
+
# @module = DataAccessorModule.new( columns_to_mapping )
|
|
328
|
+
# end
|
|
329
|
+
#
|
|
330
|
+
# def update_data_accessor_module
|
|
331
|
+
# debugger
|
|
332
|
+
# @module.remap_accessors( columns_to_mapping )
|
|
333
|
+
# end
|
|
334
|
+
#
|
|
335
|
+
# def column_accessors_map
|
|
336
|
+
# @module.column_accessors_map
|
|
337
|
+
# end
|
|
608
338
|
end
|
|
609
339
|
end
|