csv_madness 0.0.6 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,187 @@
1
+ module CsvMadness
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
+
60
+ FORBIDDEN_COLUMN_NAMES = [:to_s] # breaks things hard when you use them. Probably not comprehensive, sadly.
61
+
62
+
63
+ def column col
64
+ @records.map(&col)
65
+ end
66
+
67
+ def has_column?( col )
68
+ self.columns.include?( col.to_sym )
69
+ end
70
+
71
+ # retrieve multiple columns. Returns an array of the form
72
+ # [ [record1:col1, record1:col2...], [record2:col1, record2:col2...] [...] ]
73
+ def multiple_columns(*args)
74
+ @records.inject([]){ |memo, record|
75
+ memo << args.map{ |arg| record.send(arg) }
76
+ memo
77
+ }
78
+ end
79
+
80
+ def alter_column( column, blank = :undefined, &block )
81
+ raise_on_missing_columns( column )
82
+
83
+ if cindex = @columns.index( column )
84
+ for record in @records
85
+ if record.blank?(column) && blank != :undefined
86
+ record[cindex] = blank
87
+ else
88
+ record[cindex] = yield( record[cindex], record )
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ # If no block given, adds an empty column.
95
+ # If block given, fills the new column cells when the block is run on each record
96
+ # TODO: Should be able to specify the index of the column
97
+ def add_column( column, &block )
98
+ raise_on_forbidden_column_names( column )
99
+ raise ColumnExistsError.new( "Column already exists: #{column}" ) if @columns.include?( column )
100
+
101
+ @columns << column
102
+
103
+ # add empty column to each row
104
+ @records.map{ |r|
105
+ r.csv_data << {column => ""}
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
115
+ end
116
+
117
+ def drop_column( column )
118
+ raise_on_missing_columns( column )
119
+
120
+ @columns.delete( column )
121
+
122
+ key = column.to_s
123
+
124
+ @records.map{ |r|
125
+ r.csv_data.delete( key )
126
+ }
127
+
128
+ update_data_accessor_module
129
+ end
130
+
131
+ def rename_column( column, new_name )
132
+ @columns[@columns.index(column)] = new_name
133
+ rename_index_column( column, new_name ) if @index_columns.include?( column )
134
+ update_data_accessor_module
135
+ end
136
+
137
+ def set_column_type( column, type, blank = :undefined )
138
+ alter_column( column, blank, &COLUMN_TYPES[type] )
139
+ end
140
+
141
+
142
+ # If :reverse_merge is true, then the dest column is only overwritten for records where :dest is blank
143
+ def merge_columns( source, dest, opts = {} )
144
+ opts = { :drop_source => true, :reverse_merge => false, :default => "" }.merge( opts )
145
+ raise_on_missing_columns( source, dest )
146
+
147
+ self.records.each do |record|
148
+ if opts[:reverse_merge] == false || record.blank?( dest )
149
+ record[dest] = record.blank?(source) ? opts[:default] : record[source]
150
+ end
151
+ end
152
+
153
+ self.drop_column( source ) if opts[:drop_source]
154
+ end
155
+
156
+ # By default, the
157
+ def concat_columns( col1, col2, opts = {} )
158
+ opts = {:separator => '', :out => col1}.merge( opts )
159
+
160
+ raise_on_missing_columns( col1, col2 )
161
+ self.add_column( opts[:out] ) unless self.columns.include?( opts[:out] )
162
+
163
+ for record in self.records
164
+ record[ opts[:out] ] = "#{record[col1]}#{opts[:separator]}#{record[col2]}"
165
+ end
166
+ end
167
+
168
+ alias :concatenate :concat_columns
169
+
170
+ protected
171
+ def raise_on_missing_columns( *cols )
172
+ missing_columns = cols.flatten.reject{ |col| self.has_column?( col ) }
173
+
174
+ raise MissingColumnError.new( "#{caller[0]}: required_column(s) :#{missing_columns.inspect} not found." ) unless missing_columns.fwf_blank?
175
+ end
176
+
177
+ def forbidden_column_name?( col )
178
+ FORBIDDEN_COLUMN_NAMES.include?( col )
179
+ end
180
+
181
+ def raise_on_forbidden_column_names( *cols )
182
+ forbidden_columns = cols.flatten.select{ |col| self.forbidden_column_name?( col ) }
183
+ raise ForbiddenColumnNameError.new( "forbidden names: #{forbidden_columns.inspect}" ) unless forbidden_columns.fwf_blank?
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,41 @@
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
+ self.write_to_file( self.spreadsheet_file, @opts || {} )
24
+ else
25
+ raise "CsvMadness.save(:timestamp) - Spreadsheet must be specified by spreadsheet_file"
26
+ end
27
+ end
28
+
29
+
30
+ def reload_spreadsheet( opts = @opts )
31
+ load_csv if @spreadsheet_file
32
+ set_initial_columns( opts[:columns] )
33
+ create_record_class
34
+ package
35
+
36
+ set_index_columns( opts[:index] )
37
+ reindex
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,52 @@
1
+ module CsvMadness
2
+ module SheetMethods
3
+ module Indexing
4
+ def set_index_columns( index_columns )
5
+ @index_columns = case index_columns
6
+ when NilClass
7
+ []
8
+ when Symbol
9
+ [ index_columns ]
10
+ when Array
11
+ index_columns
12
+ end
13
+ end
14
+
15
+ def add_to_index( col, key, record )
16
+ (@indexes[col] ||= {})[key] = record
17
+ end
18
+
19
+ def add_to_indexes( records )
20
+ if records.is_a?( Array )
21
+ for record in records
22
+ add_to_indexes( record )
23
+ end
24
+ else
25
+ record = records
26
+ for col in @index_columns
27
+ add_to_index( col, record.send(col), record )
28
+ end
29
+ end
30
+ end
31
+
32
+ def remove_from_index( record )
33
+ for col in @index_columns
34
+ @indexes[col].delete( record.send(col) )
35
+ end
36
+ end
37
+
38
+ # Reindexes the record lookup tables.
39
+ def reindex
40
+ @indexes = {}
41
+ add_to_indexes( @records )
42
+ end
43
+
44
+ # shouldn't require reindex
45
+ def rename_index_column( column, new_name )
46
+ @index_columns[ @index_columns.index( column ) ] = new_name
47
+ @indexes[new_name] = @indexes[column]
48
+ @indexes.delete(column)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,99 @@
1
+ module CsvMadness
2
+ module SheetMethods
3
+ module RecordMethods
4
+ def add_record( record )
5
+ case record
6
+ when Array
7
+ # CSV::Row.new( column names, column_entries ) (in same order as columns, natch)
8
+ record = CSV::Row.new( self.columns, record )
9
+ when Hash
10
+ header = []
11
+ fields = []
12
+
13
+ for col in self.columns
14
+ header << col
15
+ fields << record[col]
16
+ end
17
+
18
+ record = CSV::Row.new( header, fields )
19
+ when CSV::Row
20
+ # do nothing
21
+ else
22
+ raise "sheet.add_record() doesn't take objects of type #{record.inspect}" unless record.respond_to?(:csv_data)
23
+ record = record.csv_data
24
+ end
25
+
26
+ record = @record_class.new( record, self.column_accessors_map )
27
+ @records << record
28
+ add_to_indexes( record )
29
+ end
30
+
31
+ alias :<< :add_record
32
+
33
+ def add_blank_record( &block )
34
+ self.add_record( {} )
35
+ if block_given?
36
+ yield self.records.last
37
+ else
38
+ self.records.last
39
+ end
40
+ end
41
+
42
+ # record can be the row number (integer from 0...@records.length)
43
+ # record can be the record itself (anonymous class)
44
+ def remove_record( record )
45
+ record = @records[record] if record.is_a?(Integer)
46
+ return if record.nil?
47
+
48
+ self.remove_from_index( record )
49
+ @records.delete( record )
50
+ end
51
+
52
+ # Here's the deal: you hand us a block, and we'll remove the records for which
53
+ # it yields _true_.
54
+ def remove_records( records = nil, &block )
55
+ if block_given?
56
+ for record in @records
57
+ remove_record( record ) if yield( record ) == true
58
+ end
59
+ else # records should be an array
60
+ for record in records
61
+ self.remove_record( record )
62
+ end
63
+ end
64
+ end
65
+
66
+ # Fetches an indexed record based on the column indexed and the keying object.
67
+ # If key is an array of keying objects, returns an array of records in the
68
+ # same order that the keying objects appear.
69
+ # Index column should yield a different, unique value for each record.
70
+ def fetch( index_col, key )
71
+ if key.is_a?(Array)
72
+ key.map{ |k| @indexes[index_col][k] }
73
+ else
74
+ @indexes[index_col][key]
75
+ end
76
+ end
77
+
78
+ # function should take an object, and return either true or false
79
+ # returns an array of objects that respond true when put through the
80
+ # meat grinder
81
+ def filter( &block )
82
+ rval = []
83
+ @records.each do |record|
84
+ rval << record if ( yield record )
85
+ end
86
+
87
+ rval
88
+ end
89
+
90
+ # removes rows which fail the given test from the spreadsheet.
91
+ # TODO! Should be returning self rather than the records.a
92
+ def filter!( &block )
93
+ @records = self.filter( &block )
94
+ reindex
95
+ @records
96
+ end
97
+ end
98
+ end
99
+ end
data/lib/csv_madness.rb CHANGED
@@ -3,6 +3,5 @@ require 'fun_with_gems'
3
3
  # require 'fun_with_version_strings'
4
4
  require 'time' # to use Time.parse to parse cells to get the date
5
5
 
6
- lib_dir = __FILE__.fwf_filepath.dirname
7
- FunWith::Gems.make_gem_fun( "CsvMadness", :require => lib_dir.join( "csv_madness" ) )
6
+ FunWith::Gems.make_gem_fun( "CsvMadness" )
8
7
 
data/test/helper.rb CHANGED
@@ -33,6 +33,10 @@ class MadTestCase < FunWith::Testing::TestCase # Test::Unit::TestCase
33
33
  set_spreadsheet_paths
34
34
  end
35
35
 
36
+ def load_sheet( *args )
37
+ @sheet = CsvMadness.load( *args )
38
+ end
39
+
36
40
  def load_mary
37
41
  id = @simple.index_columns.first
38
42
  @mary = @simple.fetch( id, MARY_ID )
@@ -61,6 +65,7 @@ class MadTestCase < FunWith::Testing::TestCase # Test::Unit::TestCase
61
65
  end
62
66
 
63
67
  def muck_up_spreadsheet
68
+ debugger
64
69
  @simple.add_column(:scrambled_name) do |val, record|
65
70
  record.fname.chars.map(&:to_s).zip( record.lname.chars.map(&:to_s) ).flatten.compact.join
66
71
  end
data/test/test_builder.rb CHANGED
@@ -4,15 +4,19 @@ class TestBuilder < MadTestCase
4
4
  context "testing simple cases" do
5
5
  should "spreadsheetize integers" do
6
6
  integers = [65, 66, 67, 68, 69, 70]
7
+
7
8
  sb = CsvMadness::Builder.new do |s|
8
- s.column( :even, "even?" )
9
+ s.column( :even, "even?" ) # calls the .even?() method on each object
9
10
  s.column( :odd, "odd?" )
10
11
  s.column( :hashh, "hash" )
11
- s.column( :hashhash, "hash.hash" )
12
+ s.column( :hashhash, "hash.hash" ) # calls .hash(), then calls .hash() on result
12
13
  s.column( :chr )
13
14
  s.column( :not_a_valid_method )
15
+ s.column( :square ) do |i| # Stores the block as a proc, runs on each object.
16
+ i * i
17
+ end
14
18
  end
15
- #
19
+
16
20
  ss = sb.build( integers )
17
21
 
18
22
  for record in ss.records
@@ -27,7 +31,7 @@ class TestBuilder < MadTestCase
27
31
  ss = sb.build( integers, :on_error => :ignore )
28
32
 
29
33
  assert_equal "", ss.records.first.not_a_valid_method
30
-
34
+ assert_equal 4225, ss.records.first.square
31
35
  end
32
36
  end
33
37
  end
@@ -110,7 +110,7 @@ class TestCsvMadness < MadTestCase
110
110
  assert_in_delta Time.parse("1986-04-08"), born1, 3600 * 24
111
111
 
112
112
  assert_kind_of String, born3
113
- assert_match /Invalid Time Format/, born3
113
+ assert_match( /Invalid Time Format/, born3 )
114
114
  end
115
115
 
116
116
  should "successfully decorate record objects with new functionality" do
@@ -212,19 +212,18 @@ class TestCsvMadness < MadTestCase
212
212
 
213
213
  should "to_csv properly" do
214
214
  @to_csv = @nilsheet.to_csv( force_quotes: true )
215
- assert_match /"Moore"/, @to_csv
216
- assert_match /"age","born"/, @to_csv
215
+ assert_match( /"Moore"/, @to_csv )
216
+ assert_match( /"age","born"/, @to_csv )
217
217
  end
218
218
 
219
219
  should "write to an output file properly" do
220
- # debugger
221
220
  @outfile = @csv_output_path.join("output_nilfile.csv")
222
221
  @nilsheet.write_to_file( @outfile, force_quotes: true )
223
222
 
224
223
  assert File.exist?( @outfile )
225
224
  @to_csv = File.read( @outfile )
226
- assert_match /"Moore"/, @to_csv
227
- assert_match /"age","born"/, @to_csv
225
+ assert_match( /"Moore"/, @to_csv )
226
+ assert_match( /"age","born"/, @to_csv )
228
227
  end
229
228
  end
230
229
 
@@ -236,7 +235,7 @@ class TestCsvMadness < MadTestCase
236
235
 
237
236
  should "add column" do
238
237
  @simple.add_column( :compound ) do |h, record|
239
- v = "#{record.fname} #{record.lname} #{record.id}"
238
+ "#{record.fname} #{record.lname} #{record.id}"
240
239
  end
241
240
 
242
241
  load_mary
@@ -12,7 +12,8 @@ class TestCsvMadness < MadTestCase
12
12
  assert @mary.respond_to?(:lname)
13
13
  assert @darwin.respond_to?(:fname)
14
14
  assert @mary.born.is_a?(String)
15
-
15
+
16
+ debugger
16
17
  muck_up_spreadsheet
17
18
  set_person_records
18
19
 
data/test/test_sheet.rb CHANGED
@@ -3,39 +3,46 @@ require 'helper'
3
3
  class TestSheet < MadTestCase
4
4
  context "testing getter_name()" do
5
5
  should "return proper function names" do
6
- assert_equal :hello_world, CsvMadness::Sheet.getter_name( " heLLo __ world " )
7
- assert_equal :_0_hello_world, CsvMadness::Sheet.getter_name( "0 heLLo __ worlD!!! " )
6
+ test_data = [
7
+ [" heLLo __ world ", :hello_world],
8
+ ["0 heLLo __ worlD!!! ", :_0_hello_world],
9
+ ["9ess 🐙 bsv++=", :_9ess_bsv ]
10
+ ]
11
+
12
+ for input, expected in test_data
13
+ assert_equal expected, CsvMadness::Sheet.getter_name( input )
14
+ end
8
15
  end
9
16
  end
10
17
 
11
18
  context "testing default spreadsheet paths" do
12
- should "only load existing paths" do
13
- assert_raises(RuntimeError) do
19
+ should "raise error if a path does not exist" do
20
+ assert_raises( RuntimeError ) do
14
21
  CsvMadness::Sheet.add_search_path( CsvMadness.root.join("rocaganthor") )
15
22
  end
16
23
  end
17
24
 
18
25
  should "check a search path for files to load" do
19
- sheet = CsvMadness.load( "with_nils.csv" )
20
- assert sheet.is_a?(CsvMadness::Sheet)
26
+ load_sheet( "with_nils.csv" )
27
+ assert @sheet.is_a?( CsvMadness::Sheet )
21
28
  end
22
29
  end
23
30
 
24
31
  context "testing column_types" do
25
32
  should "gracefully handle empty strings for all column_types" do
26
- sheet = CsvMadness.load("test_column_types.csv")
33
+ load_sheet("test_column_types.csv")
27
34
 
28
35
  for type, ignore_proc in CsvMadness::Sheet::COLUMN_TYPES
29
- sheet.set_column_type( type, type )
36
+ @sheet.set_column_type( type, type )
30
37
  end
31
38
 
32
- record = sheet.records[0]
39
+ record = @sheet.records[0]
33
40
 
34
41
  assert_kind_of String, record.date
35
42
  assert_equal nil, record.number
36
43
  assert_equal nil, record.float
37
44
 
38
- record = sheet.records[1]
45
+ record = @sheet.records[1]
39
46
  assert_equal "12", record.id
40
47
  assert_equal 134.2, record.number
41
48
  assert_equal 100, record.integer
@@ -45,7 +52,7 @@ class TestSheet < MadTestCase
45
52
 
46
53
  context "testing add/remove records" do
47
54
  setup do
48
- @sheet = CsvMadness.load( "simple.csv", :index => :id )
55
+ load_sheet( "simple.csv", :index => :id )
49
56
  end
50
57
 
51
58
  should "add record" do
@@ -95,17 +102,20 @@ class TestSheet < MadTestCase
95
102
  end
96
103
 
97
104
  should "deliver me a properly blanked spreadsheet (index)" do
98
- sheet = CsvMadness.load( "splitter.csv", :index => [:id] )
99
- blank_sheet = sheet.blanked
105
+ load_sheet( "splitter.csv", :index => [:id] )
106
+ blank_sheet = @sheet.blanked
107
+
108
+ assert_zero blank_sheet.length
109
+ assert_includes blank_sheet.index_columns, :id
100
110
  end
101
111
 
102
112
  should "split splitter" do
103
- sheet = CsvMadness.load( "splitter.csv", :index => [:id, :party] )
113
+ load_sheet( "splitter.csv", :index => [:id, :party] )
104
114
 
105
- sheets = sheet.split(&:party)
115
+ sheets = @sheet.split(&:party)
106
116
 
107
117
  for party in %w(D I R)
108
- assert_equal_length sheets[party], sheet.records.select{|r| r.party == party }
118
+ assert_equal_length sheets[party], @sheet.records.select{|r| r.party == party }
109
119
  end
110
120
 
111
121
  assert_includes sheets["D"].index_columns, :id
@@ -115,8 +125,8 @@ class TestSheet < MadTestCase
115
125
 
116
126
  context "testing hilarious spreadsheet fails" do
117
127
  should "raise error on forbidden column name" do
118
- assert_raises(RuntimeError) do
119
- sheet = CsvMadness.load( "forbidden_column.csv" )
128
+ assert_raises( CsvMadness::ForbiddenColumnNameError ) do
129
+ CsvMadness.load( "forbidden_column.csv" )
120
130
  end
121
131
  end
122
132
  end