csv_madness 0.0.12 → 0.0.14
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 +4 -4
- data/Gemfile +3 -2
- data/Rakefile +2 -65
- data/VERSION +1 -1
- data/lib/csv_madness/builder.rb +100 -20
- 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 -79
- 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 +67 -41
- 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 +161 -76
- data/lib/csv_madness/sheet_methods/class_methods.rb +15 -8
- data/lib/csv_madness/sheet_methods/column_methods.rb +95 -107
- data/lib/csv_madness/sheet_methods/file_methods.rb +11 -8
- data/lib/csv_madness/sheet_methods/indexing.rb +104 -23
- data/lib/csv_madness/sheet_methods/record_methods.rb +123 -47
- 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 -0
- data/test/csv/nil_headers.csv +5 -0
- data/test/csv/pokemon.csv +9 -0
- data/test/csv/test_column_types.csv +2 -2
- data/test/helper.rb +9 -6
- data/test/test_builder.rb +36 -0
- data/test/test_csv_madness.rb +28 -18
- data/test/test_index_set.rb +30 -0
- data/test/test_indexer.rb +50 -0
- data/test/test_reloading_spreadsheet.rb +4 -2
- data/test/test_sheet.rb +164 -27
- data/test/test_utils.rb +99 -0
- metadata +71 -3
data/lib/csv_madness/sheet.rb
CHANGED
|
@@ -2,15 +2,28 @@ module CsvMadness
|
|
|
2
2
|
class Sheet
|
|
3
3
|
extend SheetMethods::ClassMethods
|
|
4
4
|
include SheetMethods::Base
|
|
5
|
-
include SheetMethods::Indexing
|
|
5
|
+
# include SheetMethods::Indexing # TODO: May never re-implement
|
|
6
6
|
include SheetMethods::FileMethods
|
|
7
7
|
include SheetMethods::ColumnMethods
|
|
8
8
|
include SheetMethods::RecordMethods
|
|
9
|
+
include SheetMethods::SortingMethods
|
|
9
10
|
|
|
11
|
+
def indexing_enabled?
|
|
12
|
+
false
|
|
13
|
+
end
|
|
10
14
|
|
|
11
|
-
attr_reader :columns, :
|
|
15
|
+
attr_reader :columns, :column_mapping, :opts, :extra_methods
|
|
12
16
|
attr_accessor :spreadsheet_file
|
|
13
17
|
|
|
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
|
+
#
|
|
14
27
|
# opts:
|
|
15
28
|
# index: ( [:id, :id2 ] )
|
|
16
29
|
# columns you want mapped for quick
|
|
@@ -27,6 +40,11 @@ module CsvMadness
|
|
|
27
40
|
# anything else, we assume the csv file has a header row
|
|
28
41
|
def initialize( *args )
|
|
29
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
|
|
30
48
|
|
|
31
49
|
if args.last.is_a?(Hash)
|
|
32
50
|
@opts = args.pop
|
|
@@ -34,10 +52,12 @@ module CsvMadness
|
|
|
34
52
|
@opts = {}
|
|
35
53
|
end
|
|
36
54
|
|
|
55
|
+
@column_types = {}
|
|
56
|
+
|
|
37
57
|
firstarg = args.shift
|
|
38
58
|
|
|
39
59
|
case firstarg
|
|
40
|
-
when NilClass
|
|
60
|
+
when NilClass # Blank sheet, no affiliation with an existing csv file
|
|
41
61
|
@spreadsheet_file = nil
|
|
42
62
|
@opts[:columns] ||= []
|
|
43
63
|
when String, FunWith::Files::FilePath, Pathname
|
|
@@ -53,10 +73,48 @@ module CsvMadness
|
|
|
53
73
|
end
|
|
54
74
|
|
|
55
75
|
|
|
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 )
|
|
80
|
+
else
|
|
81
|
+
if method[-1] == '='
|
|
82
|
+
key = method[0..-2]
|
|
83
|
+
set_cell( record, key, args.first )
|
|
84
|
+
else
|
|
85
|
+
get_cell( record, method )
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def get_cell( record, key )
|
|
91
|
+
record = self[record] if record.is_a?( Integer )
|
|
56
92
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
100
|
+
|
|
101
|
+
raise ColumnError.new( "Column #{key} does not exist. Column mapping: #{self.column_mapping.inspect}" ) if index.nil?
|
|
102
|
+
|
|
103
|
+
record.data[ index ]
|
|
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 )
|
|
110
|
+
|
|
111
|
+
record.data[ index_of_column( key ) ] = val
|
|
112
|
+
end
|
|
113
|
+
|
|
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 )
|
|
60
118
|
end
|
|
61
119
|
end
|
|
62
120
|
|
|
@@ -73,30 +131,43 @@ module CsvMadness
|
|
|
73
131
|
def blanked()
|
|
74
132
|
sheet = self.class.new
|
|
75
133
|
sheet.columns = @columns.clone
|
|
76
|
-
sheet.
|
|
134
|
+
sheet.set_column_mapping
|
|
135
|
+
|
|
136
|
+
sheet.index_columns = @index_columns.clone if indexing_enabled?
|
|
137
|
+
|
|
77
138
|
sheet.records = []
|
|
78
139
|
sheet.spreadsheet_file = nil
|
|
79
|
-
sheet.create_data_accessor_module
|
|
80
|
-
sheet.create_record_class
|
|
140
|
+
# sheet.create_data_accessor_module
|
|
141
|
+
# sheet.create_record_class
|
|
81
142
|
sheet.opts = @opts.clone
|
|
82
|
-
sheet.reindex
|
|
143
|
+
sheet.reindex if indexing_enabled?
|
|
83
144
|
|
|
84
145
|
sheet
|
|
85
146
|
end
|
|
86
147
|
|
|
87
|
-
#
|
|
88
|
-
#
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
93
159
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
160
|
+
for record in @records
|
|
161
|
+
result_key = yield record
|
|
162
|
+
( sheets[result_key] ||= self.blanked() ) << record
|
|
163
|
+
end
|
|
98
164
|
|
|
99
|
-
|
|
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
|
|
100
171
|
# sheet_args = self.blanked
|
|
101
172
|
# for key, record_set in records
|
|
102
173
|
# sheet = self.clone
|
|
@@ -117,16 +188,6 @@ module CsvMadness
|
|
|
117
188
|
alter_column( column, blank, &block )
|
|
118
189
|
end
|
|
119
190
|
end
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
# Note: If a block is given, the mod arg will be ignored.
|
|
123
|
-
def add_record_methods( mod = nil, &block )
|
|
124
|
-
if block_given?
|
|
125
|
-
mod = Module.new( &block )
|
|
126
|
-
end
|
|
127
|
-
@record_class.send( :include, mod )
|
|
128
|
-
self
|
|
129
|
-
end
|
|
130
191
|
|
|
131
192
|
# Note: If implementation of Record[] changes, so must this.
|
|
132
193
|
def nils_are_blank_strings
|
|
@@ -140,37 +201,50 @@ module CsvMadness
|
|
|
140
201
|
end
|
|
141
202
|
|
|
142
203
|
protected
|
|
143
|
-
attr_writer :columns, :
|
|
204
|
+
attr_writer :columns, :records, :record_class, :opts
|
|
144
205
|
|
|
145
206
|
def load_csv
|
|
207
|
+
puts "Options: #{@opts.inspect}"
|
|
208
|
+
|
|
146
209
|
# encoding seems to solve a specific problem with a specific spreadsheet, at an unknown cost.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
# Each spreadsheet has its own anonymous record class, and each CSV row instantiates
|
|
153
|
-
# a record of this class. This is where the getters and setters come from.
|
|
154
|
-
def create_record_class
|
|
155
|
-
create_data_accessor_module
|
|
156
|
-
@record_class = Class.new( CsvMadness::Record )
|
|
157
|
-
@record_class.spreadsheet = self
|
|
158
|
-
@record_class.send( :include, @module )
|
|
210
|
+
data = File.read(@spreadsheet_file).force_encoding("ISO-8859-1").encode("UTF-8")
|
|
211
|
+
|
|
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
|
|
159
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
|
|
160
225
|
|
|
161
226
|
# fetch the original headers from the CSV file. If opts[:headers] is false,
|
|
162
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.
|
|
163
232
|
def fetch_csv_headers
|
|
164
233
|
if @csv && @opts[:header]
|
|
165
|
-
if @csv.headers
|
|
166
|
-
@csv.
|
|
167
|
-
|
|
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)
|
|
168
239
|
@csv.rewind # shift/rewind, else @csv.headers only returns 'true'
|
|
169
240
|
else
|
|
170
|
-
|
|
241
|
+
warn( "NO HEADERS in CSV" )
|
|
242
|
+
headers = @csv.headers || []
|
|
171
243
|
end
|
|
244
|
+
|
|
172
245
|
headers
|
|
173
246
|
else
|
|
247
|
+
warn( "THE ELSE HAPPENED")
|
|
174
248
|
[]
|
|
175
249
|
end
|
|
176
250
|
end
|
|
@@ -181,12 +255,15 @@ module CsvMadness
|
|
|
181
255
|
# otherwise, you end up with accessors like record.col1, record.col2, record.col3...
|
|
182
256
|
# If the columns given doesn't match the number of columns in the spreadsheet
|
|
183
257
|
# prints a warning and a comparison of the columns to the headers.
|
|
184
|
-
def
|
|
258
|
+
def set_columns( columns = nil )
|
|
185
259
|
if columns.nil?
|
|
186
260
|
if @opts[:header] == false
|
|
187
|
-
columns = (0...csv_column_count).map{ |i|
|
|
261
|
+
columns = (0...csv_column_count).map{ |i| self.class.default_column_name(i) }
|
|
188
262
|
else
|
|
189
|
-
|
|
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
|
|
190
267
|
end
|
|
191
268
|
else
|
|
192
269
|
unless !@csv || columns.length == csv_column_count
|
|
@@ -199,6 +276,14 @@ module CsvMadness
|
|
|
199
276
|
raise_on_forbidden_column_names( columns )
|
|
200
277
|
|
|
201
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
|
|
202
287
|
end
|
|
203
288
|
|
|
204
289
|
# Printout so the user can see which CSV columns are being matched to which
|
|
@@ -215,10 +300,10 @@ module CsvMadness
|
|
|
215
300
|
|
|
216
301
|
|
|
217
302
|
# Create objects that respond to the recipe-named methods
|
|
218
|
-
def
|
|
303
|
+
def package_each_csv_row
|
|
219
304
|
@records = []
|
|
220
305
|
(@csv || []).each do |row|
|
|
221
|
-
@records <<
|
|
306
|
+
@records << Record.new( row, self )
|
|
222
307
|
end
|
|
223
308
|
end
|
|
224
309
|
|
|
@@ -227,28 +312,28 @@ module CsvMadness
|
|
|
227
312
|
fetch_csv_headers.length
|
|
228
313
|
end
|
|
229
314
|
|
|
230
|
-
# returns a mapping based off the current ordered list of columns.
|
|
231
|
-
# A hash where the first column
|
|
232
|
-
def columns_to_mapping
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
def create_data_accessor_module
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
def update_data_accessor_module
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
end
|
|
249
|
-
|
|
250
|
-
def column_accessors_map
|
|
251
|
-
|
|
252
|
-
end
|
|
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
|
|
253
338
|
end
|
|
254
339
|
end
|
|
@@ -5,7 +5,9 @@ module CsvMadness
|
|
|
5
5
|
# " hello;: world! " => :hello_world
|
|
6
6
|
def getter_name( name )
|
|
7
7
|
# replace any run of non-word characters with a single "_"
|
|
8
|
-
|
|
8
|
+
return nil if name.nil?
|
|
9
|
+
|
|
10
|
+
name = name.downcase.to_s.gsub( /(\W|_|\s)+/, "_" )
|
|
9
11
|
|
|
10
12
|
# snip trailing and leading "_"
|
|
11
13
|
name = name.gsub( /(^_+|_+$)/, "" )
|
|
@@ -15,6 +17,10 @@ module CsvMadness
|
|
|
15
17
|
|
|
16
18
|
name.to_sym
|
|
17
19
|
end
|
|
20
|
+
|
|
21
|
+
def default_column_name( index )
|
|
22
|
+
"col#{index}".to_sym
|
|
23
|
+
end
|
|
18
24
|
|
|
19
25
|
|
|
20
26
|
# Paths to be searched when CsvMadness.load( "filename.csv" ) is called.
|
|
@@ -28,9 +34,7 @@ module CsvMadness
|
|
|
28
34
|
@search_paths << path unless @search_paths.include?( path )
|
|
29
35
|
end
|
|
30
36
|
|
|
31
|
-
|
|
32
|
-
@search_paths
|
|
33
|
-
end
|
|
37
|
+
attr_reader :search_paths
|
|
34
38
|
|
|
35
39
|
def from( csv_file, opts = {} )
|
|
36
40
|
if f = find_spreadsheet_in_filesystem( csv_file )
|
|
@@ -62,15 +66,18 @@ module CsvMadness
|
|
|
62
66
|
|
|
63
67
|
# opts are passed to underlying CSV (:row_sep, :encoding, :force_quotes)
|
|
64
68
|
def to_csv( spreadsheet, opts = {} )
|
|
65
|
-
out = spreadsheet.columns.to_csv
|
|
69
|
+
out = spreadsheet.columns.to_csv( opts )
|
|
66
70
|
spreadsheet.records.inject( out ) do |output, record|
|
|
67
|
-
output << record.to_csv
|
|
71
|
+
output << record.to_csv( opts )
|
|
68
72
|
end
|
|
69
73
|
end
|
|
70
74
|
|
|
71
|
-
def write_to_file( spreadsheet, file )
|
|
75
|
+
def write_to_file( spreadsheet, file, opts = {} )
|
|
76
|
+
opts = opts.clone
|
|
77
|
+
opts.delete( :columns ) # CSV library chokes on this option
|
|
78
|
+
|
|
72
79
|
file = file.fwf_filepath.expand_path
|
|
73
|
-
file.write( spreadsheet.to_csv )
|
|
80
|
+
file.write( spreadsheet.to_csv( opts ) )
|
|
74
81
|
end
|
|
75
82
|
end
|
|
76
83
|
end
|
|
@@ -1,67 +1,15 @@
|
|
|
1
1
|
module CsvMadness
|
|
2
2
|
module SheetMethods
|
|
3
|
-
module ColumnMethods
|
|
4
|
-
|
|
5
|
-
COLUMN_TYPES = {
|
|
6
|
-
number: Proc.new do |cell, record|
|
|
7
|
-
rval = cell
|
|
8
|
-
|
|
9
|
-
unless cell.nil? || (cell.is_a?(String) && cell.length == 0)
|
|
10
|
-
|
|
11
|
-
begin
|
|
12
|
-
rval = Integer(cell)
|
|
13
|
-
rescue
|
|
14
|
-
# do nothing
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
unless rval.is_a?(Integer)
|
|
18
|
-
begin
|
|
19
|
-
rval = Float(cell)
|
|
20
|
-
rescue
|
|
21
|
-
# do nothing
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
rval
|
|
27
|
-
end,
|
|
28
|
-
|
|
29
|
-
integer: Proc.new do |cell, record|
|
|
30
|
-
begin
|
|
31
|
-
Integer(cell)
|
|
32
|
-
rescue
|
|
33
|
-
cell
|
|
34
|
-
end
|
|
35
|
-
end,
|
|
36
|
-
|
|
37
|
-
float: Proc.new do |cell, record|
|
|
38
|
-
begin
|
|
39
|
-
Float(cell)
|
|
40
|
-
rescue
|
|
41
|
-
cell
|
|
42
|
-
end
|
|
43
|
-
end,
|
|
44
|
-
|
|
45
|
-
date: Proc.new do |cell, record|
|
|
46
|
-
begin
|
|
47
|
-
parse = Time.parse( cell || "" )
|
|
48
|
-
rescue ArgumentError
|
|
49
|
-
if cell =~ /^Invalid Time Format: /
|
|
50
|
-
parse = cell
|
|
51
|
-
else
|
|
52
|
-
parse = "Invalid Time Format: <#{cell}>"
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
parse
|
|
57
|
-
end
|
|
58
|
-
}
|
|
59
|
-
|
|
3
|
+
module ColumnMethods
|
|
60
4
|
FORBIDDEN_COLUMN_NAMES = [:to_s] # breaks things hard when you use them. Probably not comprehensive, sadly.
|
|
61
5
|
|
|
62
6
|
|
|
63
|
-
|
|
64
|
-
|
|
7
|
+
# can only be called with proper column names (not extra methods)
|
|
8
|
+
def column( col )
|
|
9
|
+
index = index_of_column( col )
|
|
10
|
+
|
|
11
|
+
# Should be faster...
|
|
12
|
+
@records.map(&:data).map{ |dat| dat[index] }
|
|
65
13
|
end
|
|
66
14
|
|
|
67
15
|
def has_column?( col )
|
|
@@ -71,71 +19,81 @@ module CsvMadness
|
|
|
71
19
|
# retrieve multiple columns. Returns an array of the form
|
|
72
20
|
# [ [record1:col1, record1:col2...], [record2:col1, record2:col2...] [...] ]
|
|
73
21
|
def multiple_columns(*args)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
end
|
|
22
|
+
column_results = args.map do |col|
|
|
23
|
+
index_of_column( col )
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
column_results.first.zip( *(column_results[1..-1] ) )
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def alter_column( col, blank = :undefined, &block )
|
|
30
|
+
cindex = index_of_column( col )
|
|
31
|
+
|
|
32
|
+
for record in @records
|
|
33
|
+
if record.blank?( col ) && blank != :undefined
|
|
34
|
+
record[cindex] = blank
|
|
35
|
+
else
|
|
36
|
+
record[cindex] = yield( record[cindex], record )
|
|
90
37
|
end
|
|
91
38
|
end
|
|
92
39
|
end
|
|
93
40
|
|
|
94
|
-
# If no block given, adds an empty column.
|
|
41
|
+
# If no block given, adds an empty column (blank string).
|
|
95
42
|
# If block given, fills the new column cells when the block is run on each record
|
|
96
43
|
# TODO: Should be able to specify the index of the column
|
|
97
|
-
def add_column(
|
|
98
|
-
raise_on_forbidden_column_names(
|
|
99
|
-
|
|
44
|
+
def add_column( col, &block )
|
|
45
|
+
raise_on_forbidden_column_names( col )
|
|
46
|
+
raise_on_columns_present( col )
|
|
100
47
|
|
|
101
|
-
@columns <<
|
|
48
|
+
@columns << col
|
|
102
49
|
|
|
103
50
|
# add empty column to each row
|
|
104
|
-
@records.map{ |r|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
update_data_accessor_module
|
|
109
|
-
|
|
110
|
-
if block_given?
|
|
111
|
-
alter_column( column ) do |val, record|
|
|
112
|
-
yield val, record
|
|
113
|
-
end
|
|
114
|
-
end
|
|
51
|
+
@records.map{ |r| r.data << "" }
|
|
52
|
+
|
|
53
|
+
set_column_mapping
|
|
54
|
+
alter_column( col, &block ) if block_given?
|
|
115
55
|
end
|
|
116
56
|
|
|
117
|
-
def drop_column( column )
|
|
118
|
-
raise_on_missing_columns( column )
|
|
119
|
-
|
|
120
|
-
@columns.delete( column )
|
|
121
57
|
|
|
122
|
-
|
|
58
|
+
def drop_column( col )
|
|
59
|
+
index = index_of_column( col )
|
|
60
|
+
@columns.delete( col.to_sym )
|
|
123
61
|
|
|
124
|
-
@records.map
|
|
125
|
-
|
|
126
|
-
|
|
62
|
+
@records.map(&:data).map do |dat|
|
|
63
|
+
dat.delete_at( index )
|
|
64
|
+
end
|
|
127
65
|
|
|
128
|
-
|
|
66
|
+
remove_column_from_mapping( col )
|
|
129
67
|
end
|
|
130
68
|
|
|
131
|
-
def rename_column(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
69
|
+
def rename_column( old_column_name, new_name )
|
|
70
|
+
raise_on_missing_columns( old_column_name )
|
|
71
|
+
raise_on_forbidden_column_names( new_name )
|
|
72
|
+
|
|
73
|
+
# - rename the column itself
|
|
74
|
+
# - rename the column in the index
|
|
75
|
+
# - rename the column in the column mapping
|
|
76
|
+
column_index = index_of_column( old_column_name )
|
|
77
|
+
|
|
78
|
+
@columns[column_index] = new_name
|
|
79
|
+
|
|
80
|
+
if indexing_enabled? && has_index_column?( old_column_name )
|
|
81
|
+
rename_index_column( column, new_name )
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
rename_column_mapping_key( old_column_name, new_name )
|
|
135
85
|
end
|
|
136
|
-
|
|
86
|
+
|
|
137
87
|
def set_column_type( column, type, blank = :undefined )
|
|
138
|
-
|
|
88
|
+
type = ColumnType::COLUMN_TYPE_LOOKUP[type]
|
|
89
|
+
|
|
90
|
+
alter_column( column, blank ) do |cell|
|
|
91
|
+
begin
|
|
92
|
+
type.convert( cell )
|
|
93
|
+
rescue StandardError => e
|
|
94
|
+
"ERROR: #{cell} (#{e.message})"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
139
97
|
end
|
|
140
98
|
|
|
141
99
|
|
|
@@ -167,11 +125,41 @@ module CsvMadness
|
|
|
167
125
|
|
|
168
126
|
alias :concatenate :concat_columns
|
|
169
127
|
|
|
128
|
+
def index_of_column( col )
|
|
129
|
+
case col
|
|
130
|
+
when Integer
|
|
131
|
+
col
|
|
132
|
+
when Symbol, String
|
|
133
|
+
raise_on_missing_columns( col )
|
|
134
|
+
@column_mapping[ col.to_sym ]
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
170
138
|
protected
|
|
139
|
+
def rename_column_mapping_key( old_column_name, new_name )
|
|
140
|
+
@column_mapping[new_name] = @column_mapping.delete( old_column_name )
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def remove_column_from_mapping( col )
|
|
144
|
+
index = @column_mapping.delete( col.to_sym )
|
|
145
|
+
|
|
146
|
+
@column_mapping.each do |_col, i|
|
|
147
|
+
@column_mapping[_col] = i - 1 if i > index
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
|
|
171
153
|
def raise_on_missing_columns( *cols )
|
|
172
154
|
missing_columns = cols.flatten.reject{ |col| self.has_column?( col ) }
|
|
173
155
|
|
|
174
|
-
raise MissingColumnError.new( "#{caller[0]}:
|
|
156
|
+
raise MissingColumnError.new( "#{caller[0]}: requested column(s) :#{missing_columns.inspect} not found." ) unless missing_columns.fwf_blank?
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def raise_on_columns_present( *cols )
|
|
160
|
+
present_columns = cols.flatten.select{ |col| self.has_column?( col ) }
|
|
161
|
+
|
|
162
|
+
raise ColumnExistsError.new( "#{caller[0]}: requested column(s) already present: #{present_columns.inspect}" ) unless present_columns.fwf_blank?
|
|
175
163
|
end
|
|
176
164
|
|
|
177
165
|
def forbidden_column_name?( col )
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
module CsvMadness
|
|
2
2
|
module SheetMethods
|
|
3
3
|
module FileMethods
|
|
4
|
-
def write_to_file( file )
|
|
5
|
-
self.class.write_to_file( self, file )
|
|
4
|
+
def write_to_file( file, opts = {} )
|
|
5
|
+
self.class.write_to_file( self, file, opts )
|
|
6
6
|
end
|
|
7
7
|
|
|
8
8
|
# pass an arbitrary filepath to save sheet to that filepath
|
|
@@ -20,7 +20,9 @@ module CsvMadness
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
if file
|
|
23
|
-
|
|
23
|
+
opts = (@opts || {}).clone
|
|
24
|
+
opts.delete(:header)
|
|
25
|
+
self.write_to_file( file, opts)
|
|
24
26
|
else
|
|
25
27
|
raise "CsvMadness.save(:timestamp) - Spreadsheet must be specified by spreadsheet_file"
|
|
26
28
|
end
|
|
@@ -29,12 +31,13 @@ module CsvMadness
|
|
|
29
31
|
|
|
30
32
|
def reload_spreadsheet( opts = @opts )
|
|
31
33
|
load_csv if @spreadsheet_file
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
package
|
|
34
|
+
set_columns( opts[:columns] )
|
|
35
|
+
package_each_csv_row
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
if indexing_enabled?
|
|
38
|
+
set_index_columns( opts[:index] )
|
|
39
|
+
reindex
|
|
40
|
+
end
|
|
38
41
|
end
|
|
39
42
|
end
|
|
40
43
|
end
|