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,6 @@
1
+ module CsvMadness
2
+ module SheetMethods
3
+ module Base
4
+ end
5
+ end
6
+ end
@@ -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,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