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.

Files changed (54) hide show
  1. checksums.yaml +5 -13
  2. data/.document +5 -0
  3. data/CHANGELOG.markdown +4 -0
  4. data/Gemfile +2 -8
  5. data/README.rdoc +55 -47
  6. data/Rakefile +10 -49
  7. data/VERSION +1 -1
  8. data/lib/csv_madness/builder.rb +95 -10
  9. data/lib/csv_madness/column.rb +30 -0
  10. data/lib/csv_madness/column_methods/core_methods.rb +6 -0
  11. data/lib/csv_madness/column_methods/default_value_methods.rb +7 -0
  12. data/lib/csv_madness/column_methods/error_methods.rb +19 -0
  13. data/lib/csv_madness/column_methods/fetch_methods.rb +11 -0
  14. data/lib/csv_madness/column_methods/typing_methods.rb +24 -0
  15. data/lib/csv_madness/column_set.rb +5 -0
  16. data/lib/csv_madness/column_type.rb +74 -0
  17. data/lib/csv_madness/column_types/date.rb +14 -0
  18. data/lib/csv_madness/column_types/float.rb +25 -0
  19. data/lib/csv_madness/column_types/integer.rb +17 -0
  20. data/lib/csv_madness/column_types/string.rb +9 -0
  21. data/lib/csv_madness/data_accessor_module.rb +79 -44
  22. data/lib/csv_madness/exceptions.rb +16 -0
  23. data/lib/csv_madness/gem_api.rb +11 -2
  24. data/lib/csv_madness/index_set.rb +36 -0
  25. data/lib/csv_madness/indexer.rb +105 -0
  26. data/lib/csv_madness/record.rb +68 -40
  27. data/lib/csv_madness/record_filter.rb +68 -0
  28. data/lib/csv_madness/record_methods/method_methods.rb +57 -0
  29. data/lib/csv_madness/sheet.rb +172 -442
  30. data/lib/csv_madness/sheet_methods/base.rb +6 -0
  31. data/lib/csv_madness/sheet_methods/class_methods.rb +84 -0
  32. data/lib/csv_madness/sheet_methods/column_methods.rb +175 -0
  33. data/lib/csv_madness/sheet_methods/file_methods.rb +44 -0
  34. data/lib/csv_madness/sheet_methods/indexing.rb +133 -0
  35. data/lib/csv_madness/sheet_methods/record_methods.rb +175 -0
  36. data/lib/csv_madness/sheet_methods/sorting_methods.rb +10 -0
  37. data/lib/csv_madness/utils/const_proc.rb +19 -0
  38. data/lib/csv_madness/utils/counter.rb +24 -0
  39. data/lib/csv_madness/utils/counter_proc.rb +31 -0
  40. data/lib/csv_madness/utils/looker_upper.rb +36 -0
  41. data/lib/csv_madness.rb +2 -2
  42. data/test/csv/nil_headers.csv +5 -0
  43. data/test/csv/out/.gitkeep +0 -0
  44. data/test/csv/pokemon.csv +9 -0
  45. data/test/csv/test_column_types.csv +2 -2
  46. data/test/helper.rb +13 -5
  47. data/test/test_builder.rb +43 -3
  48. data/test/test_csv_madness.rb +34 -25
  49. data/test/test_index_set.rb +30 -0
  50. data/test/test_indexer.rb +50 -0
  51. data/test/test_reloading_spreadsheet.rb +5 -2
  52. data/test/test_sheet.rb +184 -37
  53. data/test/test_utils.rb +99 -0
  54. metadata +82 -36
@@ -0,0 +1,25 @@
1
+ module CsvMadness
2
+ module ColumnTypes
3
+ class FloatType < CsvMadness::ColumnType
4
+
5
+ # I suspect that a blank string and a nil should be treated the same, and that
6
+ # 0.0 is the wrong choice for a default. Maybe these should return :default or :error,
7
+ # and leave it to the individual column definition to handle that.
8
+ def self.convert( input )
9
+ if input.fwf_blank?
10
+ :default
11
+ elsif input.is_a?( String )
12
+ Float( input )
13
+ elsif input.respond_to?( :to_f )
14
+ input.to_f
15
+ elsif input.respond_to?( :to_i )
16
+ input.to_i.to_f
17
+ else
18
+ :error
19
+ end
20
+ rescue ArgumentError
21
+ :error
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,17 @@
1
+ module CsvMadness
2
+ module ColumnTypes
3
+ class IntegerType < CsvMadness::ColumnType
4
+ def self.convert( input )
5
+ if input.fwf_blank?
6
+ 0
7
+ elsif input.is_a?( String )
8
+ Float( input ).round
9
+ elsif input.respond_to?( :to_f )
10
+ input.to_f.round
11
+ elsif input.respond_to?( :to_i )
12
+ input.to_i
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,9 @@
1
+ module CsvMadness
2
+ module ColumnTypes
3
+ class StringType < CsvMadness::ColumnType
4
+ def self.convert( input )
5
+ input.to_s
6
+ end
7
+ end
8
+ end
9
+ end
@@ -1,44 +1,79 @@
1
- class DataAccessorModule < Module
2
- def initialize( mapping )
3
- @column_accessors = []
4
- remap_accessors( mapping )
5
- end
6
-
7
- def install_column_accessors( column, index )
8
- @column_accessors << column
9
- eval <<-EOF
10
- self.send( :define_method, :#{column} ) do
11
- self.csv_data[#{index}]
12
- end
13
-
14
- self.send( :define_method, :#{column}= ) do |val|
15
- self.csv_data[#{index}] = val
16
- end
17
- EOF
18
- end
19
-
20
- def remove_column_accessors( column )
21
- self.send( :remove_method, column )
22
- self.send( :remove_method, :"#{column}=" )
23
- end
24
-
25
- def remove_all_column_accessors
26
- @column_accessors ||= []
27
-
28
- for sym in @column_accessors
29
- remove_column_accessors( sym )
30
- end
31
-
32
- @column_accessors = []
33
- end
34
-
35
- def remap_accessors( mapping )
36
- remove_all_column_accessors
37
-
38
- @mapping = mapping
39
-
40
- for column, index in @mapping
41
- install_column_accessors( column, index )
42
- end
43
- end
44
- end
1
+ # # Each record within the spreadsheet is extended by a module with accessor functions
2
+ # # which allow the user to treat column names as methods. When columns are added, dropped, or re-ordered,
3
+ # # the accessor module needs to be updated to reflect the change.
4
+ # class DataAccessorModule < Module
5
+ # # mapping : keys = column symbol
6
+ # # values = column index
7
+ # attr_accessor :column_accessors_map
8
+ #
9
+ # def initialize( mapping )
10
+ # puts "Got mapping #{mapping.inspect}"
11
+ # @column_accessors_map = mapping
12
+ # remap_accessors
13
+ # end
14
+ #
15
+ # def install_column_accessors( *columns )
16
+ # for column in columns.flatten
17
+ # unless is_valid_ruby_method?( column )
18
+ # warn( "#{column.inspect} is not a valid ruby method" )
19
+ # require "data_accessor_module" # TODO: WTF?
20
+ # column = "csv_column_#{index}"
21
+ # warn( " ----> renaming to #{column.inspect}" )
22
+ # end
23
+ #
24
+ # if self.respond_to?( column )
25
+ # puts "Column already has accessors"
26
+ # else
27
+ # eval <<-EOF
28
+ # self.send( :define_method, :#{column} ) do
29
+ # self.csv_data[ self.column_accessors_map[ :#{column} ] ]
30
+ # end
31
+ #
32
+ # self.send( :define_method, :#{column}= ) do |val|
33
+ # self.csv_data[ self.column_accessors_map[ :#{column} ] ] = val
34
+ # end
35
+ # EOF
36
+ # end
37
+ # end
38
+ # end
39
+ #
40
+ # # But if instead of hardcoding the index, the index was pulled from the spreadsheet mapping...
41
+ # # a titch slower to look up? But a change to the mapping automatically updates the method
42
+ # def remove_column_accessors( *columns )
43
+ # for column in columns.flatten
44
+ # for method_sym in [column, :"#{column}="]
45
+ # self.send( :remove_method, method_sym ) if self.respond_to?( method_sym )
46
+ # end
47
+ # end
48
+ # end
49
+ #
50
+ # def remove_all_column_accessors
51
+ # # 2016-11-24 : was this necessary for anything
52
+ # # @column_accessors ||= []
53
+ # remove_column_accessors( @column_accessors_map.keys )
54
+ # end
55
+ #
56
+ # # Partly obsoleted.
57
+ # def remap_accessors( *args )
58
+ # #
59
+ #
60
+ #
61
+ # remove_all_column_accessors
62
+ #
63
+ # if args.length == 1
64
+ # debugger
65
+ # @column_accessors_map = args.first
66
+ # end
67
+ #
68
+ # install_column_accessors( @column_accessors_map.keys )
69
+ # end
70
+ #
71
+ # # Symbol.inspect doesn't output " quotes when given a sym
72
+ # # that expresses a valid method name or a valid identifier.
73
+ # def is_valid_ruby_method?( str_or_sym )
74
+ # sym = str_or_sym.to_sym
75
+ # test_string = sym.inspect
76
+ #
77
+ # test_string.match( /^:[@"]/ ).nil?
78
+ # end
79
+ # end
@@ -0,0 +1,16 @@
1
+ module CsvMadness
2
+ class LoadError < RuntimeError
3
+ end
4
+
5
+ class ColumnError < ArgumentError
6
+ end
7
+
8
+ class ForbiddenColumnNameError < ColumnError
9
+ end
10
+
11
+ class MissingColumnError < ColumnError
12
+ end
13
+
14
+ class ColumnExistsError < ColumnError
15
+ end
16
+ end
@@ -1,12 +1,21 @@
1
1
  module CsvMadness
2
2
  module GemAPI
3
+ # Loads a sheet from a csv file. If given a relative path, will look in
4
+ # default folders first.
3
5
  def load( csv, opts = {} )
4
6
  CsvMadness::Sheet.from( csv, opts )
5
7
  end
6
8
 
9
+
10
+ def add_search_path( path )
11
+ CsvMadness::Sheet.add_search_path( path )
12
+ end
13
+
14
+ # Define a builder using a block, then run
15
+ # the given objects through it to create a
16
+ # Sheet.
7
17
  def build( objects, &block )
8
- builder = CsvMadness::Builder.new(&block)
9
- builder.build( objects )
18
+ CsvMadness::Builder.new(&block).build( objects )
10
19
  end
11
20
  end
12
21
  end
@@ -0,0 +1,36 @@
1
+ module CsvMadness
2
+ # Used by indexer to store indexing data. With a regular set, two sets
3
+ # are considered equal if they hold the same objects, which is a problem
4
+ # for the @sets_containing_item data (needed for unindexing)
5
+ class IndexSet < Set
6
+ attr_reader :index_key, :index_value
7
+
8
+ def initialize( k, v )
9
+ super()
10
+ @index_key = k
11
+ @index_value = v
12
+ end
13
+ end
14
+ end
15
+
16
+ class Set
17
+ def inner_hash
18
+ @hash
19
+ end
20
+ end
21
+
22
+
23
+ class Set2 < Set
24
+ def eql?( rhs )
25
+ false
26
+ end
27
+ end
28
+
29
+ set = Set.new
30
+ set2 = Set2.new
31
+ set3 = Set2.new
32
+
33
+ set.add( set2 )
34
+ set.add( set3 )
35
+ set.length
36
+
@@ -0,0 +1,105 @@
1
+ module CsvMadness
2
+ class Indexer
3
+ # Just columns where every value is unique? Or fast results when several records have one value?
4
+
5
+ # :indexes => {
6
+ # :col1 => {
7
+ # :val1 => Set( Record1 ),
8
+ # :val2 => Set( Record2 )
9
+ # }
10
+ #
11
+ # :col2 => {
12
+ # :val1 => Set( Record0, Record2 ),
13
+ # :val2 => Set( Record1, Record3, Record7 )
14
+ # }
15
+ #
16
+ # But when a cell changes value, (say, R1.col2 switches from :val1 to :val2)
17
+ # :sets_containing_item => {
18
+ #
19
+ # }
20
+ # This could be a general-purpose module. So I'm dropping the restriction
21
+ # that the index has to be keyed to a symbol. Should be simple enough to
22
+ # manage on the caller's end, if that's needed
23
+ def initialize
24
+ # Maintaining this information is necessary to remove items
25
+ # from the indexes quickly.
26
+ @sets_containing_the_item = {} # key: a item, value: a set of set sets which live in @indexes
27
+ @indexes = {}
28
+ end
29
+
30
+
31
+ # If the item has previously been indexed in this index_name under another
32
+ # value, it must be removed
33
+ def index( item, index_key, index_val )
34
+ @sets_containing_the_item[item] ||= Set.new
35
+
36
+ @indexes[index_key] ||= {}
37
+ @indexes[index_key][index_val] ||= IndexSet.new( index_key, index_val )
38
+
39
+ unindex_item_from_one_index( item, index_key )
40
+
41
+ added_set = @indexes[index_key][index_val]
42
+ added_set.add( item )
43
+
44
+ @sets_containing_the_item[item].add( added_set )
45
+
46
+ item
47
+ end
48
+
49
+ # remove the item from all
50
+ def unindex( item )
51
+ if @sets_containing_the_item.has_key?( item )
52
+ for item_set in @sets_containing_the_item[item]
53
+ item_set.delete( item )
54
+ end
55
+
56
+ @sets_containing_the_item.delete( item )
57
+ end
58
+
59
+ item
60
+ end
61
+
62
+ def unindex_item_from_one_index( item, index_key )
63
+ indx = @indexes[index_key]
64
+
65
+ unless indx.fwf_blank?
66
+
67
+ end
68
+ end
69
+
70
+ # returns an array of items whose index_val matches the given value
71
+ def lookup( index_key, index_val )
72
+ if @indexes.has_key?( index_key ) && @indexes[index_key].has_key?( index_val )
73
+ @indexes[index_key][index_val].to_a
74
+ else
75
+ []
76
+ end
77
+ end
78
+
79
+ # worrying cases:
80
+ #
81
+ # If the old index_name name doesn't exist and new index_name name doesn't exist,
82
+ # it's a null operation
83
+ #
84
+ # If the new_index_name already exists, will raise an error rather than
85
+ # accidentally discarding the index.
86
+ def rename_index( old_index_key, new_index_key )
87
+ raise IndexExistsError.new( "Index exists: #{new_index_key}" ) if @indexes.has_key?( new_index_key )
88
+
89
+ @indexes[new_index_key] = @indexes.delete( old_index_key )
90
+ end
91
+
92
+ # deletes all indexing information for the given index
93
+ def drop_index( index_key )
94
+ # returns
95
+ sets_to_remove = @indexes.delete( index_key )
96
+
97
+ unless set_to_remove.fwf_blank?
98
+ # ci => key => a set of items
99
+ @sets_containing_the_item.each do |item, set|
100
+ set.delete( set_to_remove )
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -7,48 +7,58 @@ module CsvMadness
7
7
  # future. I'd like to be able to address by row and by
8
8
  # symbol.
9
9
  class Record
10
- attr_accessor :csv_data
10
+ include RecordMethods::MethodMethods
11
11
 
12
- def initialize( data )
13
- import_record_data( data )
14
- end
12
+ attr_accessor :data
13
+ attr_reader :spreadsheet
15
14
 
16
- def [] key
17
- case key
18
- when String, Integer
19
- @csv_data[key]
20
- when Symbol
21
- @csv_data[key.to_s]
22
- end
15
+ def initialize( _data, sheet ) # , mapping )
16
+ @spreadsheet = sheet
17
+ import_record_data( _data )
18
+ convert_types
23
19
  end
24
20
 
25
- def []= key, val
26
- case key
27
- when String, Integer
28
- @csv_data[key] = val
29
- when Symbol
30
- @csv_data[key.to_s] = val
31
- end
21
+ # Experimental approach to getter/setter methods. Seems easier than
22
+ # constantly rewriting an accessor module every time the columns get
23
+ # manipulated.
24
+ #
25
+ # Should raise a method_missing if the get/set method isn't in
26
+ # the index mapping.
27
+ def method_missing( method, *args )
28
+ self.spreadsheet.call_record_method( self, method, args )
32
29
  end
33
30
 
34
- def columns
35
- self.class.spreadsheet.columns
36
- end
37
31
 
38
- def self.columns
39
- self.spreadsheet.columns
32
+ def [] key
33
+ self.spreadsheet.get_cell( self, key )
40
34
  end
41
35
 
42
- def self.spreadsheet= sheet
43
- @spreadsheet = sheet
36
+
37
+ def []= key, val
38
+ self.spreadsheet.set_cell( self, key, val )
39
+ # case key
40
+ # when String, Integer
41
+ # @csv_data[key] = val
42
+ # when Symbol
43
+ # @csv_data[key.to_s] = val
44
+ # end
44
45
  end
45
46
 
46
- def self.spreadsheet
47
- @spreadsheet
47
+ def columns
48
+ self.spreadsheet.columns
48
49
  end
49
50
 
50
- def to_csv( opts = {} )
51
- self.columns.map{|col| self.send(col) }.to_csv( opts )
51
+ # def self.spreadsheet= sheet
52
+ # @spreadsheet = sheet
53
+ # end
54
+ #
55
+ # def self.spreadsheet
56
+ # @spreadsheet
57
+ # end
58
+ #
59
+ def to_csv( hash_opts = {}, **keyword_opts )
60
+ opts = hash_opts.merge(keyword_opts)
61
+ self.columns.map{|col| self.send(col) }.to_csv( **opts )
52
62
  end
53
63
 
54
64
  def to_hash
@@ -56,35 +66,53 @@ module CsvMadness
56
66
  end
57
67
 
58
68
  def to_a
59
- self.to_hash.to_a
69
+ self.data
60
70
  end
61
71
 
62
72
  def blank?( col )
63
73
  (self.send( col.to_sym ).to_s || "").strip.length == 0
64
74
  end
65
75
 
76
+ def convert_types
77
+ self.spreadsheet.convert_types( self ) if self.spreadsheet
78
+ end
79
+
80
+ def apply_update_hash( h )
81
+ for key, val in h
82
+ self.send( :"#{key}=", val )
83
+ end
84
+
85
+ self
86
+ end
87
+
88
+ def inspect
89
+ cols = self.spreadsheet.columns
90
+ cols.zip( self.data ).inspect
91
+ end
92
+
93
+
94
+
66
95
  protected
67
- def import_record_data( data )
68
- case data
96
+ def import_record_data( _data )
97
+ case _data
69
98
  when Array
70
- csv_data = CSV::Row.new( self.columns, data )
99
+ self.data = _data
71
100
  when Hash
72
101
  fields = self.columns.map do |col|
73
- data[col]
102
+ _data[col]
74
103
  end
75
104
  # for col in self.columns
76
105
  # fields << data[col]
77
106
  # end
78
107
 
79
- csv_data = CSV::Row.new( self.columns, fields )
108
+ self.data = fields # CSV::Row.new( self.columns, fields )
80
109
  when CSV::Row
81
- csv_data = data
110
+ self.data = _data.to_a.map(&:last)
111
+ when Record
112
+ self.data = _data.data.clone
82
113
  else
83
- raise "record.import_record_data() doesn't take objects of type #{data.inspect}" unless data.respond_to?(:csv_data)
84
- csv_data = data.csv_data.clone
114
+ raise "Don't know how to handle this input"
85
115
  end
86
-
87
- @csv_data = csv_data
88
116
  end
89
117
  end
90
118
  end
@@ -0,0 +1,68 @@
1
+ module CsvMadness
2
+ class RecordFilter
3
+ def initialize( &block )
4
+ @filters = []
5
+ yield self if block_given?
6
+ end
7
+
8
+ # if the block returns true, the record passes filters, no subsequent filters will be accepted
9
+ def accept( &block )
10
+ @filters << [:accept, block]
11
+ end
12
+
13
+ # if it returns true, the record fails the filter and is cut from the spreadsheet. Subsequent filters are ignored
14
+ def reject( &block )
15
+ @filters << [:reject, block]
16
+ end
17
+
18
+
19
+ def accept_column( col, val )
20
+ @filters << [:accept_column, col, val]
21
+ end
22
+
23
+ def reject_column( col, val )
24
+ @filters << [:reject_column, col, val]
25
+ end
26
+
27
+ def filter( records )
28
+ filtered_records = []
29
+
30
+ for r in records
31
+ accepted = false
32
+ rejected = false
33
+
34
+ for filter in @filters
35
+ next if accepted || rejected
36
+
37
+ case filter.first
38
+ when :accept, :accept_column
39
+ accepted = apply_filter( filter, record )
40
+ when :reject, :reject_column
41
+ rejected = apply_filter( filter, record )
42
+ end
43
+ end
44
+
45
+ filtered_records << record if accepted
46
+ end
47
+
48
+ filtered_records
49
+ end
50
+
51
+ def apply_filter( filter, record )
52
+ mode, col, val = filter
53
+ procedure = col if col.is_a?(Proc)
54
+
55
+ case mode
56
+ when :accept, :reject
57
+ procedure.call( record )
58
+ when :accept_column, :reject_column
59
+ case val
60
+ when String, Numeric, Date
61
+ record.send( col ) == val
62
+ when Regexp
63
+ record.send( col ) =~ val
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,57 @@
1
+ module CsvMadness
2
+ module RecordMethods
3
+ module MethodMethods
4
+ alias :respond_to_naturally? :respond_to?
5
+ alias :natural_methods :methods
6
+
7
+ def extra_method?( method )
8
+ self.spreadsheet.extra_methods.keys.include?( method )
9
+ end
10
+
11
+ def column_method?( method )
12
+ column_setter_method?( method ) || column_getter_method?( method )
13
+ end
14
+
15
+ def column_setter_method?( method )
16
+ if method[-1] == "="
17
+ self.spreadsheet.has_column?( method[0..-2].to_sym )
18
+ else
19
+ false
20
+ end
21
+ end
22
+
23
+ def column_getter_method?( method )
24
+ self.spreadsheet.has_column?( method )
25
+ end
26
+
27
+ def respond_to?( method )
28
+ self.methods.include?( method )
29
+ end
30
+
31
+ def methods
32
+ self.natural_methods +
33
+ self.spreadsheet.extra_methods.keys +
34
+ self.spreadsheet.columns +
35
+ self.spreadsheet.columns.map{ |col| to_setter_method( col ) }
36
+ end
37
+
38
+ def to_setter_method( method )
39
+ if method[-1] != "="
40
+ "#{method}=".to_sym
41
+ else
42
+ method.to_sym
43
+ end
44
+ end
45
+
46
+ def to_getter_method( method )
47
+ if method[-1] == "="
48
+ method[0..-2].to_sym
49
+ else
50
+ method.to_sym
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+