csv_madness 0.0.6 → 0.0.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.
- checksums.yaml +5 -13
- data/Gemfile +2 -3
- data/README.rdoc +55 -47
- data/Rakefile +79 -52
- data/VERSION +1 -1
- data/lib/csv_madness/builder.rb +19 -14
- data/lib/csv_madness/data_accessor_module.rb +59 -24
- data/lib/csv_madness/exceptions.rb +16 -0
- data/lib/csv_madness/record.rb +4 -2
- data/lib/csv_madness/sheet.rb +35 -390
- data/lib/csv_madness/sheet_methods/base.rb +6 -0
- data/lib/csv_madness/sheet_methods/class_methods.rb +77 -0
- data/lib/csv_madness/sheet_methods/column_methods.rb +187 -0
- data/lib/csv_madness/sheet_methods/file_methods.rb +41 -0
- data/lib/csv_madness/sheet_methods/indexing.rb +52 -0
- data/lib/csv_madness/sheet_methods/record_methods.rb +99 -0
- data/lib/csv_madness.rb +1 -3
- data/test/helper.rb +5 -0
- data/test/test_builder.rb +8 -4
- data/test/test_csv_madness.rb +6 -7
- data/test/test_reloading_spreadsheet.rb +2 -1
- data/test/test_sheet.rb +28 -18
- metadata +54 -36
data/lib/csv_madness/sheet.rb
CHANGED
|
@@ -1,144 +1,16 @@
|
|
|
1
1
|
module CsvMadness
|
|
2
2
|
class Sheet
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
begin
|
|
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
|
|
3
|
+
extend SheetMethods::ClassMethods
|
|
4
|
+
include SheetMethods::Base
|
|
5
|
+
include SheetMethods::Indexing
|
|
6
|
+
include SheetMethods::FileMethods
|
|
7
|
+
include SheetMethods::ColumnMethods
|
|
8
|
+
include SheetMethods::RecordMethods
|
|
83
9
|
|
|
84
|
-
def self.search_paths
|
|
85
|
-
@search_paths
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def self.from( csv_file, opts = {} )
|
|
89
|
-
if f = find_spreadsheet_in_filesystem( csv_file )
|
|
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
|
|
114
|
-
end
|
|
115
10
|
|
|
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
|
|
11
|
+
attr_reader :columns, :index_columns, :record_class, :opts
|
|
12
|
+
attr_accessor :spreadsheet_file
|
|
123
13
|
|
|
124
|
-
def self.write_to_file( spreadsheet, file, opts = {} )
|
|
125
|
-
file = file.fwf_filepath.expand_path
|
|
126
|
-
File.open( file, "w" ) do |f|
|
|
127
|
-
f << spreadsheet.to_csv( opts )
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def write_to_file( file, opts = {} )
|
|
132
|
-
self.class.write_to_file( self, file, opts )
|
|
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
|
|
142
14
|
# opts:
|
|
143
15
|
# index: ( [:id, :id2 ] )
|
|
144
16
|
# columns you want mapped for quick
|
|
@@ -154,6 +26,8 @@ module CsvMadness
|
|
|
154
26
|
# header: false
|
|
155
27
|
# anything else, we assume the csv file has a header row
|
|
156
28
|
def initialize( *args )
|
|
29
|
+
@csv = nil
|
|
30
|
+
|
|
157
31
|
if args.last.is_a?(Hash)
|
|
158
32
|
@opts = args.pop
|
|
159
33
|
else
|
|
@@ -178,117 +52,22 @@ module CsvMadness
|
|
|
178
52
|
reload_spreadsheet
|
|
179
53
|
end
|
|
180
54
|
|
|
181
|
-
def <<( record )
|
|
182
|
-
self.add_record( record )
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
def add_record( record )
|
|
186
|
-
case record
|
|
187
|
-
when Array
|
|
188
|
-
# CSV::Row.new( column names, column_entries ) (in same order as columns, natch)
|
|
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
|
|
202
|
-
else
|
|
203
|
-
raise "sheet.add_record() doesn't take objects of type #{record.inspect}" unless record.respond_to?(:csv_data)
|
|
204
|
-
record = record.csv_data
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
record = @record_class.new( record )
|
|
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
55
|
|
|
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 )
|
|
232
|
-
end
|
|
233
|
-
end
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
def reload_spreadsheet( opts = @opts )
|
|
237
|
-
load_csv if @spreadsheet_file
|
|
238
|
-
set_initial_columns( opts[:columns] )
|
|
239
|
-
create_record_class
|
|
240
|
-
package
|
|
241
|
-
|
|
242
|
-
set_index_columns( opts[:index] )
|
|
243
|
-
reindex
|
|
244
|
-
end
|
|
245
56
|
|
|
246
|
-
def
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
[]
|
|
250
|
-
when Symbol
|
|
251
|
-
[ index_columns ]
|
|
252
|
-
when Array
|
|
253
|
-
index_columns
|
|
57
|
+
def to_csv
|
|
58
|
+
self.records.inject( self.columns.to_csv ) do |output, record|
|
|
59
|
+
output << record.to_csv
|
|
254
60
|
end
|
|
255
61
|
end
|
|
256
|
-
|
|
62
|
+
|
|
257
63
|
def [] offset
|
|
258
64
|
@records[offset]
|
|
259
65
|
end
|
|
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]
|
|
270
|
-
end
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
# function should take an object, and return either true or false
|
|
274
|
-
# returns an array of objects that respond true when put through the
|
|
275
|
-
# meat grinder
|
|
276
|
-
def filter( &block )
|
|
277
|
-
rval = []
|
|
278
|
-
@records.each do |record|
|
|
279
|
-
rval << record if ( yield record )
|
|
280
|
-
end
|
|
281
66
|
|
|
282
|
-
|
|
67
|
+
def records
|
|
68
|
+
@records ||= []
|
|
283
69
|
end
|
|
284
|
-
|
|
285
|
-
# removes rows which fail the given test from the spreadsheet.
|
|
286
|
-
def filter!( &block )
|
|
287
|
-
@records = self.filter( &block )
|
|
288
|
-
reindex
|
|
289
|
-
@records
|
|
290
|
-
end
|
|
291
|
-
|
|
70
|
+
|
|
292
71
|
|
|
293
72
|
# give a copy of the current spreadsheet, but with no records
|
|
294
73
|
def blanked()
|
|
@@ -329,18 +108,6 @@ module CsvMadness
|
|
|
329
108
|
# records
|
|
330
109
|
end
|
|
331
110
|
|
|
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
111
|
|
|
345
112
|
# if blank is defined, only the records which are non-blank in that
|
|
346
113
|
# column will actually be yielded. The rest will be set to the provided
|
|
@@ -351,92 +118,6 @@ module CsvMadness
|
|
|
351
118
|
end
|
|
352
119
|
end
|
|
353
120
|
|
|
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
121
|
|
|
441
122
|
# Note: If a block is given, the mod arg will be ignored.
|
|
442
123
|
def add_record_methods( mod = nil, &block )
|
|
@@ -459,50 +140,13 @@ module CsvMadness
|
|
|
459
140
|
end
|
|
460
141
|
|
|
461
142
|
protected
|
|
462
|
-
attr_writer :columns, :index_columns, :records, :
|
|
143
|
+
attr_writer :columns, :index_columns, :records, :record_class, :opts
|
|
463
144
|
|
|
464
145
|
def load_csv
|
|
465
|
-
|
|
466
146
|
# encoding seems to solve a specific problem with a specific spreadsheet, at an unknown cost.
|
|
467
147
|
@csv = CSV.new( File.read(@spreadsheet_file).force_encoding("ISO-8859-1").encode("UTF-8"),
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
end
|
|
471
|
-
|
|
472
|
-
def add_to_index( col, key, record )
|
|
473
|
-
(@indexes[col] ||= {})[key] = record
|
|
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)
|
|
148
|
+
write_headers: true,
|
|
149
|
+
headers: ( @opts[:header] ? :first_row : false ))
|
|
506
150
|
end
|
|
507
151
|
|
|
508
152
|
# Each spreadsheet has its own anonymous record class, and each CSV row instantiates
|
|
@@ -547,14 +191,13 @@ module CsvMadness
|
|
|
547
191
|
else
|
|
548
192
|
unless !@csv || columns.length == csv_column_count
|
|
549
193
|
$stderr.puts "Warning <#{@spreadsheet_file}>: columns array does not match the number of columns in the spreadsheet."
|
|
194
|
+
@columns = columns
|
|
550
195
|
compare_columns_to_headers
|
|
551
196
|
end
|
|
552
197
|
end
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
end
|
|
557
|
-
|
|
198
|
+
|
|
199
|
+
raise_on_forbidden_column_names( columns )
|
|
200
|
+
|
|
558
201
|
@columns = columns
|
|
559
202
|
end
|
|
560
203
|
|
|
@@ -563,9 +206,10 @@ module CsvMadness
|
|
|
563
206
|
# column list.
|
|
564
207
|
def compare_columns_to_headers
|
|
565
208
|
headers = fetch_csv_headers
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
209
|
+
max_index = ([@columns, headers].map(&:length).max)
|
|
210
|
+
|
|
211
|
+
for i in 0 ... max_index
|
|
212
|
+
$stdout.puts "\t#{i}: #{@columns[i].inspect} ==> #{headers[i].inspect}"
|
|
569
213
|
end
|
|
570
214
|
end
|
|
571
215
|
|
|
@@ -574,7 +218,7 @@ module CsvMadness
|
|
|
574
218
|
def package
|
|
575
219
|
@records = []
|
|
576
220
|
(@csv || []).each do |row|
|
|
577
|
-
@records << @record_class.new( row )
|
|
221
|
+
@records << @record_class.new( row, self.column_accessors_map )
|
|
578
222
|
end
|
|
579
223
|
end
|
|
580
224
|
|
|
@@ -583,6 +227,8 @@ module CsvMadness
|
|
|
583
227
|
fetch_csv_headers.length
|
|
584
228
|
end
|
|
585
229
|
|
|
230
|
+
# returns a mapping based off the current ordered list of columns.
|
|
231
|
+
# A hash where the first column
|
|
586
232
|
def columns_to_mapping
|
|
587
233
|
@columns.each_with_index.inject({}){ |memo, item|
|
|
588
234
|
memo[item.first] = item.last
|
|
@@ -597,13 +243,12 @@ module CsvMadness
|
|
|
597
243
|
end
|
|
598
244
|
|
|
599
245
|
def update_data_accessor_module
|
|
246
|
+
debugger
|
|
600
247
|
@module.remap_accessors( columns_to_mapping )
|
|
601
248
|
end
|
|
602
|
-
|
|
603
|
-
def
|
|
604
|
-
|
|
605
|
-
raise ArgumentError.new( "#{caller[0]}: column :#{col} does not exist.") unless self.columns.include?(col)
|
|
606
|
-
end
|
|
249
|
+
|
|
250
|
+
def column_accessors_map
|
|
251
|
+
@module.column_accessors_map
|
|
607
252
|
end
|
|
608
253
|
end
|
|
609
254
|
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
module CsvMadness
|
|
2
|
+
module SheetMethods
|
|
3
|
+
module ClassMethods
|
|
4
|
+
# Used to make getter/setter names out of the original header strings.
|
|
5
|
+
# " hello;: world! " => :hello_world
|
|
6
|
+
def getter_name( name )
|
|
7
|
+
# replace any run of non-word characters with a single "_"
|
|
8
|
+
name = name.downcase.gsub( /(\W|_|\s)+/, "_" )
|
|
9
|
+
|
|
10
|
+
# snip trailing and leading "_"
|
|
11
|
+
name = name.gsub( /(^_+|_+$)/, "" )
|
|
12
|
+
|
|
13
|
+
# add leading "_" when we'd otherwise have a method name starting with a digit
|
|
14
|
+
name = "_#{name}" if name.match( /^\d/ )
|
|
15
|
+
|
|
16
|
+
name.to_sym
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Paths to be searched when CsvMadness.load( "filename.csv" ) is called.
|
|
21
|
+
def add_search_path( path )
|
|
22
|
+
@search_paths ||= []
|
|
23
|
+
path = Pathname.new( path ).expand_path
|
|
24
|
+
unless path.directory?
|
|
25
|
+
raise "The given path does not exist"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
@search_paths << path unless @search_paths.include?( path )
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def search_paths
|
|
32
|
+
@search_paths
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def from( csv_file, opts = {} )
|
|
36
|
+
if f = find_spreadsheet_in_filesystem( csv_file )
|
|
37
|
+
Sheet.new( f, opts )
|
|
38
|
+
else
|
|
39
|
+
raise "File not found."
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Search absolute/relative-to-current-dir before checking
|
|
44
|
+
# search paths.
|
|
45
|
+
def find_spreadsheet_in_filesystem( name )
|
|
46
|
+
@search_paths ||= []
|
|
47
|
+
|
|
48
|
+
expanded_path = Pathname.new( name ).expand_path
|
|
49
|
+
if expanded_path.exist?
|
|
50
|
+
return expanded_path
|
|
51
|
+
else # look for it in the search paths
|
|
52
|
+
@search_paths.each do |p|
|
|
53
|
+
file = p.join( name )
|
|
54
|
+
if file.exist? && file.file?
|
|
55
|
+
return p.join( name )
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# opts are passed to underlying CSV (:row_sep, :encoding, :force_quotes)
|
|
64
|
+
def to_csv( spreadsheet, opts = {} )
|
|
65
|
+
out = spreadsheet.columns.to_csv
|
|
66
|
+
spreadsheet.records.inject( out ) do |output, record|
|
|
67
|
+
output << record.to_csv
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def write_to_file( spreadsheet, file )
|
|
72
|
+
file = file.fwf_filepath.expand_path
|
|
73
|
+
file.write( spreadsheet.to_csv )
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|