csv_madness 0.0.4 → 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.
- checksums.yaml +7 -0
- data/CHANGELOG.markdown +21 -3
- data/Gemfile +9 -7
- data/README.rdoc +136 -95
- data/Rakefile +79 -60
- data/VERSION +1 -1
- data/lib/csv_madness/builder.rb +100 -0
- data/lib/csv_madness/data_accessor_module.rb +59 -24
- data/lib/csv_madness/exceptions.rb +16 -0
- data/lib/csv_madness/gem_api.rb +12 -0
- data/lib/csv_madness/record.rb +41 -2
- data/lib/csv_madness/sheet.rb +101 -292
- 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 +3 -13
- data/test/csv/forbidden_column.csv +2 -0
- data/test/csv/splitter.csv +11 -0
- data/test/csv/test_column_types.csv +3 -0
- data/test/csv/with_nils.csv +5 -0
- data/test/helper.rb +31 -19
- data/test/test_builder.rb +37 -0
- data/test/test_csv_madness.rb +8 -10
- data/test/test_merging_columns.rb +40 -0
- data/test/test_reloading_spreadsheet.rb +31 -0
- data/test/test_sheet.rb +117 -8
- metadata +52 -93
|
@@ -1,44 +1,79 @@
|
|
|
1
|
+
# Each record within the spreadsheet is extended by a module with accessor functions
|
|
2
|
+
# which allow the user to treat column names as methods. When columns are added, dropped, or re-ordered,
|
|
3
|
+
# the accessor module needs to be updated to reflect the change.
|
|
1
4
|
class DataAccessorModule < Module
|
|
5
|
+
# mapping : keys = column symbol
|
|
6
|
+
# values = column index
|
|
7
|
+
attr_accessor :column_accessors_map
|
|
8
|
+
|
|
2
9
|
def initialize( mapping )
|
|
3
|
-
|
|
4
|
-
|
|
10
|
+
puts "Got mapping #{mapping.inspect}"
|
|
11
|
+
@column_accessors_map = mapping
|
|
12
|
+
remap_accessors
|
|
5
13
|
end
|
|
6
14
|
|
|
7
|
-
def install_column_accessors(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
15
|
+
def install_column_accessors( *columns )
|
|
16
|
+
for column in columns.flatten
|
|
17
|
+
unless is_valid_ruby_method?( column )
|
|
18
|
+
warn( "#{column.inspect} is not a valid ruby method" )
|
|
19
|
+
require "data_accessor_module" # TODO: WTF?
|
|
20
|
+
column = "csv_column_#{index}"
|
|
21
|
+
warn( " ----> renaming to #{column.inspect}" )
|
|
12
22
|
end
|
|
23
|
+
|
|
24
|
+
if self.respond_to?( column )
|
|
25
|
+
puts "Column already has accessors"
|
|
26
|
+
else
|
|
27
|
+
eval <<-EOF
|
|
28
|
+
self.send( :define_method, :#{column} ) do
|
|
29
|
+
self.csv_data[ self.column_accessors_map[ :#{column} ] ]
|
|
30
|
+
end
|
|
13
31
|
|
|
14
|
-
|
|
15
|
-
|
|
32
|
+
self.send( :define_method, :#{column}= ) do |val|
|
|
33
|
+
self.csv_data[ self.column_accessors_map[ :#{column} ] ] = val
|
|
34
|
+
end
|
|
35
|
+
EOF
|
|
16
36
|
end
|
|
17
|
-
|
|
37
|
+
end
|
|
18
38
|
end
|
|
19
39
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
40
|
+
# But if instead of hardcoding the index, the index was pulled from the spreadsheet mapping...
|
|
41
|
+
# a titch slower to look up? But a change to the mapping automatically updates the method
|
|
42
|
+
def remove_column_accessors( *columns )
|
|
43
|
+
for column in columns.flatten
|
|
44
|
+
for method_sym in [column, :"#{column}="]
|
|
45
|
+
self.send( :remove_method, method_sym ) if self.respond_to?( method_sym )
|
|
46
|
+
end
|
|
47
|
+
end
|
|
23
48
|
end
|
|
24
49
|
|
|
25
50
|
def remove_all_column_accessors
|
|
26
|
-
|
|
51
|
+
# 2016-11-24 : was this necessary for anything
|
|
52
|
+
# @column_accessors ||= []
|
|
53
|
+
remove_column_accessors( @column_accessors_map.keys )
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Partly obsoleted.
|
|
57
|
+
def remap_accessors( *args )
|
|
58
|
+
#
|
|
27
59
|
|
|
28
|
-
|
|
29
|
-
|
|
60
|
+
|
|
61
|
+
remove_all_column_accessors
|
|
62
|
+
|
|
63
|
+
if args.length == 1
|
|
64
|
+
debugger
|
|
65
|
+
@column_accessors_map = args.first
|
|
30
66
|
end
|
|
31
67
|
|
|
32
|
-
@
|
|
68
|
+
install_column_accessors( @column_accessors_map.keys )
|
|
33
69
|
end
|
|
34
70
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
71
|
+
# Symbol.inspect doesn't output " quotes when given a sym
|
|
72
|
+
# that expresses a valid method name or a valid identifier.
|
|
73
|
+
def is_valid_ruby_method?( str_or_sym )
|
|
74
|
+
sym = str_or_sym.to_sym
|
|
75
|
+
test_string = sym.inspect
|
|
39
76
|
|
|
40
|
-
|
|
41
|
-
install_column_accessors( column, index )
|
|
42
|
-
end
|
|
77
|
+
test_string.match( /^:[@"]/ ).nil?
|
|
43
78
|
end
|
|
44
79
|
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module CsvMadness
|
|
2
|
+
class LoadError < RuntimeError
|
|
3
|
+
end
|
|
4
|
+
|
|
5
|
+
class ColumnError < ArgumentError
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
class ForbiddenColumnNameError < ColumnError
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class MissingColumnError < ColumnError
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class ColumnExistsError < ColumnError
|
|
15
|
+
end
|
|
16
|
+
end
|
data/lib/csv_madness/record.rb
CHANGED
|
@@ -8,8 +8,11 @@ module CsvMadness
|
|
|
8
8
|
# symbol.
|
|
9
9
|
class Record
|
|
10
10
|
attr_accessor :csv_data
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
attr_reader :column_accessors_map
|
|
12
|
+
|
|
13
|
+
def initialize( data, mapping )
|
|
14
|
+
@column_accessors_map = mapping # holds a reference to its spreadsheet's overall mapping
|
|
15
|
+
import_record_data( data )
|
|
13
16
|
end
|
|
14
17
|
|
|
15
18
|
def [] key
|
|
@@ -34,6 +37,10 @@ module CsvMadness
|
|
|
34
37
|
self.class.spreadsheet.columns
|
|
35
38
|
end
|
|
36
39
|
|
|
40
|
+
def self.columns
|
|
41
|
+
self.spreadsheet.columns
|
|
42
|
+
end
|
|
43
|
+
|
|
37
44
|
def self.spreadsheet= sheet
|
|
38
45
|
@spreadsheet = sheet
|
|
39
46
|
end
|
|
@@ -46,8 +53,40 @@ module CsvMadness
|
|
|
46
53
|
self.columns.map{|col| self.send(col) }.to_csv( opts )
|
|
47
54
|
end
|
|
48
55
|
|
|
56
|
+
def to_hash
|
|
57
|
+
self.columns.inject({}){ |hash, col| hash[col] = self.send( col ); hash }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def to_a
|
|
61
|
+
self.to_hash.to_a
|
|
62
|
+
end
|
|
63
|
+
|
|
49
64
|
def blank?( col )
|
|
50
65
|
(self.send( col.to_sym ).to_s || "").strip.length == 0
|
|
51
66
|
end
|
|
67
|
+
|
|
68
|
+
protected
|
|
69
|
+
def import_record_data( data )
|
|
70
|
+
case data
|
|
71
|
+
when Array
|
|
72
|
+
csv_data = CSV::Row.new( self.columns, data )
|
|
73
|
+
when Hash
|
|
74
|
+
fields = self.columns.map do |col|
|
|
75
|
+
data[col]
|
|
76
|
+
end
|
|
77
|
+
# for col in self.columns
|
|
78
|
+
# fields << data[col]
|
|
79
|
+
# end
|
|
80
|
+
|
|
81
|
+
csv_data = CSV::Row.new( self.columns, fields )
|
|
82
|
+
when CSV::Row
|
|
83
|
+
csv_data = data
|
|
84
|
+
else
|
|
85
|
+
raise "record.import_record_data() doesn't take objects of type #{data.inspect}" unless data.respond_to?(:csv_data)
|
|
86
|
+
csv_data = data.csv_data.clone
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
@csv_data = csv_data
|
|
90
|
+
end
|
|
52
91
|
end
|
|
53
92
|
end
|
data/lib/csv_madness/sheet.rb
CHANGED
|
@@ -1,114 +1,16 @@
|
|
|
1
1
|
module CsvMadness
|
|
2
2
|
class Sheet
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
end
|
|
10
|
-
end,
|
|
11
|
-
|
|
12
|
-
integer: Proc.new do |cell, record|
|
|
13
|
-
cell.to_i
|
|
14
|
-
end,
|
|
15
|
-
|
|
16
|
-
float: Proc.new do |cell, record|
|
|
17
|
-
cell.to_f
|
|
18
|
-
end,
|
|
19
|
-
|
|
20
|
-
date: Proc.new do |cell, record|
|
|
21
|
-
begin
|
|
22
|
-
parse = Time.parse( cell )
|
|
23
|
-
rescue ArgumentError
|
|
24
|
-
parse = "Invalid Time Format: <#{cell}>"
|
|
25
|
-
end
|
|
26
|
-
parse
|
|
27
|
-
end
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
# Used to make getter/setter names out of the original header strings.
|
|
31
|
-
# " hello;: world! " => :hello_world
|
|
32
|
-
def self.getter_name( name )
|
|
33
|
-
name = name.strip.gsub( /\s+/, "_" ).gsub( /(\W|_)+/, "_" ).downcase
|
|
34
|
-
name = name.gsub( /_+$/, "" )
|
|
35
|
-
if name.match( /^\d/ )
|
|
36
|
-
name = "_#{name}"
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
name.to_sym
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
# Paths to be searched when CsvMadness.load( "filename.csv" ) is called.
|
|
44
|
-
def self.add_search_path( path )
|
|
45
|
-
@search_paths ||= []
|
|
46
|
-
path = Pathname.new( path ).expand_path
|
|
47
|
-
unless path.directory?
|
|
48
|
-
raise "The given path does not exist"
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
@search_paths << path unless @search_paths.include?( path )
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def self.search_paths
|
|
55
|
-
@search_paths
|
|
56
|
-
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
|
|
57
9
|
|
|
58
|
-
def self.from( csv_file, opts = {} )
|
|
59
|
-
if f = find_spreadsheet_in_filesystem( csv_file )
|
|
60
|
-
Sheet.new( f, opts )
|
|
61
|
-
else
|
|
62
|
-
raise "File not found."
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
10
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def self.find_spreadsheet_in_filesystem( name )
|
|
69
|
-
@search_paths ||= []
|
|
70
|
-
|
|
71
|
-
expanded_path = Pathname.new( name ).expand_path
|
|
72
|
-
if expanded_path.exist?
|
|
73
|
-
return expanded_path
|
|
74
|
-
else # look for it in the search paths
|
|
75
|
-
@search_paths.each do |p|
|
|
76
|
-
file = p.join( name )
|
|
77
|
-
if file.exist? && file.file?
|
|
78
|
-
return p.join( name )
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
nil
|
|
84
|
-
end
|
|
11
|
+
attr_reader :columns, :index_columns, :record_class, :opts
|
|
12
|
+
attr_accessor :spreadsheet_file
|
|
85
13
|
|
|
86
|
-
# opts are passed to underlying CSV (:row_sep, :encoding, :force_quotes)
|
|
87
|
-
def self.to_csv( spreadsheet, opts = {} )
|
|
88
|
-
out = spreadsheet.columns.to_csv( opts )
|
|
89
|
-
spreadsheet.records.inject( out ) do |output, record|
|
|
90
|
-
output << record.to_csv( opts )
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def self.write_to_file( spreadsheet, file, opts = {} )
|
|
95
|
-
file = file.fwf_filepath.expand_path
|
|
96
|
-
File.open( file, "w" ) do |f|
|
|
97
|
-
f << spreadsheet.to_csv( opts )
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def write_to_file( file, opts = {} )
|
|
102
|
-
self.class.write_to_file( self, file, opts )
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def to_csv( opts = {} )
|
|
106
|
-
self.records.inject( self.columns.to_csv( opts ) ) do |output, record|
|
|
107
|
-
output << record.to_csv( opts )
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
attr_reader :columns, :index_columns, :records, :spreadsheet_file, :record_class
|
|
112
14
|
# opts:
|
|
113
15
|
# index: ( [:id, :id2 ] )
|
|
114
16
|
# columns you want mapped for quick
|
|
@@ -123,88 +25,90 @@ module CsvMadness
|
|
|
123
25
|
#
|
|
124
26
|
# header: false
|
|
125
27
|
# anything else, we assume the csv file has a header row
|
|
126
|
-
def initialize(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
28
|
+
def initialize( *args )
|
|
29
|
+
@csv = nil
|
|
30
|
+
|
|
31
|
+
if args.last.is_a?(Hash)
|
|
32
|
+
@opts = args.pop
|
|
130
33
|
else
|
|
131
|
-
@
|
|
34
|
+
@opts = {}
|
|
132
35
|
end
|
|
133
|
-
|
|
36
|
+
|
|
37
|
+
firstarg = args.shift
|
|
38
|
+
|
|
39
|
+
case firstarg
|
|
40
|
+
when NilClass
|
|
41
|
+
@spreadsheet_file = nil
|
|
42
|
+
@opts[:columns] ||= []
|
|
43
|
+
when String, FunWith::Files::FilePath, Pathname
|
|
44
|
+
@spreadsheet_file = self.class.find_spreadsheet_in_filesystem( firstarg )
|
|
45
|
+
when Array
|
|
46
|
+
@spreadsheet_file = nil
|
|
47
|
+
@opts[:columns] ||= firstarg
|
|
48
|
+
end
|
|
49
|
+
|
|
134
50
|
@opts[:header] = (@opts[:header] == false ? false : true) # true unless already explicitly set to false
|
|
135
51
|
|
|
136
52
|
reload_spreadsheet
|
|
137
53
|
end
|
|
138
54
|
|
|
139
|
-
|
|
140
|
-
load_csv
|
|
141
|
-
set_initial_columns( opts[:columns] )
|
|
142
|
-
create_record_class
|
|
143
|
-
package
|
|
144
|
-
|
|
145
|
-
set_index_columns( opts[:index] )
|
|
146
|
-
reindex
|
|
147
|
-
end
|
|
55
|
+
|
|
148
56
|
|
|
149
|
-
def
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
[]
|
|
153
|
-
when Symbol
|
|
154
|
-
[ index_columns ]
|
|
155
|
-
when Array
|
|
156
|
-
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 )
|
|
157
60
|
end
|
|
158
61
|
end
|
|
159
|
-
|
|
62
|
+
|
|
160
63
|
def [] offset
|
|
161
64
|
@records[offset]
|
|
162
65
|
end
|
|
163
|
-
|
|
164
|
-
# Fetches an indexed record based on the column indexed and the keying object.
|
|
165
|
-
# If key is an array of keying objects, returns an array of records in the
|
|
166
|
-
# same order that the keying objects appear.
|
|
167
|
-
# Index column should yield a different, unique value for each record.
|
|
168
|
-
def fetch( index_col, key )
|
|
169
|
-
if key.is_a?(Array)
|
|
170
|
-
key.map{ |key| @indexes[index_col][key] }
|
|
171
|
-
else
|
|
172
|
-
@indexes[index_col][key]
|
|
173
|
-
end
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
# function should take an object, and return either true or false
|
|
177
|
-
# returns an array of objects that respond true when put through the
|
|
178
|
-
# meat grinder
|
|
179
|
-
def filter( &block )
|
|
180
|
-
rval = []
|
|
181
|
-
@records.each do |record|
|
|
182
|
-
rval << record if ( yield record )
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
rval
|
|
186
|
-
end
|
|
187
66
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
@records = self.filter( &block )
|
|
191
|
-
reindex
|
|
192
|
-
@records
|
|
67
|
+
def records
|
|
68
|
+
@records ||= []
|
|
193
69
|
end
|
|
194
70
|
|
|
195
|
-
|
|
196
|
-
|
|
71
|
+
|
|
72
|
+
# give a copy of the current spreadsheet, but with no records
|
|
73
|
+
def blanked()
|
|
74
|
+
sheet = self.class.new
|
|
75
|
+
sheet.columns = @columns.clone
|
|
76
|
+
sheet.index_columns = @index_columns.clone
|
|
77
|
+
sheet.records = []
|
|
78
|
+
sheet.spreadsheet_file = nil
|
|
79
|
+
sheet.create_data_accessor_module
|
|
80
|
+
sheet.create_record_class
|
|
81
|
+
sheet.opts = @opts.clone
|
|
82
|
+
sheet.reindex
|
|
83
|
+
|
|
84
|
+
sheet
|
|
197
85
|
end
|
|
198
86
|
|
|
199
|
-
#
|
|
200
|
-
#
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
87
|
+
# give a block, and get back a hash.
|
|
88
|
+
# The hash keys are the results of the block.
|
|
89
|
+
# The hash values are copies of the spreadsheets, with only the records
|
|
90
|
+
# which caused the block to return the key.
|
|
91
|
+
def split( &block )
|
|
92
|
+
sheets = Hash.new
|
|
93
|
+
|
|
94
|
+
for record in @records
|
|
95
|
+
result_key = yield record
|
|
96
|
+
( sheets[result_key] ||= self.blanked() ) << record
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
sheets
|
|
100
|
+
# sheet_args = self.blanked
|
|
101
|
+
# for key, record_set in records
|
|
102
|
+
# sheet = self.clone
|
|
103
|
+
# sheet.records =
|
|
104
|
+
#
|
|
105
|
+
# records[key] = sheet
|
|
106
|
+
# end
|
|
107
|
+
#
|
|
108
|
+
# records
|
|
206
109
|
end
|
|
207
110
|
|
|
111
|
+
|
|
208
112
|
# if blank is defined, only the records which are non-blank in that
|
|
209
113
|
# column will actually be yielded. The rest will be set to the provided
|
|
210
114
|
# default
|
|
@@ -214,91 +118,6 @@ module CsvMadness
|
|
|
214
118
|
end
|
|
215
119
|
end
|
|
216
120
|
|
|
217
|
-
def alter_column( column, blank = :undefined, &block )
|
|
218
|
-
raise "Column does not exist: #{column}" unless @columns.include?( column )
|
|
219
|
-
|
|
220
|
-
if cindex = @columns.index( column )
|
|
221
|
-
for record in @records
|
|
222
|
-
if record.blank?(column) && blank != :undefined
|
|
223
|
-
record[cindex] = blank
|
|
224
|
-
else
|
|
225
|
-
record[cindex] = yield( record[cindex], record )
|
|
226
|
-
end
|
|
227
|
-
end
|
|
228
|
-
end
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
# If no block given, adds an empty column
|
|
232
|
-
def add_column( column, &block )
|
|
233
|
-
raise "Column already exists: #{column}" if @columns.include?( column )
|
|
234
|
-
@columns << column
|
|
235
|
-
|
|
236
|
-
# add empty column to each row
|
|
237
|
-
@records.map{ |r|
|
|
238
|
-
r.csv_data << {column => ""}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
update_data_accessor_module
|
|
242
|
-
|
|
243
|
-
if block_given?
|
|
244
|
-
alter_column( column ) do |val, record|
|
|
245
|
-
yield val, record
|
|
246
|
-
end
|
|
247
|
-
end
|
|
248
|
-
end
|
|
249
|
-
|
|
250
|
-
def drop_column( column )
|
|
251
|
-
raise "Column does not exist" unless @columns.include?( column )
|
|
252
|
-
|
|
253
|
-
@columns.delete( column )
|
|
254
|
-
|
|
255
|
-
key = column.to_s
|
|
256
|
-
|
|
257
|
-
@records.map{ |r|
|
|
258
|
-
r.csv_data.delete( key )
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
update_data_accessor_module
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
def rename_column( column, new_name )
|
|
265
|
-
@columns[@columns.index(column)] = new_name
|
|
266
|
-
rename_index_column( column, new_name ) if @index_columns.include?( column )
|
|
267
|
-
update_data_accessor_module
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
def set_column_type( column, type, blank = :undefined )
|
|
271
|
-
alter_column( column, blank, &COLUMN_TYPES[type] )
|
|
272
|
-
end
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
# If :reverse_merge is true, then the dest column is only overwritten for records where :dest is blank
|
|
276
|
-
def merge_columns( source, dest, opts = {} )
|
|
277
|
-
opts = { :drop_source => true, :reverse_merge => false, :default => "" }.merge( opts )
|
|
278
|
-
column_must_exist( source, dest )
|
|
279
|
-
|
|
280
|
-
self.records.each do |record|
|
|
281
|
-
if opts[:reverse_merge] == false || record.blank?( dest )
|
|
282
|
-
record[dest] = record.blank?(source) ? opts[:default] : record[source]
|
|
283
|
-
end
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
self.drop_column( source ) if opts[:drop_source]
|
|
287
|
-
end
|
|
288
|
-
|
|
289
|
-
# By default, the
|
|
290
|
-
def concat_columns( col1, col2, opts = {} )
|
|
291
|
-
opts = {:separator => '', :out => col1}.merge( opts )
|
|
292
|
-
|
|
293
|
-
column_must_exist( col1, col2 )
|
|
294
|
-
self.add_column( opts[:out] ) unless self.columns.include?( opts[:out] )
|
|
295
|
-
|
|
296
|
-
for record in self.records
|
|
297
|
-
record[ opts[:out] ] = "#{record[col1]}#{opts[:separator]}#{record[col2]}"
|
|
298
|
-
end
|
|
299
|
-
end
|
|
300
|
-
|
|
301
|
-
alias :concatenate :concat_columns
|
|
302
121
|
|
|
303
122
|
# Note: If a block is given, the mod arg will be ignored.
|
|
304
123
|
def add_record_methods( mod = nil, &block )
|
|
@@ -316,36 +135,20 @@ module CsvMadness
|
|
|
316
135
|
end
|
|
317
136
|
end
|
|
318
137
|
|
|
138
|
+
def length
|
|
139
|
+
self.records.length
|
|
140
|
+
end
|
|
141
|
+
|
|
319
142
|
protected
|
|
143
|
+
attr_writer :columns, :index_columns, :records, :record_class, :opts
|
|
144
|
+
|
|
320
145
|
def load_csv
|
|
146
|
+
|
|
321
147
|
# encoding seems to solve a specific problem with a specific spreadsheet, at an unknown cost.
|
|
322
148
|
@csv = CSV.new( File.read(@spreadsheet_file).force_encoding("ISO-8859-1").encode("UTF-8"),
|
|
323
149
|
{ write_headers: true,
|
|
324
150
|
headers: ( @opts[:header] ? :first_row : false ) } )
|
|
325
151
|
end
|
|
326
|
-
|
|
327
|
-
def add_to_index( col, key, record )
|
|
328
|
-
@indexes[col][key] = record
|
|
329
|
-
end
|
|
330
|
-
|
|
331
|
-
# Reindexes the record lookup tables.
|
|
332
|
-
def reindex
|
|
333
|
-
@indexes = {}
|
|
334
|
-
for col in @index_columns
|
|
335
|
-
@indexes[col] = {}
|
|
336
|
-
|
|
337
|
-
for record in @records
|
|
338
|
-
add_to_index( col, record.send(col), record )
|
|
339
|
-
end
|
|
340
|
-
end
|
|
341
|
-
end
|
|
342
|
-
|
|
343
|
-
# shouldn't require reindex
|
|
344
|
-
def rename_index_column( column, new_name )
|
|
345
|
-
@index_columns[ @index_columns.index( column ) ] = new_name
|
|
346
|
-
@indexes[new_name] = @indexes[column]
|
|
347
|
-
@indexes.delete(column)
|
|
348
|
-
end
|
|
349
152
|
|
|
350
153
|
# Each spreadsheet has its own anonymous record class, and each CSV row instantiates
|
|
351
154
|
# a record of this class. This is where the getters and setters come from.
|
|
@@ -381,18 +184,22 @@ module CsvMadness
|
|
|
381
184
|
# prints a warning and a comparison of the columns to the headers.
|
|
382
185
|
def set_initial_columns( columns = nil )
|
|
383
186
|
if columns.nil?
|
|
384
|
-
if @opts[:header] == false
|
|
385
|
-
|
|
187
|
+
if @opts[:header] == false
|
|
188
|
+
columns = (0...csv_column_count).map{ |i| :"col#{i}" }
|
|
386
189
|
else
|
|
387
|
-
|
|
190
|
+
columns = fetch_csv_headers.map{ |name| self.class.getter_name( name ) }
|
|
388
191
|
end
|
|
389
192
|
else
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
193
|
+
unless !@csv || columns.length == csv_column_count
|
|
194
|
+
$stderr.puts "Warning <#{@spreadsheet_file}>: columns array does not match the number of columns in the spreadsheet."
|
|
195
|
+
@columns = columns
|
|
393
196
|
compare_columns_to_headers
|
|
394
197
|
end
|
|
395
198
|
end
|
|
199
|
+
|
|
200
|
+
raise_on_forbidden_column_names( columns )
|
|
201
|
+
|
|
202
|
+
@columns = columns
|
|
396
203
|
end
|
|
397
204
|
|
|
398
205
|
# Printout so the user can see which CSV columns are being matched to which
|
|
@@ -400,9 +207,10 @@ module CsvMadness
|
|
|
400
207
|
# column list.
|
|
401
208
|
def compare_columns_to_headers
|
|
402
209
|
headers = fetch_csv_headers
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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}"
|
|
406
214
|
end
|
|
407
215
|
end
|
|
408
216
|
|
|
@@ -410,8 +218,8 @@ module CsvMadness
|
|
|
410
218
|
# Create objects that respond to the recipe-named methods
|
|
411
219
|
def package
|
|
412
220
|
@records = []
|
|
413
|
-
@csv.each do |row|
|
|
414
|
-
@records << @record_class.new( row )
|
|
221
|
+
(@csv || []).each do |row|
|
|
222
|
+
@records << @record_class.new( row, self.column_accessors_map )
|
|
415
223
|
end
|
|
416
224
|
end
|
|
417
225
|
|
|
@@ -420,6 +228,8 @@ module CsvMadness
|
|
|
420
228
|
fetch_csv_headers.length
|
|
421
229
|
end
|
|
422
230
|
|
|
231
|
+
# returns a mapping based off the current ordered list of columns.
|
|
232
|
+
# A hash where the first column
|
|
423
233
|
def columns_to_mapping
|
|
424
234
|
@columns.each_with_index.inject({}){ |memo, item|
|
|
425
235
|
memo[item.first] = item.last
|
|
@@ -434,13 +244,12 @@ module CsvMadness
|
|
|
434
244
|
end
|
|
435
245
|
|
|
436
246
|
def update_data_accessor_module
|
|
247
|
+
debugger
|
|
437
248
|
@module.remap_accessors( columns_to_mapping )
|
|
438
249
|
end
|
|
439
|
-
|
|
440
|
-
def
|
|
441
|
-
|
|
442
|
-
raise ArgumentError.new( "#{caller[0]}: column :#{col} does not exist.") unless self.columns.include?(col)
|
|
443
|
-
end
|
|
250
|
+
|
|
251
|
+
def column_accessors_map
|
|
252
|
+
@module.column_accessors_map
|
|
444
253
|
end
|
|
445
254
|
end
|
|
446
255
|
end
|