csv_madness 0.0.6 → 0.0.9

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.
@@ -1,144 +1,16 @@
1
1
  module CsvMadness
2
2
  class Sheet
3
- COLUMN_TYPES = {
4
- number: Proc.new do |cell, record|
5
- rval = cell
6
-
7
- unless cell.nil? || (cell.is_a?(String) && cell.length == 0)
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
- }
3
+ extend SheetMethods::ClassMethods
4
+ include SheetMethods::Base
5
+ include SheetMethods::Indexing
6
+ include SheetMethods::FileMethods
7
+ include SheetMethods::ColumnMethods
8
+ include SheetMethods::RecordMethods
57
9
 
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
87
10
 
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
-
116
- # opts are passed to underlying CSV (:row_sep, :encoding, :force_quotes)
117
- def self.to_csv( spreadsheet, opts = {} )
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
123
-
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
11
+ attr_reader :columns, :index_columns, :record_class, :opts
12
+ attr_accessor :spreadsheet_file
140
13
 
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
55
 
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
-
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 set_index_columns( index_columns )
247
- @index_columns = case index_columns
248
- when NilClass
249
- []
250
- when Symbol
251
- [ index_columns ]
252
- when Array
253
- index_columns
57
+ def to_csv( opts = {} )
58
+ self.records.inject( self.columns.to_csv( opts ) ) do |output, record|
59
+ output << record.to_csv( opts )
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
-
282
- rval
283
- end
284
66
 
285
- # removes rows which fail the given test from the spreadsheet.
286
- def filter!( &block )
287
- @records = self.filter( &block )
288
- reindex
289
- @records
67
+ def records
68
+ @records ||= []
290
69
  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,7 +140,7 @@ module CsvMadness
459
140
  end
460
141
 
461
142
  protected
462
- attr_writer :columns, :index_columns, :records, :spreadsheet_file, :record_class, :opts
143
+ attr_writer :columns, :index_columns, :records, :record_class, :opts
463
144
 
464
145
  def load_csv
465
146
 
@@ -468,42 +149,6 @@ module CsvMadness
468
149
  { write_headers: true,
469
150
  headers: ( @opts[:header] ? :first_row : false ) } )
470
151
  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)
506
- end
507
152
 
508
153
  # Each spreadsheet has its own anonymous record class, and each CSV row instantiates
509
154
  # a record of this class. This is where the getters and setters come from.
@@ -547,14 +192,13 @@ module CsvMadness
547
192
  else
548
193
  unless !@csv || columns.length == csv_column_count
549
194
  $stderr.puts "Warning <#{@spreadsheet_file}>: columns array does not match the number of columns in the spreadsheet."
195
+ @columns = columns
550
196
  compare_columns_to_headers
551
197
  end
552
198
  end
553
-
554
- for column in columns
555
- raise "#{column} is in the list FORBIDDEN_COLUMN_NAMES" if FORBIDDEN_COLUMN_NAMES.include?(column)
556
- end
557
-
199
+
200
+ raise_on_forbidden_column_names( columns )
201
+
558
202
  @columns = columns
559
203
  end
560
204
 
@@ -563,9 +207,10 @@ module CsvMadness
563
207
  # column list.
564
208
  def compare_columns_to_headers
565
209
  headers = fetch_csv_headers
566
-
567
- for i in 0...([@columns, headers].map(&:length).max)
568
- $stdout.puts "\t#{i}: #{@columns[i]} ==> #{headers[i]}"
210
+ max_index = ([@columns, headers].map(&:length).max)
211
+
212
+ for i in 0 ... max_index
213
+ $stdout.puts "\t#{i}: #{@columns[i].inspect} ==> #{headers[i].inspect}"
569
214
  end
570
215
  end
571
216
 
@@ -574,7 +219,7 @@ module CsvMadness
574
219
  def package
575
220
  @records = []
576
221
  (@csv || []).each do |row|
577
- @records << @record_class.new( row )
222
+ @records << @record_class.new( row, self.column_accessors_map )
578
223
  end
579
224
  end
580
225
 
@@ -583,6 +228,8 @@ module CsvMadness
583
228
  fetch_csv_headers.length
584
229
  end
585
230
 
231
+ # returns a mapping based off the current ordered list of columns.
232
+ # A hash where the first column
586
233
  def columns_to_mapping
587
234
  @columns.each_with_index.inject({}){ |memo, item|
588
235
  memo[item.first] = item.last
@@ -597,13 +244,12 @@ module CsvMadness
597
244
  end
598
245
 
599
246
  def update_data_accessor_module
247
+ debugger
600
248
  @module.remap_accessors( columns_to_mapping )
601
249
  end
602
-
603
- def column_must_exist( *cols )
604
- for col in cols
605
- raise ArgumentError.new( "#{caller[0]}: column :#{col} does not exist.") unless self.columns.include?(col)
606
- end
250
+
251
+ def column_accessors_map
252
+ @module.column_accessors_map
607
253
  end
608
254
  end
609
255
  end
@@ -0,0 +1,6 @@
1
+ module CsvMadness
2
+ module SheetMethods
3
+ module Base
4
+ end
5
+ end
6
+ 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( opts )
66
+ spreadsheet.records.inject( out ) do |output, record|
67
+ output << record.to_csv( opts )
68
+ end
69
+ end
70
+
71
+ def write_to_file( spreadsheet, file, opts = {} )
72
+ file = file.fwf_filepath.expand_path
73
+ file.write( spreadsheet.to_csv( opts ) )
74
+ end
75
+ end
76
+ end
77
+ end