csv_madness 0.0.9 → 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 +4 -4
- data/.document +5 -0
- data/CHANGELOG.markdown +4 -0
- data/Gemfile +2 -7
- data/Rakefile +12 -78
- data/VERSION +1 -1
- data/lib/csv_madness/builder.rb +91 -9
- 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 +68 -42
- 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 +160 -76
- data/lib/csv_madness/sheet_methods/class_methods.rb +11 -4
- data/lib/csv_madness/sheet_methods/column_methods.rb +95 -107
- data/lib/csv_madness/sheet_methods/file_methods.rb +9 -6
- 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 +1 -0
- 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 +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 +66 -38
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 )
|
|
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
|
|
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 )
|
|
56
110
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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,38 +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}"
|
|
146
208
|
|
|
147
209
|
# encoding seems to solve a specific problem with a specific spreadsheet, at an unknown cost.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
# Each spreadsheet has its own anonymous record class, and each CSV row instantiates
|
|
154
|
-
# a record of this class. This is where the getters and setters come from.
|
|
155
|
-
def create_record_class
|
|
156
|
-
create_data_accessor_module
|
|
157
|
-
@record_class = Class.new( CsvMadness::Record )
|
|
158
|
-
@record_class.spreadsheet = self
|
|
159
|
-
@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
|
|
160
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
|
|
161
225
|
|
|
162
226
|
# fetch the original headers from the CSV file. If opts[:headers] is false,
|
|
163
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.
|
|
164
232
|
def fetch_csv_headers
|
|
165
233
|
if @csv && @opts[:header]
|
|
166
|
-
if @csv.headers
|
|
167
|
-
@csv.
|
|
168
|
-
|
|
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)
|
|
169
239
|
@csv.rewind # shift/rewind, else @csv.headers only returns 'true'
|
|
170
240
|
else
|
|
171
|
-
|
|
241
|
+
warn( "NO HEADERS in CSV" )
|
|
242
|
+
headers = @csv.headers || []
|
|
172
243
|
end
|
|
244
|
+
|
|
173
245
|
headers
|
|
174
246
|
else
|
|
247
|
+
warn( "THE ELSE HAPPENED")
|
|
175
248
|
[]
|
|
176
249
|
end
|
|
177
250
|
end
|
|
@@ -182,12 +255,15 @@ module CsvMadness
|
|
|
182
255
|
# otherwise, you end up with accessors like record.col1, record.col2, record.col3...
|
|
183
256
|
# If the columns given doesn't match the number of columns in the spreadsheet
|
|
184
257
|
# prints a warning and a comparison of the columns to the headers.
|
|
185
|
-
def
|
|
258
|
+
def set_columns( columns = nil )
|
|
186
259
|
if columns.nil?
|
|
187
260
|
if @opts[:header] == false
|
|
188
|
-
columns = (0...csv_column_count).map{ |i|
|
|
261
|
+
columns = (0...csv_column_count).map{ |i| self.class.default_column_name(i) }
|
|
189
262
|
else
|
|
190
|
-
|
|
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
|
|
191
267
|
end
|
|
192
268
|
else
|
|
193
269
|
unless !@csv || columns.length == csv_column_count
|
|
@@ -200,6 +276,14 @@ module CsvMadness
|
|
|
200
276
|
raise_on_forbidden_column_names( columns )
|
|
201
277
|
|
|
202
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
|
|
203
287
|
end
|
|
204
288
|
|
|
205
289
|
# Printout so the user can see which CSV columns are being matched to which
|
|
@@ -216,10 +300,10 @@ module CsvMadness
|
|
|
216
300
|
|
|
217
301
|
|
|
218
302
|
# Create objects that respond to the recipe-named methods
|
|
219
|
-
def
|
|
303
|
+
def package_each_csv_row
|
|
220
304
|
@records = []
|
|
221
305
|
(@csv || []).each do |row|
|
|
222
|
-
@records <<
|
|
306
|
+
@records << Record.new( row, self )
|
|
223
307
|
end
|
|
224
308
|
end
|
|
225
309
|
|
|
@@ -228,28 +312,28 @@ module CsvMadness
|
|
|
228
312
|
fetch_csv_headers.length
|
|
229
313
|
end
|
|
230
314
|
|
|
231
|
-
# returns a mapping based off the current ordered list of columns.
|
|
232
|
-
# A hash where the first column
|
|
233
|
-
def columns_to_mapping
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
def create_data_accessor_module
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
def update_data_accessor_module
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
def column_accessors_map
|
|
252
|
-
|
|
253
|
-
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
|
|
254
338
|
end
|
|
255
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 )
|
|
@@ -69,6 +73,9 @@ module CsvMadness
|
|
|
69
73
|
end
|
|
70
74
|
|
|
71
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
80
|
file.write( spreadsheet.to_csv( opts ) )
|
|
74
81
|
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 )
|
|
@@ -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
|