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.

Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.document +5 -0
  3. data/CHANGELOG.markdown +4 -0
  4. data/Gemfile +2 -7
  5. data/Rakefile +12 -78
  6. data/VERSION +1 -1
  7. data/lib/csv_madness/builder.rb +91 -9
  8. data/lib/csv_madness/column.rb +30 -0
  9. data/lib/csv_madness/column_methods/core_methods.rb +6 -0
  10. data/lib/csv_madness/column_methods/default_value_methods.rb +7 -0
  11. data/lib/csv_madness/column_methods/error_methods.rb +19 -0
  12. data/lib/csv_madness/column_methods/fetch_methods.rb +11 -0
  13. data/lib/csv_madness/column_methods/typing_methods.rb +24 -0
  14. data/lib/csv_madness/column_set.rb +5 -0
  15. data/lib/csv_madness/column_type.rb +74 -0
  16. data/lib/csv_madness/column_types/date.rb +14 -0
  17. data/lib/csv_madness/column_types/float.rb +25 -0
  18. data/lib/csv_madness/column_types/integer.rb +17 -0
  19. data/lib/csv_madness/column_types/string.rb +9 -0
  20. data/lib/csv_madness/data_accessor_module.rb +79 -79
  21. data/lib/csv_madness/gem_api.rb +11 -2
  22. data/lib/csv_madness/index_set.rb +36 -0
  23. data/lib/csv_madness/indexer.rb +105 -0
  24. data/lib/csv_madness/record.rb +68 -42
  25. data/lib/csv_madness/record_filter.rb +68 -0
  26. data/lib/csv_madness/record_methods/method_methods.rb +57 -0
  27. data/lib/csv_madness/sheet.rb +160 -76
  28. data/lib/csv_madness/sheet_methods/class_methods.rb +11 -4
  29. data/lib/csv_madness/sheet_methods/column_methods.rb +95 -107
  30. data/lib/csv_madness/sheet_methods/file_methods.rb +9 -6
  31. data/lib/csv_madness/sheet_methods/indexing.rb +104 -23
  32. data/lib/csv_madness/sheet_methods/record_methods.rb +123 -47
  33. data/lib/csv_madness/sheet_methods/sorting_methods.rb +10 -0
  34. data/lib/csv_madness/utils/const_proc.rb +19 -0
  35. data/lib/csv_madness/utils/counter.rb +24 -0
  36. data/lib/csv_madness/utils/counter_proc.rb +31 -0
  37. data/lib/csv_madness/utils/looker_upper.rb +36 -0
  38. data/lib/csv_madness.rb +1 -0
  39. data/test/csv/nil_headers.csv +5 -0
  40. data/test/csv/out/.gitkeep +0 -0
  41. data/test/csv/pokemon.csv +9 -0
  42. data/test/csv/test_column_types.csv +2 -2
  43. data/test/helper.rb +9 -6
  44. data/test/test_builder.rb +36 -0
  45. data/test/test_csv_madness.rb +28 -18
  46. data/test/test_index_set.rb +30 -0
  47. data/test/test_indexer.rb +50 -0
  48. data/test/test_reloading_spreadsheet.rb +4 -2
  49. data/test/test_sheet.rb +164 -27
  50. data/test/test_utils.rb +99 -0
  51. metadata +66 -38
@@ -1,25 +1,69 @@
1
1
  module CsvMadness
2
2
  module SheetMethods
3
3
  module Indexing
4
+ attr_accessor :index_columns
5
+
4
6
  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
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 )
12
18
  end
13
19
  end
14
20
 
15
- def add_to_index( col, key, record )
16
- (@indexes[col] ||= {})[key] = record
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
17
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
+
18
62
 
19
- def add_to_indexes( records )
63
+ def index_records( records )
20
64
  if records.is_a?( Array )
21
65
  for record in records
22
- add_to_indexes( record )
66
+ index_records( record )
23
67
  end
24
68
  else
25
69
  record = records
@@ -28,24 +72,61 @@ module CsvMadness
28
72
  end
29
73
  end
30
74
  end
31
-
32
- def remove_from_index( record )
33
- for col in @index_columns
34
- @indexes[col].delete( record.send(col) )
35
- end
75
+
76
+ def has_index_column?( col )
77
+ @index_columns.include?( col.to_sym )
36
78
  end
37
79
 
38
80
  # Reindexes the record lookup tables.
39
- def reindex
40
- @indexes = {}
41
- add_to_indexes( @records )
81
+ def reindex( *cols )
82
+ # for column in (columns )
83
+ #
84
+ # @indexes = {}
85
+ # index_records( @records )
42
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
43
97
 
44
98
  # 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)
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?
49
130
  end
50
131
  end
51
132
  end
@@ -1,64 +1,101 @@
1
1
  module CsvMadness
2
2
  module SheetMethods
3
3
  module RecordMethods
4
- def add_record( record )
4
+ def add_record( record, &block )
5
5
  case record
6
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 )
7
+ # do nothing
9
8
  when Hash
10
- header = []
11
- fields = []
12
-
13
- for col in self.columns
14
- header << col
15
- fields << record[col]
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
16
27
  end
17
-
18
- record = CSV::Row.new( header, fields )
19
28
  when CSV::Row
20
- # do nothing
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
21
34
  else
22
35
  raise "sheet.add_record() doesn't take objects of type #{record.inspect}" unless record.respond_to?(:csv_data)
23
36
  record = record.csv_data
24
37
  end
25
-
26
- record = @record_class.new( record, self.column_accessors_map )
38
+
39
+ record = Record.new( record, self )
40
+
27
41
  @records << record
28
- add_to_indexes( record )
42
+
43
+ yield record if block_given?
44
+
45
+ index_records( record ) if indexing_enabled?
46
+
47
+ record
29
48
  end
30
49
 
31
50
  alias :<< :add_record
32
51
 
33
52
  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
53
+ self.add_record( [], &block )
54
+ #
55
+ # if block_given?
56
+ # yield self.records.last
57
+ # else
58
+ # self.records.last
59
+ # end
40
60
  end
41
61
 
42
62
  # record can be the row number (integer from 0...@records.length)
43
- # record can be the record itself (anonymous class)
63
+ # record can be the record itself
64
+ #
65
+ # returns the record that was removed.
44
66
  def remove_record( record )
45
- record = @records[record] if record.is_a?(Integer)
46
- return if record.nil?
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
47
76
 
48
- self.remove_from_index( record )
49
- @records.delete( record )
77
+ def remove_record_at_index( index )
78
+ self.records.delete_at( index )
50
79
  end
51
80
 
52
- # Here's the deal: you hand us a block, and we'll remove the records for which
53
- # it yields _true_.
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.
54
87
  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 )
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
62
99
  end
63
100
  end
64
101
  end
@@ -68,32 +105,71 @@ module CsvMadness
68
105
  # same order that the keying objects appear.
69
106
  # Index column should yield a different, unique value for each record.
70
107
  def fetch( index_col, key )
71
- if key.is_a?(Array)
72
- key.map{ |k| @indexes[index_col][k] }
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
73
114
  else
74
- @indexes[index_col][key]
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
75
121
  end
76
122
  end
77
123
 
78
124
  # function should take an object, and return either true or false
79
125
  # returns an array of objects that respond true when put through the
80
126
  # meat grinder
81
- def filter( &block )
82
- rval = []
83
- @records.each do |record|
84
- rval << record if ( yield record )
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
85
139
  end
86
-
87
- rval
140
+
141
+ return filtered_records
88
142
  end
89
143
 
90
144
  # removes rows which fail the given test from the spreadsheet.
91
145
  # TODO! Should be returning self rather than the records.a
92
- def filter!( &block )
93
- @records = self.filter( &block )
94
- reindex
146
+ def filter!( record_filter = nil, &block )
147
+ @records = self.filter( record_filter, &block )
148
+ reindex if indexing_enabled?
95
149
  @records
96
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
97
173
  end
98
174
  end
99
175
  end
@@ -0,0 +1,10 @@
1
+ module CsvMadness
2
+ module SheetMethods
3
+ module SortingMethods
4
+ # Changes the order that the records appear in the sheet
5
+ def sort( *columns, &block )
6
+
7
+ end
8
+ end
9
+ end
10
+ 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
@@ -0,0 +1,36 @@
1
+ module CsvMadness
2
+ module Utils
3
+ # Fast insert/remove/lookup indexing utility.
4
+ #
5
+ # For example, if you're keeping an index of a spreadsheet column, and you want to be able at any moment to get the records
6
+ # containing that value (id = 7124) or (name = 'Mary'),
7
+ #
8
+ # Is it a hash? Two hashes?
9
+ # It's like a hash because...
10
+ #
11
+ # It's like an array because order needs to be preserved.
12
+ #
13
+ # - insert( key, val )
14
+ # - add_to_result_index
15
+ # - add_position_in_result_index_to removal index
16
+ # -
17
+ # - update( key, val )
18
+ #
19
+ # - remove( val )
20
+ #
21
+ # - fetch_results( key )
22
+ #
23
+ # Is the order important? Definitely. But that means re-indexing whenever the record order changes?
24
+ #
25
+ # results_data
26
+ # removal_data
27
+ # ordering_data ... shared by multiple
28
+
29
+ # lu = LookerUpper.new
30
+ # lu.keep( )
31
+
32
+ class LookerUpper
33
+
34
+ end
35
+ end
36
+ end
data/lib/csv_madness.rb CHANGED
@@ -2,6 +2,7 @@ require 'csv'
2
2
  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
+ require 'set'
5
6
 
6
7
  FunWith::Gems.make_gem_fun( "CsvMadness" )
7
8
 
@@ -0,0 +1,5 @@
1
+ ,fname,,,
2
+ "1","Mary","Moore","27","1986-04-08 15:06:10"
3
+ "2","Bill","Paxton","39","1974-02-22"
4
+ "3","Charles","Darwin","72","Invalid Date"
5
+ "4","Chuck","Norris","57","1901-03-02"
File without changes
@@ -0,0 +1,9 @@
1
+ ID,PokemonType,CP,HP
2
+ 1,Jigglypuff,392,54
3
+ 2,Pikachu,408,29
4
+ 3,Charmander,392,35
5
+ 4,Tentacool,376,35
6
+ 5,Haunter,338,27
7
+ 6,Meowth,401,55
8
+ 7,Geodude,338,48
9
+ 8,Haunter,301,55
@@ -1,3 +1,3 @@
1
- "id","number","integer","float","date"
1
+ "id","number","integer","float","date","string"
2
2
  ,,,,
3
- 12,134.2,100,123.4,2013-01-13
3
+ 12,134.2,100,123.4,2013-01-13,"I am the very model of a coding individual"
data/test/helper.rb CHANGED
@@ -38,22 +38,22 @@ class MadTestCase < FunWith::Testing::TestCase # Test::Unit::TestCase
38
38
  end
39
39
 
40
40
  def load_mary
41
- id = @simple.index_columns.first
41
+ id = @simple.columns.first
42
42
  @mary = @simple.fetch( id, MARY_ID )
43
43
  end
44
44
 
45
45
  def load_bill
46
- id = @simple.index_columns.first
46
+ id = @simple.columns.first
47
47
  @bill = @simple.fetch( id, BILL_ID )
48
48
  end
49
49
 
50
50
  def load_darwin
51
- id = @simple.index_columns.first
51
+ id = @simple.columns.first
52
52
  @darwin = @simple.fetch( id, DARWIN_ID )
53
53
  end
54
54
 
55
55
  def load_chuck
56
- id = @simple.index_columns.first
56
+ id = @simple.columns.first
57
57
  @chuck = @simple.fetch( id, CHUCK_ID )
58
58
  end
59
59
 
@@ -65,7 +65,6 @@ class MadTestCase < FunWith::Testing::TestCase # Test::Unit::TestCase
65
65
  end
66
66
 
67
67
  def muck_up_spreadsheet
68
- debugger
69
68
  @simple.add_column(:scrambled_name) do |val, record|
70
69
  record.fname.chars.map(&:to_s).zip( record.lname.chars.map(&:to_s) ).flatten.compact.join
71
70
  end
@@ -80,8 +79,12 @@ class MadTestCase < FunWith::Testing::TestCase # Test::Unit::TestCase
80
79
  @simple.alter_column(:born) do |val|
81
80
  if val.is_a?(String)
82
81
  -1.0
83
- else
82
+ elsif val.is_a?(Date)
83
+ val.wday
84
+ elsif val.is_a?(Time)
84
85
  (Time.now - val).to_f
86
+ else
87
+ 57.3
85
88
  end
86
89
  end
87
90
 
data/test/test_builder.rb CHANGED
@@ -34,4 +34,40 @@ class TestBuilder < MadTestCase
34
34
  assert_equal 4225, ss.records.first.square
35
35
  end
36
36
  end
37
+
38
+ context "testing convenience column methods" do
39
+ setup do
40
+ @builder = CsvMadness::Builder.new do |s|
41
+ s.constant_column :string, "String"
42
+ s.constant_column :int, 5
43
+ s.constant_column :sym, :sym
44
+ s.blank_column :blank
45
+ s.counter_column :line, 1
46
+ end
47
+
48
+ @sheet = @builder.build( [nil, nil, nil] )
49
+ end
50
+
51
+ should "calculate the resulting sheet accurately" do
52
+ assert_equal [nil,nil,nil], @sheet.column( :blank )
53
+ assert_equal ["String", "String", "String"], @sheet.column( :string )
54
+ assert_equal 3, @sheet.records.last.line
55
+ assert_equal :sym, @sheet.records[0].sym
56
+ assert_equal 5, @sheet.records[1].int
57
+ assert_equal 5, @sheet.records[0][1]
58
+ end
59
+
60
+ should "add a record without a dumpster fire ensuing" do
61
+ data = [7] * @builder.column_names.length
62
+
63
+ @sheet.add_record( data )
64
+
65
+
66
+ r = @sheet.records.last
67
+
68
+ for sym in @builder.column_names
69
+ assert_equal 7, r.send( sym )
70
+ end
71
+ end
72
+ end
37
73
  end