reckon 0.5.1 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +50 -0
  3. data/.gitignore +2 -0
  4. data/.ruby-version +1 -1
  5. data/CHANGELOG.md +74 -0
  6. data/Gemfile.lock +1 -5
  7. data/README.md +72 -16
  8. data/Rakefile +17 -1
  9. data/lib/reckon.rb +2 -5
  10. data/lib/reckon/app.rb +145 -71
  11. data/lib/reckon/cosine_similarity.rb +92 -89
  12. data/lib/reckon/csv_parser.rb +67 -122
  13. data/lib/reckon/date_column.rb +10 -0
  14. data/lib/reckon/ledger_parser.rb +11 -1
  15. data/lib/reckon/logger.rb +4 -0
  16. data/lib/reckon/money.rb +52 -51
  17. data/lib/reckon/version.rb +1 -1
  18. data/reckon.gemspec +1 -2
  19. data/spec/data_fixtures/51-sample.csv +8 -0
  20. data/spec/data_fixtures/51-tokens.yml +9 -0
  21. data/spec/data_fixtures/85-date-example.csv +2 -0
  22. data/spec/integration/another_bank_example/input.csv +9 -0
  23. data/spec/integration/another_bank_example/output.ledger +36 -0
  24. data/spec/integration/another_bank_example/test_args +1 -0
  25. data/spec/integration/austrian_example/input.csv +13 -0
  26. data/spec/integration/austrian_example/output.ledger +52 -0
  27. data/spec/integration/austrian_example/test_args +2 -0
  28. data/spec/integration/bom_utf8_file/input.csv +3 -0
  29. data/spec/integration/bom_utf8_file/output.ledger +4 -0
  30. data/spec/integration/bom_utf8_file/test_args +3 -0
  31. data/spec/integration/broker_canada_example/input.csv +12 -0
  32. data/spec/integration/broker_canada_example/output.ledger +48 -0
  33. data/spec/integration/broker_canada_example/test_args +1 -0
  34. data/spec/integration/chase/account_tokens_and_regex/output.ledger +36 -0
  35. data/spec/integration/chase/account_tokens_and_regex/test_args +2 -0
  36. data/spec/integration/chase/account_tokens_and_regex/tokens.yml +16 -0
  37. data/spec/integration/chase/default_account_names/output.ledger +36 -0
  38. data/spec/integration/chase/default_account_names/test_args +3 -0
  39. data/spec/integration/chase/input.csv +9 -0
  40. data/spec/integration/chase/learn_from_existing/learn.ledger +7 -0
  41. data/spec/integration/chase/learn_from_existing/output.ledger +36 -0
  42. data/spec/integration/chase/learn_from_existing/test_args +1 -0
  43. data/spec/integration/chase/simple/output.ledger +36 -0
  44. data/spec/integration/chase/simple/test_args +1 -0
  45. data/spec/integration/danish_kroner_nordea_example/input.csv +6 -0
  46. data/spec/integration/danish_kroner_nordea_example/output.ledger +24 -0
  47. data/spec/integration/danish_kroner_nordea_example/test_args +1 -0
  48. data/spec/integration/english_date_example/input.csv +3 -0
  49. data/spec/integration/english_date_example/output.ledger +12 -0
  50. data/spec/integration/english_date_example/test_args +1 -0
  51. data/spec/integration/extratofake/input.csv +24 -0
  52. data/spec/integration/extratofake/output.ledger +92 -0
  53. data/spec/integration/extratofake/test_args +1 -0
  54. data/spec/integration/french_example/input.csv +9 -0
  55. data/spec/integration/french_example/output.ledger +36 -0
  56. data/spec/integration/french_example/test_args +2 -0
  57. data/spec/integration/german_date_example/input.csv +3 -0
  58. data/spec/integration/german_date_example/output.ledger +12 -0
  59. data/spec/integration/german_date_example/test_args +1 -0
  60. data/spec/integration/harder_date_example/input.csv +5 -0
  61. data/spec/integration/harder_date_example/output.ledger +20 -0
  62. data/spec/integration/harder_date_example/test_args +1 -0
  63. data/spec/integration/ing/input.csv +3 -0
  64. data/spec/integration/ing/output.ledger +12 -0
  65. data/spec/integration/ing/test_args +1 -0
  66. data/spec/integration/intuit_mint_example/input.csv +7 -0
  67. data/spec/integration/intuit_mint_example/output.ledger +28 -0
  68. data/spec/integration/intuit_mint_example/test_args +1 -0
  69. data/spec/integration/invalid_header_example/input.csv +6 -0
  70. data/spec/integration/invalid_header_example/output.ledger +8 -0
  71. data/spec/integration/invalid_header_example/test_args +1 -0
  72. data/spec/integration/inversed_credit_card/input.csv +16 -0
  73. data/spec/integration/inversed_credit_card/output.ledger +64 -0
  74. data/spec/integration/inversed_credit_card/test_args +1 -0
  75. data/spec/integration/nationwide/input.csv +4 -0
  76. data/spec/integration/nationwide/output.ledger +16 -0
  77. data/spec/integration/nationwide/test_args +1 -0
  78. data/spec/integration/regression/issue_51_account_tokens/input.csv +8 -0
  79. data/spec/integration/regression/issue_51_account_tokens/output.ledger +32 -0
  80. data/spec/integration/regression/issue_51_account_tokens/test_args +4 -0
  81. data/spec/integration/regression/issue_51_account_tokens/tokens.yml +9 -0
  82. data/spec/integration/regression/issue_64_date_column/input.csv +3 -0
  83. data/spec/integration/regression/issue_64_date_column/output.ledger +8 -0
  84. data/spec/integration/regression/issue_64_date_column/test_args +1 -0
  85. data/spec/integration/regression/issue_73_account_token_matching/input.csv +2 -0
  86. data/spec/integration/regression/issue_73_account_token_matching/output.ledger +4 -0
  87. data/spec/integration/regression/issue_73_account_token_matching/test_args +6 -0
  88. data/spec/integration/regression/issue_73_account_token_matching/tokens.yml +8 -0
  89. data/spec/integration/regression/issue_85_date_example/input.csv +2 -0
  90. data/spec/integration/regression/issue_85_date_example/output.ledger +8 -0
  91. data/spec/integration/regression/issue_85_date_example/test_args +1 -0
  92. data/spec/integration/spanish_date_example/input.csv +3 -0
  93. data/spec/integration/spanish_date_example/output.ledger +12 -0
  94. data/spec/integration/spanish_date_example/test_args +1 -0
  95. data/spec/integration/suntrust/input.csv +7 -0
  96. data/spec/integration/suntrust/output.ledger +28 -0
  97. data/spec/integration/suntrust/test_args +1 -0
  98. data/spec/integration/test.sh +82 -0
  99. data/spec/integration/test_money_column/input.csv +3 -0
  100. data/spec/integration/test_money_column/output.ledger +8 -0
  101. data/spec/integration/test_money_column/test_args +1 -0
  102. data/spec/integration/two_money_columns/input.csv +5 -0
  103. data/spec/integration/two_money_columns/output.ledger +20 -0
  104. data/spec/integration/two_money_columns/test_args +1 -0
  105. data/spec/integration/yyyymmdd_date_example/input.csv +1 -0
  106. data/spec/integration/yyyymmdd_date_example/output.ledger +4 -0
  107. data/spec/integration/yyyymmdd_date_example/test_args +1 -0
  108. data/spec/reckon/app_spec.rb +18 -2
  109. data/spec/reckon/csv_parser_spec.rb +129 -129
  110. data/spec/reckon/ledger_parser_spec.rb +42 -5
  111. data/spec/reckon/money_column_spec.rb +24 -24
  112. data/spec/reckon/money_spec.rb +36 -42
  113. data/spec/spec_helper.rb +19 -0
  114. metadata +97 -22
  115. data/.travis.yml +0 -13
@@ -1,119 +1,122 @@
1
1
  require 'matrix'
2
+ require 'set'
2
3
 
3
4
  # Implementation of consine similarity using TF-IDF for vectorization.
4
5
  # Used to suggest which account a transaction should be assigned to
5
- class CosineSimilarity
6
- def initialize(options)
7
- @options = options
8
- @tokens = {}
9
- @accounts = Hash.new(0)
10
- end
11
-
12
- def add_document(account, doc)
13
- tokenize(doc).each do |n|
14
- (token, count) = n
15
-
16
- @tokens[token] ||= {}
17
- @tokens[token][account] ||= 0
18
- @tokens[token][account] += count
19
- @accounts[account] += count
6
+ module Reckon
7
+ class CosineSimilarity
8
+ def initialize(options)
9
+ @options = options
10
+ @tokens = {}
11
+ @accounts = Hash.new(0)
20
12
  end
21
- end
22
-
23
- # find most similar documents to query
24
- def find_similar(query)
25
- (query_scores, corpus_scores) = td_idf_scores_for(query)
26
13
 
27
- query_vector = Vector.elements(query_scores, false)
14
+ def add_document(account, doc)
15
+ tokenize(doc).each do |n|
16
+ (token, count) = n
28
17
 
29
- # For each doc, calculate the similarity to the query
30
- suggestions = corpus_scores.map do |account, scores|
31
- acct_vector = Vector.elements(scores, false)
18
+ @tokens[token] ||= {}
19
+ @tokens[token][account] ||= 0
20
+ @tokens[token][account] += count
21
+ @accounts[account] += count
22
+ end
23
+ end
32
24
 
33
- acct_query_dp = acct_vector.inner_product(query_vector)
34
- # similarity is a float between 1 and -1, where 1 is exactly the same and -1 is
35
- # exactly opposite
36
- # see https://en.wikipedia.org/wiki/Cosine_similarity
37
- # cos(theta) = (A . B) / (||A|| ||B||)
38
- # where A . B is the "dot product" and ||A|| is the magnitude of A
39
- # ruby has the 'matrix' library we can use to do these calculations.
40
- {
41
- similarity: acct_query_dp / (acct_vector.magnitude * query_vector.magnitude),
42
- account: account,
43
- }
44
- end.select { |n| n[:similarity] > 0 }.sort_by { |n| -n[:similarity] }
25
+ # find most similar documents to query
26
+ def find_similar(query)
27
+ (query_scores, corpus_scores) = td_idf_scores_for(query)
45
28
 
46
- LOGGER.info "most similar accounts: #{suggestions}"
29
+ query_vector = Vector.elements(query_scores, false)
47
30
 
48
- return suggestions
49
- end
31
+ # For each doc, calculate the similarity to the query
32
+ suggestions = corpus_scores.map do |account, scores|
33
+ acct_vector = Vector.elements(scores, false)
50
34
 
51
- private
35
+ acct_query_dp = acct_vector.inner_product(query_vector)
36
+ # similarity is a float between 1 and -1, where 1 is exactly the same and -1 is
37
+ # exactly opposite
38
+ # see https://en.wikipedia.org/wiki/Cosine_similarity
39
+ # cos(theta) = (A . B) / (||A|| ||B||)
40
+ # where A . B is the "dot product" and ||A|| is the magnitude of A
41
+ # ruby has the 'matrix' library we can use to do these calculations.
42
+ {
43
+ similarity: acct_query_dp / (acct_vector.magnitude * query_vector.magnitude),
44
+ account: account,
45
+ }
46
+ end.select { |n| n[:similarity] > 0 }.sort_by { |n| -n[:similarity] }
52
47
 
53
- def td_idf_scores_for(query)
54
- query_tokens = tokenize(query)
55
- corpus = Set.new
56
- corpus_scores = {}
57
- query_scores = []
58
- num_docs = @accounts.length
48
+ LOGGER.info "most similar accounts: #{suggestions}"
59
49
 
60
- query_tokens.each do |n|
61
- (token, _count) = n
62
- next unless @tokens[token]
63
- corpus = corpus.union(Set.new(@tokens[token].keys))
50
+ return suggestions
64
51
  end
65
52
 
66
- query_tokens.each do |n|
67
- (token, count) = n
53
+ private
68
54
 
69
- # if no other docs have token, ignore it
70
- next unless @tokens[token]
55
+ def td_idf_scores_for(query)
56
+ query_tokens = tokenize(query)
57
+ corpus = Set.new
58
+ corpus_scores = {}
59
+ query_scores = []
60
+ num_docs = @accounts.length
61
+
62
+ query_tokens.each do |n|
63
+ (token, _count) = n
64
+ next unless @tokens[token]
65
+ corpus = corpus.union(Set.new(@tokens[token].keys))
66
+ end
71
67
 
72
- ## First, calculate scores for our query as we're building scores for the corpus
73
- query_scores << calc_tf_idf(
74
- count,
75
- query_tokens.length,
76
- @tokens[token].length,
77
- num_docs
78
- )
68
+ query_tokens.each do |n|
69
+ (token, count) = n
79
70
 
80
- ## Next, calculate for the corpus, where our "account" is a document
81
- corpus.each do |account|
82
- corpus_scores[account] ||= []
71
+ # if no other docs have token, ignore it
72
+ next unless @tokens[token]
83
73
 
84
- corpus_scores[account] << calc_tf_idf(
85
- (@tokens[token][account] || 0),
86
- @accounts[account].to_f,
87
- @tokens[token].length.to_f,
74
+ ## First, calculate scores for our query as we're building scores for the corpus
75
+ query_scores << calc_tf_idf(
76
+ count,
77
+ query_tokens.length,
78
+ @tokens[token].length,
88
79
  num_docs
89
80
  )
81
+
82
+ ## Next, calculate for the corpus, where our "account" is a document
83
+ corpus.each do |account|
84
+ corpus_scores[account] ||= []
85
+
86
+ corpus_scores[account] << calc_tf_idf(
87
+ (@tokens[token][account] || 0),
88
+ @accounts[account].to_f,
89
+ @tokens[token].length.to_f,
90
+ num_docs
91
+ )
92
+ end
90
93
  end
94
+ [query_scores, corpus_scores]
91
95
  end
92
- [query_scores, corpus_scores]
93
- end
94
96
 
95
- def calc_tf_idf(token_count, num_words_in_doc, df, num_docs)
97
+ def calc_tf_idf(token_count, num_words_in_doc, df, num_docs)
96
98
 
97
- # tf(t,d) = count of t in d / number of words in d
98
- tf = token_count / num_words_in_doc.to_f
99
+ # tf(t,d) = count of t in d / number of words in d
100
+ tf = token_count / num_words_in_doc.to_f
99
101
 
100
- # smooth idf weight
101
- # see https://en.wikipedia.org/wiki/Tf%E2%80%93idf#Inverse_document_frequency_2
102
- # df(t) = num of documents with term t in them
103
- # idf(t) = log(N/(1 + df )) + 1
104
- idf = Math.log(num_docs.to_f / (1 + df)) + 1
102
+ # smooth idf weight
103
+ # see https://en.wikipedia.org/wiki/Tf%E2%80%93idf#Inverse_document_frequency_2
104
+ # df(t) = num of documents with term t in them
105
+ # idf(t) = log(N/(1 + df )) + 1
106
+ idf = Math.log(num_docs.to_f / (1 + df)) + 1
105
107
 
106
- tf * idf
107
- end
108
+ tf * idf
109
+ end
108
110
 
109
- def tokenize(str)
110
- mk_tokens(str).inject(Hash.new(0)) do |memo, n|
111
- memo[n] += 1
112
- memo
113
- end.to_a
114
- end
115
- end
111
+ def tokenize(str)
112
+ mk_tokens(str).inject(Hash.new(0)) do |memo, n|
113
+ memo[n] += 1
114
+ memo
115
+ end.to_a
116
+ end
116
117
 
117
- def mk_tokens(str)
118
- str.downcase.tr(';', ' ').tr("'", '').split(/[^a-z0-9.]+/)
118
+ def mk_tokens(str)
119
+ str.downcase.tr(';', ' ').tr("'", '').split(/[^a-z0-9.]+/)
120
+ end
121
+ end
119
122
  end
@@ -12,24 +12,39 @@ module Reckon
12
12
  detect_columns
13
13
  end
14
14
 
15
- def row(index)
16
- csv_data[index].join(", ")
15
+ def columns
16
+ @columns ||=
17
+ begin
18
+ last_row_length = nil
19
+ csv_data.inject([]) do |memo, row|
20
+ unless row.all? { |i| i.nil? || i.length == 0 }
21
+ row.each_with_index do |entry, index|
22
+ memo[index] ||= []
23
+ memo[index] << (entry || '').strip
24
+ end
25
+ last_row_length = row.length
26
+ end
27
+ memo
28
+ end
29
+ end
17
30
  end
18
31
 
19
- def filter_csv
20
- if options[:ignore_columns]
21
- new_columns = []
22
- columns.each_with_index do |column, index|
23
- new_columns << column unless options[:ignore_columns].include?(index + 1)
24
- end
25
- @columns = new_columns
26
- end
32
+ def date_for(index)
33
+ @date_column.for(index)
34
+ end
35
+
36
+ def pretty_date_for(index)
37
+ @date_column.pretty_for( index )
27
38
  end
28
39
 
29
40
  def money_for(index)
30
41
  @money_column[index]
31
42
  end
32
43
 
44
+ def pretty_money(amount, negate = false)
45
+ Money.new( amount, @options ).pretty( negate )
46
+ end
47
+
33
48
  def pretty_money_for(index, negate = false)
34
49
  money = money_for(index)
35
50
  return 0 if money.nil?
@@ -37,20 +52,29 @@ module Reckon
37
52
  money.pretty(negate)
38
53
  end
39
54
 
40
- def pretty_money(amount, negate = false)
41
- Money.new( amount, @options ).pretty( negate )
55
+ def description_for(index)
56
+ description_column_indices.map { |i| columns[i][index].to_s.strip }
57
+ .reject(&:empty?)
58
+ .join("; ")
59
+ .squeeze(" ")
60
+ .gsub(/(;\s+){2,}/, '')
61
+ .strip
42
62
  end
43
63
 
44
- def date_for(index)
45
- @date_column.for(index)
64
+ def row(index)
65
+ csv_data[index].join(", ")
46
66
  end
47
67
 
48
- def pretty_date_for(index)
49
- @date_column.pretty_for( index )
50
- end
68
+ private
51
69
 
52
- def description_for(index)
53
- description_column_indices.map { |i| columns[i][index] }.reject(&:empty?).join("; ").squeeze(" ").gsub(/(;\s+){2,}/, '').strip
70
+ def filter_csv
71
+ if options[:ignore_columns]
72
+ new_columns = []
73
+ columns.each_with_index do |column, index|
74
+ new_columns << column unless options[:ignore_columns].include?(index + 1)
75
+ end
76
+ @columns = new_columns
77
+ end
54
78
  end
55
79
 
56
80
  def evaluate_columns(cols)
@@ -65,12 +89,7 @@ module Reckon
65
89
  money_score += Money::likelihood( entry )
66
90
  possible_neg_money_count += 1 if entry =~ /^\$?[\-\(]\$?\d+/
67
91
  possible_pos_money_count += 1 if entry =~ /^\+?\$?\+?\d+/
68
- date_score += 10 if entry =~ /\b(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)/i
69
- date_score += 5 if entry =~ /^[\-\/\.\d:\[\]]+$/
70
- date_score += entry.gsub(/[^\-\/\.\d:\[\]]/, '').length if entry.gsub(/[^\-\/\.\d:\[\]]/, '').length > 3
71
- date_score -= entry.gsub(/[\-\/\.\d:\[\]]/, '').length
72
- date_score += 30 if entry =~ /^\d+[:\/\.-]\d+[:\/\.-]\d+([ :]\d+[:\/\.]\d+)?$/
73
- date_score += 10 if entry =~ /^\d+\[\d+:GMT\]$/i
92
+ date_score += DateColumn.likelihood(entry)
74
93
 
75
94
  # Try to determine if this is a balance column
76
95
  entry_as_num = entry.gsub(/[^\-\d\.]/, '').to_f
@@ -94,48 +113,24 @@ module Reckon
94
113
  results << { :index => index, :money_score => money_score, :date_score => date_score }
95
114
  end
96
115
 
97
- return [results, found_likely_money_column]
98
- end
116
+ results.sort_by! { |n| -n[:money_score] }
99
117
 
100
- def merge_columns(a, b)
101
- output_columns = []
102
- columns.each_with_index do |column, index|
103
- if index == a
104
- new_column = MoneyColumn.new( column )
105
- .merge!( MoneyColumn.new( columns[b] ) )
106
- .map { |m| m.amount.to_s }
107
- output_columns << new_column
108
- elsif index == b
109
- # skip
110
- else
111
- output_columns << column
112
- end
118
+ # check if it looks like a 2-column file with a balance field
119
+ if results.length >= 3 && results[1][:money_score] + results[2][:money_score] >= results[0][:money_score]
120
+ results[1][:is_money_column] = true
121
+ results[2][:is_money_column] = true
122
+ else
123
+ results[0][:is_money_column] = true
113
124
  end
114
- output_columns
115
- end
116
125
 
117
- def evaluate_two_money_columns( columns, id1, id2, unmerged_results )
118
- merged_columns = merge_columns( id1, id2 )
119
- results, found_likely_money_column = evaluate_columns( merged_columns )
120
- if !found_likely_money_column
121
- new_res = results.find { |el| el[:index] == id1 }
122
- old_res1 = unmerged_results.find { |el| el[:index] == id1 }
123
- old_res2 = unmerged_results.find { |el| el[:index] == id2 }
124
- if new_res[:money_score] > old_res1[:money_score] &&
125
- new_res[:money_score] > old_res2[:money_score]
126
- found_likely_money_column = true
127
- end
128
- end
129
- [results, found_likely_money_column]
126
+ return results.sort_by { |n| n[:index] }
130
127
  end
131
128
 
132
- def found_double_money_column( id1, id2 )
133
- self.money_column_indices = [ id1, id2 ]
134
- unless settings[:testing]
135
- puts "It looks like this CSV has two seperate columns for money, one of which shows positive"
136
- puts "changes and one of which shows negative changes. If this is true, great. Otherwise,"
137
- puts "please report this issue to us so we can take a look!\n"
138
- end
129
+ def found_double_money_column(id1, id2)
130
+ self.money_column_indices = [id1, id2]
131
+ puts "It looks like this CSV has two seperate columns for money, one of which shows positive"
132
+ puts "changes and one of which shows negative changes. If this is true, great. Otherwise,"
133
+ puts "please report this issue to us so we can take a look!\n"
139
134
  end
140
135
 
141
136
  # Some csv files negative/positive amounts are indicated in separate account
@@ -165,41 +160,18 @@ module Reckon
165
160
  end
166
161
 
167
162
  def detect_columns
168
- results, found_likely_money_column = evaluate_columns(columns)
163
+ results = evaluate_columns(columns)
164
+
169
165
  if options[:money_column]
170
- found_likely_money_column = true
171
- self.money_column_indices = [ options[:money_column] - 1 ]
166
+ self.money_column_indices = [options[:money_column] - 1]
172
167
  else
173
- self.money_column_indices = [ results.max_by { |n| n[:money_score] }[:index] ]
174
- end
175
-
176
- if !found_likely_money_column
177
- found_likely_double_money_columns = false
178
- 0.upto(columns.length - 2) do |i|
179
- if MoneyColumn.new( columns[i] ).merge!( MoneyColumn.new( columns[i+1] ) )
180
- _, found_likely_double_money_columns = evaluate_columns(merge_columns(i, i+1))
181
- if found_likely_double_money_columns
182
- found_double_money_column( i, i + 1 )
183
- break
184
- end
185
- end
186
- end
187
-
188
- if !found_likely_double_money_columns
189
- 0.upto(columns.length - 2) do |i|
190
- if MoneyColumn.new( columns[i] ).merge!( MoneyColumn.new( columns[i+1] ) )
191
- # Try a more specific test
192
- _, found_likely_double_money_columns = evaluate_two_money_columns( columns, i, i+1, results )
193
- if found_likely_double_money_columns
194
- found_double_money_column( i, i + 1 )
195
- break
196
- end
197
- end
198
- end
199
- end
200
-
201
- if !found_likely_double_money_columns && !settings[:testing]
202
- puts "I didn't find a high-likelyhood money column, but I'm taking my best guess with column #{money_column_indices.first + 1}."
168
+ self.money_column_indices = results.select { |n| n[:is_money_column] }.map { |n| n[:index] }
169
+ if self.money_column_indices.length == 1
170
+ puts "Using column #{money_column_indices.first + 1} as the money column. Use --money-colum to specify a different one."
171
+ elsif self.money_column_indices.length == 2
172
+ found_double_money_column(*self.money_column_indices)
173
+ else
174
+ puts "Unable to determine a money column, use --money-column to specify the column reckon should use."
203
175
  end
204
176
  end
205
177
 
@@ -223,23 +195,6 @@ module Reckon
223
195
  self.description_column_indices = results.map { |i| i[:index] }
224
196
  end
225
197
 
226
- def columns
227
- @columns ||= begin
228
- last_row_length = nil
229
- csv_data.inject([]) do |memo, row|
230
- # fail "Input CSV must have consistent row lengths." if last_row_length && row.length != last_row_length
231
- unless row.all? { |i| i.nil? || i.length == 0 }
232
- row.each_with_index do |entry, index|
233
- memo[index] ||= []
234
- memo[index] << (entry || '').strip
235
- end
236
- last_row_length = row.length
237
- end
238
- memo
239
- end
240
- end
241
- end
242
-
243
198
  def parse(data, filename=nil)
244
199
  # Use force_encoding to convert the string to utf-8 with as few invalid characters
245
200
  # as possible.
@@ -281,15 +236,5 @@ module Reckon
281
236
  end
282
237
  m && m[1]
283
238
  end
284
-
285
- @settings = { :testing => false }
286
-
287
- def self.settings
288
- @settings
289
- end
290
-
291
- def settings
292
- self.class.settings
293
- end
294
239
  end
295
240
  end