csv_madness 0.0.6 → 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 +5 -13
- data/.document +5 -0
- data/CHANGELOG.markdown +4 -0
- data/Gemfile +2 -8
- data/README.rdoc +55 -47
- data/Rakefile +10 -49
- data/VERSION +1 -1
- data/lib/csv_madness/builder.rb +95 -10
- 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 -44
- data/lib/csv_madness/exceptions.rb +16 -0
- 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 -40
- 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 +172 -442
- data/lib/csv_madness/sheet_methods/base.rb +6 -0
- data/lib/csv_madness/sheet_methods/class_methods.rb +84 -0
- data/lib/csv_madness/sheet_methods/column_methods.rb +175 -0
- data/lib/csv_madness/sheet_methods/file_methods.rb +44 -0
- data/lib/csv_madness/sheet_methods/indexing.rb +133 -0
- data/lib/csv_madness/sheet_methods/record_methods.rb +175 -0
- 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 -2
- 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 +13 -5
- data/test/test_builder.rb +43 -3
- data/test/test_csv_madness.rb +34 -25
- data/test/test_index_set.rb +30 -0
- data/test/test_indexer.rb +50 -0
- data/test/test_reloading_spreadsheet.rb +5 -2
- data/test/test_sheet.rb +184 -37
- data/test/test_utils.rb +99 -0
- metadata +82 -36
|
@@ -0,0 +1,84 @@
|
|
|
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
|
+
return nil if name.nil?
|
|
9
|
+
|
|
10
|
+
name = name.downcase.to_s.gsub( /(\W|_|\s)+/, "_" )
|
|
11
|
+
|
|
12
|
+
# snip trailing and leading "_"
|
|
13
|
+
name = name.gsub( /(^_+|_+$)/, "" )
|
|
14
|
+
|
|
15
|
+
# add leading "_" when we'd otherwise have a method name starting with a digit
|
|
16
|
+
name = "_#{name}" if name.match( /^\d/ )
|
|
17
|
+
|
|
18
|
+
name.to_sym
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def default_column_name( index )
|
|
22
|
+
"col#{index}".to_sym
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Paths to be searched when CsvMadness.load( "filename.csv" ) is called.
|
|
27
|
+
def add_search_path( path )
|
|
28
|
+
@search_paths ||= []
|
|
29
|
+
path = Pathname.new( path ).expand_path
|
|
30
|
+
unless path.directory?
|
|
31
|
+
raise "The given path does not exist"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
@search_paths << path unless @search_paths.include?( path )
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
attr_reader :search_paths
|
|
38
|
+
|
|
39
|
+
def from( csv_file, opts = {} )
|
|
40
|
+
if f = find_spreadsheet_in_filesystem( csv_file )
|
|
41
|
+
Sheet.new( f, opts )
|
|
42
|
+
else
|
|
43
|
+
raise "File not found."
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Search absolute/relative-to-current-dir before checking
|
|
48
|
+
# search paths.
|
|
49
|
+
def find_spreadsheet_in_filesystem( name )
|
|
50
|
+
@search_paths ||= []
|
|
51
|
+
|
|
52
|
+
expanded_path = Pathname.new( name ).expand_path
|
|
53
|
+
if expanded_path.exist?
|
|
54
|
+
return expanded_path
|
|
55
|
+
else # look for it in the search paths
|
|
56
|
+
@search_paths.each do |p|
|
|
57
|
+
file = p.join( name )
|
|
58
|
+
if file.exist? && file.file?
|
|
59
|
+
return p.join( name )
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# opts are passed to underlying CSV (:row_sep, :encoding, :force_quotes)
|
|
68
|
+
def to_csv( spreadsheet, opts = {} )
|
|
69
|
+
out = spreadsheet.columns.to_csv( opts )
|
|
70
|
+
spreadsheet.records.inject( out ) do |output, record|
|
|
71
|
+
output << record.to_csv( opts )
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def write_to_file( spreadsheet, file, opts = {} )
|
|
76
|
+
opts = opts.clone
|
|
77
|
+
opts.delete( :columns ) # CSV library chokes on this option
|
|
78
|
+
|
|
79
|
+
file = file.fwf_filepath.expand_path
|
|
80
|
+
file.write( spreadsheet.to_csv( opts ) )
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
module CsvMadness
|
|
2
|
+
module SheetMethods
|
|
3
|
+
module ColumnMethods
|
|
4
|
+
FORBIDDEN_COLUMN_NAMES = [:to_s] # breaks things hard when you use them. Probably not comprehensive, sadly.
|
|
5
|
+
|
|
6
|
+
|
|
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] }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def has_column?( col )
|
|
16
|
+
self.columns.include?( col.to_sym )
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# retrieve multiple columns. Returns an array of the form
|
|
20
|
+
# [ [record1:col1, record1:col2...], [record2:col1, record2:col2...] [...] ]
|
|
21
|
+
def multiple_columns(*args)
|
|
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 )
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# If no block given, adds an empty column (blank string).
|
|
42
|
+
# If block given, fills the new column cells when the block is run on each record
|
|
43
|
+
# TODO: Should be able to specify the index of the column
|
|
44
|
+
def add_column( col, &block )
|
|
45
|
+
raise_on_forbidden_column_names( col )
|
|
46
|
+
raise_on_columns_present( col )
|
|
47
|
+
|
|
48
|
+
@columns << col
|
|
49
|
+
|
|
50
|
+
# add empty column to each row
|
|
51
|
+
@records.map{ |r| r.data << "" }
|
|
52
|
+
|
|
53
|
+
set_column_mapping
|
|
54
|
+
alter_column( col, &block ) if block_given?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def drop_column( col )
|
|
59
|
+
index = index_of_column( col )
|
|
60
|
+
@columns.delete( col.to_sym )
|
|
61
|
+
|
|
62
|
+
@records.map(&:data).map do |dat|
|
|
63
|
+
dat.delete_at( index )
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
remove_column_from_mapping( col )
|
|
67
|
+
end
|
|
68
|
+
|
|
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 )
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def set_column_type( column, type, blank = :undefined )
|
|
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
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# If :reverse_merge is true, then the dest column is only overwritten for records where :dest is blank
|
|
101
|
+
def merge_columns( source, dest, opts = {} )
|
|
102
|
+
opts = { :drop_source => true, :reverse_merge => false, :default => "" }.merge( opts )
|
|
103
|
+
raise_on_missing_columns( source, dest )
|
|
104
|
+
|
|
105
|
+
self.records.each do |record|
|
|
106
|
+
if opts[:reverse_merge] == false || record.blank?( dest )
|
|
107
|
+
record[dest] = record.blank?(source) ? opts[:default] : record[source]
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
self.drop_column( source ) if opts[:drop_source]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# By default, the
|
|
115
|
+
def concat_columns( col1, col2, opts = {} )
|
|
116
|
+
opts = {:separator => '', :out => col1}.merge( opts )
|
|
117
|
+
|
|
118
|
+
raise_on_missing_columns( col1, col2 )
|
|
119
|
+
self.add_column( opts[:out] ) unless self.columns.include?( opts[:out] )
|
|
120
|
+
|
|
121
|
+
for record in self.records
|
|
122
|
+
record[ opts[:out] ] = "#{record[col1]}#{opts[:separator]}#{record[col2]}"
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
alias :concatenate :concat_columns
|
|
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
|
+
|
|
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
|
+
|
|
153
|
+
def raise_on_missing_columns( *cols )
|
|
154
|
+
missing_columns = cols.flatten.reject{ |col| self.has_column?( col ) }
|
|
155
|
+
|
|
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?
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def forbidden_column_name?( col )
|
|
166
|
+
FORBIDDEN_COLUMN_NAMES.include?( col )
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def raise_on_forbidden_column_names( *cols )
|
|
170
|
+
forbidden_columns = cols.flatten.select{ |col| self.forbidden_column_name?( col ) }
|
|
171
|
+
raise ForbiddenColumnNameError.new( "forbidden names: #{forbidden_columns.inspect}" ) unless forbidden_columns.fwf_blank?
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module CsvMadness
|
|
2
|
+
module SheetMethods
|
|
3
|
+
module FileMethods
|
|
4
|
+
def write_to_file( file, opts = {} )
|
|
5
|
+
self.class.write_to_file( self, file, opts )
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
# pass an arbitrary filepath to save sheet to that filepath
|
|
9
|
+
# call save() with no arguments to overwrite the current spreadsheet file
|
|
10
|
+
# call save(:timestamp) to make a timestamped copy of the spreadsheet file
|
|
11
|
+
#
|
|
12
|
+
# Obviously, the last two require self.spreadsheet_file to exist and be writable
|
|
13
|
+
def save( file = self.spreadsheet_file )
|
|
14
|
+
if file == :timestamp
|
|
15
|
+
if self.spreadsheet_file
|
|
16
|
+
file = self.spreadsheet_file.fwf_filepath.timestamp
|
|
17
|
+
else
|
|
18
|
+
raise ArgumentError.new( "CsvMadness.save(:timestamp) - Spreadsheet must be specified by @spreadsheet_file" )
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
if file
|
|
23
|
+
opts = (@opts || {}).clone
|
|
24
|
+
opts.delete(:header)
|
|
25
|
+
self.write_to_file( file, opts)
|
|
26
|
+
else
|
|
27
|
+
raise "CsvMadness.save(:timestamp) - Spreadsheet must be specified by spreadsheet_file"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def reload_spreadsheet( opts = @opts )
|
|
33
|
+
load_csv if @spreadsheet_file
|
|
34
|
+
set_columns( opts[:columns] )
|
|
35
|
+
package_each_csv_row
|
|
36
|
+
|
|
37
|
+
if indexing_enabled?
|
|
38
|
+
set_index_columns( opts[:index] )
|
|
39
|
+
reindex
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
module CsvMadness
|
|
2
|
+
module SheetMethods
|
|
3
|
+
module Indexing
|
|
4
|
+
attr_accessor :index_columns
|
|
5
|
+
|
|
6
|
+
def set_index_columns( index_columns )
|
|
7
|
+
cols = case index_columns
|
|
8
|
+
when NilClass
|
|
9
|
+
[]
|
|
10
|
+
when Symbol
|
|
11
|
+
[ index_columns ]
|
|
12
|
+
when Array
|
|
13
|
+
index_columns.map{|col| add_index_column( col ) }
|
|
14
|
+
end
|
|
15
|
+
for col in cols.map(&:to_sym)
|
|
16
|
+
remove_index_column( col ) if self.has_index_column?( col )
|
|
17
|
+
add_index_column( col )
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def add_index_column( col )
|
|
22
|
+
raise_if_index_columns( col )
|
|
23
|
+
|
|
24
|
+
col = col.to_sym
|
|
25
|
+
|
|
26
|
+
@index_columns << col
|
|
27
|
+
@indexes[col] = {}
|
|
28
|
+
|
|
29
|
+
reindex( col )
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# TODO: Suddenly seeing a huge problem with the indexes.
|
|
33
|
+
# What if the index values aren't unique?
|
|
34
|
+
# Add a single record to a single index. Index value calculated by caller.
|
|
35
|
+
def add_to_index( col, index_value, record )
|
|
36
|
+
raise_unless_index_columns( col )
|
|
37
|
+
|
|
38
|
+
remove_from_index( col, record ) # first remove in case it's been indexed previously with a different value... gah, this is getting ugly.
|
|
39
|
+
|
|
40
|
+
index_hash = (@indexes[col.to_sym] ||= {})
|
|
41
|
+
index_hash_records = (index_hash[index_value] ||= Set.new)
|
|
42
|
+
|
|
43
|
+
index_hash_records << record
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def unindex_record( record )
|
|
47
|
+
for index_col in @index_columns
|
|
48
|
+
remove_from_index( index_col, record )
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def remove_from_index( col, record )
|
|
53
|
+
raise_unless_index_columns( col )
|
|
54
|
+
|
|
55
|
+
@indexes[col.to_sym].each do |index_value, indexed_records|
|
|
56
|
+
indexed_records.delete( record )
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def index_records( records )
|
|
64
|
+
if records.is_a?( Array )
|
|
65
|
+
for record in records
|
|
66
|
+
index_records( record )
|
|
67
|
+
end
|
|
68
|
+
else
|
|
69
|
+
record = records
|
|
70
|
+
for col in @index_columns
|
|
71
|
+
add_to_index( col, record.send(col), record )
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def has_index_column?( col )
|
|
77
|
+
@index_columns.include?( col.to_sym )
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Reindexes the record lookup tables.
|
|
81
|
+
def reindex( *cols )
|
|
82
|
+
# for column in (columns )
|
|
83
|
+
#
|
|
84
|
+
# @indexes = {}
|
|
85
|
+
# index_records( @records )
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def empty_indexes( *cols )
|
|
89
|
+
raise_unless_index_columns( cols )
|
|
90
|
+
|
|
91
|
+
icols = @index_columns if cols.fwf_blank?
|
|
92
|
+
|
|
93
|
+
for icol in icols
|
|
94
|
+
@indexes[icol.to_sym] = {}
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# shouldn't require reindex
|
|
99
|
+
def rename_index_column( old_name, new_name )
|
|
100
|
+
@index_columns[ @index_columns.index( old_name ) ] = new_name
|
|
101
|
+
@indexes[new_name] = @indexes.delete( old_name )
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def remove_index_column( column_name )
|
|
105
|
+
raise_unless_index_columns( column_name )
|
|
106
|
+
|
|
107
|
+
@indexes.delete( column_name.sym )
|
|
108
|
+
@index_columns.delete( column_name.sym )
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
protected
|
|
112
|
+
# def each_index_column( cols = @index_columns, &block )
|
|
113
|
+
# raise_unless_index_columns( cols )
|
|
114
|
+
#
|
|
115
|
+
# for col in cols
|
|
116
|
+
# yield col.to_sym
|
|
117
|
+
# end
|
|
118
|
+
# end
|
|
119
|
+
|
|
120
|
+
def raise_unless_index_columns( *cols )
|
|
121
|
+
absent_columns = cols.reject{ |col| self.has_index_column?( col ) }
|
|
122
|
+
|
|
123
|
+
raise MissingColumnError.new( "Not an index column(s): #{absent_columns.inspect}" ) unless absent_columns.fwf_blank?
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def raise_if_index_columns( *cols )
|
|
127
|
+
present_columns = cols.detect{ |col| self.has_index_column?( col ) }
|
|
128
|
+
|
|
129
|
+
raise MissingColumnError.new( "Index column(s) already exist: #{present_columns.inspect}" ) unless present_columns.fwf_blank?
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
module CsvMadness
|
|
2
|
+
module SheetMethods
|
|
3
|
+
module RecordMethods
|
|
4
|
+
def add_record( record, &block )
|
|
5
|
+
case record
|
|
6
|
+
when Array
|
|
7
|
+
# do nothing
|
|
8
|
+
when Hash
|
|
9
|
+
record = @column_mapping.inject( [] ) do |memo, col_and_index|
|
|
10
|
+
col, index = col_and_index
|
|
11
|
+
|
|
12
|
+
if record.has_key?( col )
|
|
13
|
+
val = record[col]
|
|
14
|
+
elsif record.has_key?( col.to_s )
|
|
15
|
+
val = record[col.to_s]
|
|
16
|
+
else
|
|
17
|
+
val = nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
if index >= memo.length
|
|
21
|
+
memo.insert( index, val )
|
|
22
|
+
else
|
|
23
|
+
memo[index] = val
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
memo
|
|
27
|
+
end
|
|
28
|
+
when CSV::Row
|
|
29
|
+
record = record.to_a.map( &:last )
|
|
30
|
+
when CsvMadness::Record
|
|
31
|
+
# TODO: But what if the two records aren't from the same kinds of sheet
|
|
32
|
+
# IOW, different columns or columns in different orders, etc.
|
|
33
|
+
record = record.data
|
|
34
|
+
else
|
|
35
|
+
raise "sheet.add_record() doesn't take objects of type #{record.inspect}" unless record.respond_to?(:csv_data)
|
|
36
|
+
record = record.csv_data
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
record = Record.new( record, self )
|
|
40
|
+
|
|
41
|
+
@records << record
|
|
42
|
+
|
|
43
|
+
yield record if block_given?
|
|
44
|
+
|
|
45
|
+
index_records( record ) if indexing_enabled?
|
|
46
|
+
|
|
47
|
+
record
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
alias :<< :add_record
|
|
51
|
+
|
|
52
|
+
def add_blank_record( &block )
|
|
53
|
+
self.add_record( [], &block )
|
|
54
|
+
#
|
|
55
|
+
# if block_given?
|
|
56
|
+
# yield self.records.last
|
|
57
|
+
# else
|
|
58
|
+
# self.records.last
|
|
59
|
+
# end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# record can be the row number (integer from 0...@records.length)
|
|
63
|
+
# record can be the record itself
|
|
64
|
+
#
|
|
65
|
+
# returns the record that was removed.
|
|
66
|
+
def remove_record( record )
|
|
67
|
+
if record.is_a?(Integer)
|
|
68
|
+
record = remove_record_at_index( record )
|
|
69
|
+
else
|
|
70
|
+
record = self.records.delete( record )
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
self.unindex_record( record ) if indexing_enabled?
|
|
74
|
+
record
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def remove_record_at_index( index )
|
|
78
|
+
self.records.delete_at( index )
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Here's the deal: you hand it an array of records (or indexes, or a mix of the two),
|
|
82
|
+
# and it will remove the records (or the index... may want to test the indexing thing).
|
|
83
|
+
# If you give it a block, and it'll remove all the records for which
|
|
84
|
+
# the block yields _true_.
|
|
85
|
+
#
|
|
86
|
+
# returns an array of the removed records, in case you want them for anything.
|
|
87
|
+
def remove_records( records = nil, &block )
|
|
88
|
+
[].tap do |removed|
|
|
89
|
+
if block_given?
|
|
90
|
+
for record in @records
|
|
91
|
+
removed << self.remove_record( record ) if yield( record ) == true
|
|
92
|
+
end
|
|
93
|
+
else # records should be an array
|
|
94
|
+
records = records.map{ |r| r.is_a?( Integer ) ? self[r] : r } # turn indexes into records
|
|
95
|
+
|
|
96
|
+
for record in records
|
|
97
|
+
removed << self.remove_record( record )
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Fetches an indexed record based on the column indexed and the keying object.
|
|
104
|
+
# If key is an array of keying objects, returns an array of records in the
|
|
105
|
+
# same order that the keying objects appear.
|
|
106
|
+
# Index column should yield a different, unique value for each record.
|
|
107
|
+
def fetch( index_col, key )
|
|
108
|
+
if indexing_enabled?
|
|
109
|
+
if key.is_a?(Array)
|
|
110
|
+
key.map{ |k| @indexes[index_col][k] }
|
|
111
|
+
else
|
|
112
|
+
@indexes[index_col][key]
|
|
113
|
+
end
|
|
114
|
+
else
|
|
115
|
+
if key.is_a?(Array)
|
|
116
|
+
key.map{ |k| self.fetch( index_col, k ) }
|
|
117
|
+
else
|
|
118
|
+
index = index_of_column( index_col )
|
|
119
|
+
@records.detect{ |r| r.data[index] == key }
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# function should take an object, and return either true or false
|
|
125
|
+
# returns an array of objects that respond true when put through the
|
|
126
|
+
# meat grinder
|
|
127
|
+
def filter( record_filter = nil, &block )
|
|
128
|
+
if block_given?
|
|
129
|
+
filtered_records = []
|
|
130
|
+
|
|
131
|
+
@records.each do |record|
|
|
132
|
+
filtered_records << record if ( yield record )
|
|
133
|
+
end
|
|
134
|
+
elsif record_filter
|
|
135
|
+
filtered_records = record_filter.apply( @records )
|
|
136
|
+
else
|
|
137
|
+
warn "#{self.class}.filter called without block or RecordFilter.. "
|
|
138
|
+
filtered_records = @records
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
return filtered_records
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# removes rows which fail the given test from the spreadsheet.
|
|
145
|
+
# TODO! Should be returning self rather than the records.a
|
|
146
|
+
def filter!( record_filter = nil, &block )
|
|
147
|
+
@records = self.filter( record_filter, &block )
|
|
148
|
+
reindex if indexing_enabled?
|
|
149
|
+
@records
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def extra_method?( m )
|
|
153
|
+
@extra_methods.keys.include?( m )
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# The method given should take a record, return... well, anything.
|
|
157
|
+
def add_extra_method( method, &block )
|
|
158
|
+
@extra_methods[method.to_sym] = block
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def remove_extra_method( method )
|
|
162
|
+
@extra_methods.delete( method.to_sym )
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def call_extra_method( method, *args )
|
|
166
|
+
@extra_methods[method.to_sym].call( *args )
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def convert_types( record )
|
|
171
|
+
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module CsvMadness
|
|
2
|
+
module Utils
|
|
3
|
+
class ConstProc < Proc
|
|
4
|
+
def initialize( val, &block )
|
|
5
|
+
@rval = val
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def call(*args,&block)
|
|
9
|
+
@rval
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.proc_for( val )
|
|
13
|
+
ConstProc.new( val ) do
|
|
14
|
+
:does_nothing
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module CsvMadness
|
|
2
|
+
module Utils
|
|
3
|
+
class Counter
|
|
4
|
+
attr_reader :value
|
|
5
|
+
|
|
6
|
+
def initialize( initial_value = 0 )
|
|
7
|
+
@initial_value = initial_value
|
|
8
|
+
self.reset
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def click
|
|
12
|
+
@value += 1
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def reset
|
|
16
|
+
@value = resets_to()
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def resets_to
|
|
20
|
+
@initial_value
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module CsvMadness
|
|
2
|
+
module Utils
|
|
3
|
+
class CounterProc < Proc
|
|
4
|
+
def initialize( initial_value, &block )
|
|
5
|
+
@counter = Counter.new( initial_value )
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
# Each call return the value of the internal counter at the time the call mas made.
|
|
9
|
+
# Which is one lower than the number of times called. Hence the existence of the times_called method
|
|
10
|
+
def call(*args,&block)
|
|
11
|
+
rval = @counter.value
|
|
12
|
+
@counter.click
|
|
13
|
+
rval
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def reset
|
|
17
|
+
@counter.reset
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def times_called
|
|
21
|
+
@counter.value - @counter.resets_to
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.counter( initial_value = 0 )
|
|
25
|
+
self.new( initial_value ) do
|
|
26
|
+
:proc_does_nothing
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|