reckon 0.5.2 → 0.6.2

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.
Files changed (112) 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 +66 -2
  6. data/Gemfile.lock +1 -5
  7. data/README.md +76 -16
  8. data/Rakefile +17 -1
  9. data/bin/reckon +6 -1
  10. data/lib/reckon.rb +2 -5
  11. data/lib/reckon/app.rb +156 -73
  12. data/lib/reckon/cosine_similarity.rb +91 -89
  13. data/lib/reckon/csv_parser.rb +8 -8
  14. data/lib/reckon/date_column.rb +10 -0
  15. data/lib/reckon/ledger_parser.rb +11 -1
  16. data/lib/reckon/logger.rb +4 -0
  17. data/lib/reckon/money.rb +48 -48
  18. data/lib/reckon/version.rb +1 -1
  19. data/reckon.gemspec +1 -2
  20. data/spec/integration/another_bank_example/input.csv +9 -0
  21. data/spec/integration/another_bank_example/output.ledger +36 -0
  22. data/spec/integration/another_bank_example/test_args +1 -0
  23. data/spec/integration/austrian_example/input.csv +13 -0
  24. data/spec/integration/austrian_example/output.ledger +52 -0
  25. data/spec/integration/austrian_example/test_args +2 -0
  26. data/spec/integration/bom_utf8_file/input.csv +3 -0
  27. data/spec/integration/bom_utf8_file/output.ledger +4 -0
  28. data/spec/integration/bom_utf8_file/test_args +3 -0
  29. data/spec/integration/broker_canada_example/input.csv +12 -0
  30. data/spec/integration/broker_canada_example/output.ledger +48 -0
  31. data/spec/integration/broker_canada_example/test_args +1 -0
  32. data/spec/integration/chase/account_tokens_and_regex/output.ledger +36 -0
  33. data/spec/integration/chase/account_tokens_and_regex/test_args +2 -0
  34. data/spec/integration/chase/account_tokens_and_regex/tokens.yml +16 -0
  35. data/spec/integration/chase/default_account_names/output.ledger +36 -0
  36. data/spec/integration/chase/default_account_names/test_args +3 -0
  37. data/spec/integration/chase/input.csv +9 -0
  38. data/spec/integration/chase/learn_from_existing/learn.ledger +7 -0
  39. data/spec/integration/chase/learn_from_existing/output.ledger +36 -0
  40. data/spec/integration/chase/learn_from_existing/test_args +1 -0
  41. data/spec/integration/chase/simple/output.ledger +36 -0
  42. data/spec/integration/chase/simple/test_args +1 -0
  43. data/spec/integration/danish_kroner_nordea_example/input.csv +6 -0
  44. data/spec/integration/danish_kroner_nordea_example/output.ledger +24 -0
  45. data/spec/integration/danish_kroner_nordea_example/test_args +1 -0
  46. data/spec/integration/english_date_example/input.csv +3 -0
  47. data/spec/integration/english_date_example/output.ledger +12 -0
  48. data/spec/integration/english_date_example/test_args +1 -0
  49. data/spec/integration/extratofake/input.csv +24 -0
  50. data/spec/integration/extratofake/output.ledger +92 -0
  51. data/spec/integration/extratofake/test_args +1 -0
  52. data/spec/integration/french_example/input.csv +9 -0
  53. data/spec/integration/french_example/output.ledger +36 -0
  54. data/spec/integration/french_example/test_args +2 -0
  55. data/spec/integration/german_date_example/input.csv +3 -0
  56. data/spec/integration/german_date_example/output.ledger +12 -0
  57. data/spec/integration/german_date_example/test_args +1 -0
  58. data/spec/integration/harder_date_example/input.csv +5 -0
  59. data/spec/integration/harder_date_example/output.ledger +20 -0
  60. data/spec/integration/harder_date_example/test_args +1 -0
  61. data/spec/integration/ing/input.csv +3 -0
  62. data/spec/integration/ing/output.ledger +12 -0
  63. data/spec/integration/ing/test_args +1 -0
  64. data/spec/integration/intuit_mint_example/input.csv +7 -0
  65. data/spec/integration/intuit_mint_example/output.ledger +28 -0
  66. data/spec/integration/intuit_mint_example/test_args +1 -0
  67. data/spec/integration/invalid_header_example/input.csv +6 -0
  68. data/spec/integration/invalid_header_example/output.ledger +8 -0
  69. data/spec/integration/invalid_header_example/test_args +1 -0
  70. data/spec/integration/inversed_credit_card/input.csv +16 -0
  71. data/spec/integration/inversed_credit_card/output.ledger +64 -0
  72. data/spec/integration/inversed_credit_card/test_args +1 -0
  73. data/spec/integration/nationwide/input.csv +4 -0
  74. data/spec/integration/nationwide/output.ledger +16 -0
  75. data/spec/integration/nationwide/test_args +1 -0
  76. data/spec/integration/regression/issue_51_account_tokens/input.csv +8 -0
  77. data/spec/integration/regression/issue_51_account_tokens/output.ledger +32 -0
  78. data/spec/integration/regression/issue_51_account_tokens/test_args +4 -0
  79. data/spec/integration/regression/issue_51_account_tokens/tokens.yml +9 -0
  80. data/spec/integration/regression/issue_64_date_column/input.csv +3 -0
  81. data/spec/integration/regression/issue_64_date_column/output.ledger +8 -0
  82. data/spec/integration/regression/issue_64_date_column/test_args +1 -0
  83. data/spec/integration/regression/issue_73_account_token_matching/input.csv +2 -0
  84. data/spec/integration/regression/issue_73_account_token_matching/output.ledger +4 -0
  85. data/spec/integration/regression/issue_73_account_token_matching/test_args +6 -0
  86. data/spec/integration/regression/issue_73_account_token_matching/tokens.yml +8 -0
  87. data/spec/integration/regression/issue_85_date_example/input.csv +2 -0
  88. data/spec/integration/regression/issue_85_date_example/output.ledger +8 -0
  89. data/spec/integration/regression/issue_85_date_example/test_args +1 -0
  90. data/spec/integration/spanish_date_example/input.csv +3 -0
  91. data/spec/integration/spanish_date_example/output.ledger +12 -0
  92. data/spec/integration/spanish_date_example/test_args +1 -0
  93. data/spec/integration/suntrust/input.csv +7 -0
  94. data/spec/integration/suntrust/output.ledger +28 -0
  95. data/spec/integration/suntrust/test_args +1 -0
  96. data/spec/integration/test.sh +82 -0
  97. data/spec/integration/test_money_column/input.csv +3 -0
  98. data/spec/integration/test_money_column/output.ledger +8 -0
  99. data/spec/integration/test_money_column/test_args +1 -0
  100. data/spec/integration/two_money_columns/input.csv +5 -0
  101. data/spec/integration/two_money_columns/output.ledger +20 -0
  102. data/spec/integration/two_money_columns/test_args +1 -0
  103. data/spec/integration/yyyymmdd_date_example/input.csv +1 -0
  104. data/spec/integration/yyyymmdd_date_example/output.ledger +4 -0
  105. data/spec/integration/yyyymmdd_date_example/test_args +1 -0
  106. data/spec/reckon/app_spec.rb +18 -2
  107. data/spec/reckon/csv_parser_spec.rb +5 -0
  108. data/spec/reckon/ledger_parser_spec.rb +42 -5
  109. data/spec/reckon/money_column_spec.rb +24 -24
  110. data/spec/reckon/money_spec.rb +13 -32
  111. metadata +94 -21
  112. data/.travis.yml +0 -13
@@ -3,118 +3,120 @@ require 'set'
3
3
 
4
4
  # Implementation of consine similarity using TF-IDF for vectorization.
5
5
  # Used to suggest which account a transaction should be assigned to
6
- class CosineSimilarity
7
- def initialize(options)
8
- @options = options
9
- @tokens = {}
10
- @accounts = Hash.new(0)
11
- end
12
-
13
- def add_document(account, doc)
14
- tokenize(doc).each do |n|
15
- (token, count) = n
16
-
17
- @tokens[token] ||= {}
18
- @tokens[token][account] ||= 0
19
- @tokens[token][account] += count
20
- @accounts[account] += count
6
+ module Reckon
7
+ class CosineSimilarity
8
+ def initialize(options)
9
+ @options = options
10
+ @tokens = {}
11
+ @accounts = Hash.new(0)
21
12
  end
22
- end
23
-
24
- # find most similar documents to query
25
- def find_similar(query)
26
- (query_scores, corpus_scores) = td_idf_scores_for(query)
27
13
 
28
- query_vector = Vector.elements(query_scores, false)
14
+ def add_document(account, doc)
15
+ tokenize(doc).each do |n|
16
+ (token, count) = n
29
17
 
30
- # For each doc, calculate the similarity to the query
31
- suggestions = corpus_scores.map do |account, scores|
32
- 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
33
24
 
34
- acct_query_dp = acct_vector.inner_product(query_vector)
35
- # similarity is a float between 1 and -1, where 1 is exactly the same and -1 is
36
- # exactly opposite
37
- # see https://en.wikipedia.org/wiki/Cosine_similarity
38
- # cos(theta) = (A . B) / (||A|| ||B||)
39
- # where A . B is the "dot product" and ||A|| is the magnitude of A
40
- # ruby has the 'matrix' library we can use to do these calculations.
41
- {
42
- similarity: acct_query_dp / (acct_vector.magnitude * query_vector.magnitude),
43
- account: account,
44
- }
45
- 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)
46
28
 
47
- LOGGER.info "most similar accounts: #{suggestions}"
29
+ query_vector = Vector.elements(query_scores, false)
48
30
 
49
- return suggestions
50
- 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)
51
34
 
52
- 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] }
53
47
 
54
- def td_idf_scores_for(query)
55
- query_tokens = tokenize(query)
56
- corpus = Set.new
57
- corpus_scores = {}
58
- query_scores = []
59
- num_docs = @accounts.length
48
+ LOGGER.info "most similar accounts: #{suggestions}"
60
49
 
61
- query_tokens.each do |n|
62
- (token, _count) = n
63
- next unless @tokens[token]
64
- corpus = corpus.union(Set.new(@tokens[token].keys))
50
+ return suggestions
65
51
  end
66
52
 
67
- query_tokens.each do |n|
68
- (token, count) = n
53
+ private
69
54
 
70
- # if no other docs have token, ignore it
71
- 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
72
67
 
73
- ## First, calculate scores for our query as we're building scores for the corpus
74
- query_scores << calc_tf_idf(
75
- count,
76
- query_tokens.length,
77
- @tokens[token].length,
78
- num_docs
79
- )
68
+ query_tokens.each do |n|
69
+ (token, count) = n
80
70
 
81
- ## Next, calculate for the corpus, where our "account" is a document
82
- corpus.each do |account|
83
- corpus_scores[account] ||= []
71
+ # if no other docs have token, ignore it
72
+ next unless @tokens[token]
84
73
 
85
- corpus_scores[account] << calc_tf_idf(
86
- (@tokens[token][account] || 0),
87
- @accounts[account].to_f,
88
- @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,
89
79
  num_docs
90
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
91
93
  end
94
+ [query_scores, corpus_scores]
92
95
  end
93
- [query_scores, corpus_scores]
94
- end
95
96
 
96
- 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)
97
98
 
98
- # tf(t,d) = count of t in d / number of words in d
99
- 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
100
101
 
101
- # smooth idf weight
102
- # see https://en.wikipedia.org/wiki/Tf%E2%80%93idf#Inverse_document_frequency_2
103
- # df(t) = num of documents with term t in them
104
- # idf(t) = log(N/(1 + df )) + 1
105
- 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
106
107
 
107
- tf * idf
108
- end
108
+ tf * idf
109
+ end
109
110
 
110
- def tokenize(str)
111
- mk_tokens(str).inject(Hash.new(0)) do |memo, n|
112
- memo[n] += 1
113
- memo
114
- end.to_a
115
- end
116
- 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
117
117
 
118
- def mk_tokens(str)
119
- 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
120
122
  end
@@ -53,7 +53,12 @@ module Reckon
53
53
  end
54
54
 
55
55
  def description_for(index)
56
- description_column_indices.map { |i| columns[i][index] }.reject(&:empty?).join("; ").squeeze(" ").gsub(/(;\s+){2,}/, '').strip
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
57
62
  end
58
63
 
59
64
  def row(index)
@@ -84,12 +89,7 @@ module Reckon
84
89
  money_score += Money::likelihood( entry )
85
90
  possible_neg_money_count += 1 if entry =~ /^\$?[\-\(]\$?\d+/
86
91
  possible_pos_money_count += 1 if entry =~ /^\+?\$?\+?\d+/
87
- date_score += 10 if entry =~ /\b(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)/i
88
- date_score += 5 if entry =~ /^[\-\/\.\d:\[\]]+$/
89
- date_score += entry.gsub(/[^\-\/\.\d:\[\]]/, '').length if entry.gsub(/[^\-\/\.\d:\[\]]/, '').length > 3
90
- date_score -= entry.gsub(/[\-\/\.\d:\[\]]/, '').length
91
- date_score += 30 if entry =~ /^\d+[:\/\.-]\d+[:\/\.-]\d+([ :]\d+[:\/\.]\d+)?$/
92
- date_score += 10 if entry =~ /^\d+\[\d+:GMT\]$/i
92
+ date_score += DateColumn.likelihood(entry)
93
93
 
94
94
  # Try to determine if this is a balance column
95
95
  entry_as_num = entry.gsub(/[^\-\d\.]/, '').to_f
@@ -163,7 +163,7 @@ module Reckon
163
163
  results = evaluate_columns(columns)
164
164
 
165
165
  if options[:money_column]
166
- self.money_column_indices = [ options[:money_column] - 1 ]
166
+ self.money_column_indices = [options[:money_column] - 1]
167
167
  else
168
168
  self.money_column_indices = results.select { |n| n[:is_money_column] }.map { |n| n[:index] }
169
169
  if self.money_column_indices.length == 1
@@ -56,5 +56,15 @@ module Reckon
56
56
  date.iso8601
57
57
  end
58
58
 
59
+ def self.likelihood(entry)
60
+ date_score = 0
61
+ date_score += 10 if entry =~ /\b(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)/i
62
+ date_score += 5 if entry =~ /^[\-\/\.\d:\[\]]+$/
63
+ date_score += entry.gsub(/[^\-\/\.\d:\[\]]/, '').length if entry.gsub(/[^\-\/\.\d:\[\]]/, '').length > 3
64
+ date_score -= entry.gsub(/[\-\/\.\d:\[\]]/, '').length
65
+ date_score += 30 if entry =~ /^\d+[:\/\.-]\d+[:\/\.-]\d+([ :]\d+[:\/\.]\d+)?$/
66
+ date_score += 10 if entry =~ /^\d+\[\d+:GMT\]$/i
67
+ return date_score
68
+ end
59
69
  end
60
70
  end
@@ -121,8 +121,14 @@ module Reckon
121
121
  def parse(ledger)
122
122
  @entries = []
123
123
  new_entry = {}
124
+ in_comment = false
125
+ comment_chars = ';#%*|'
124
126
  ledger.strip.split("\n").each do |entry|
125
- next if entry =~ /^\s*$/ || entry =~ /^\s*;/
127
+ # strip comment lines
128
+ in_comment = true if entry == 'comment'
129
+ in_comment = false if entry == 'end comment'
130
+ next if in_comment
131
+ next if entry =~ /^\s*[#{comment_chars}]/
126
132
 
127
133
  # (date, type, code, description), type and code are optional
128
134
  if (m = entry.match(%r{^(\d+[\d/-]+)\s+([*!])?\s*(\([^)]+\))?\s*(.*)$}))
@@ -134,7 +140,11 @@ module Reckon
134
140
  desc: m[4].strip,
135
141
  accounts: []
136
142
  }
143
+ elsif entry =~ /^\s*$/ && new_entry[:date]
144
+ add_entry(new_entry)
145
+ new_entry = {}
137
146
  elsif new_entry[:date] && entry =~ /^\s+/
147
+ LOGGER.info("Adding new account #{entry}")
138
148
  new_entry[:accounts] << parse_account_line(entry)
139
149
  else
140
150
  LOGGER.info("Unknown entry type: #{entry}")
@@ -0,0 +1,4 @@
1
+ module Reckon
2
+ LOGGER = Logger.new(STDERR)
3
+ LOGGER.level = Logger::WARN
4
+ end
@@ -5,12 +5,13 @@ module Reckon
5
5
  class Money
6
6
  include Comparable
7
7
  attr_accessor :amount, :currency, :suffixed
8
- def initialize( amount, options = {} )
9
- if options[:inverse]
10
- @amount = -1*amount.to_f
11
- else
12
- @amount = amount.to_f
13
- end
8
+ def initialize(amount, options = {})
9
+ @options = options
10
+ @amount_raw = amount
11
+ @raw = options[:raw]
12
+
13
+ @amount = parse(amount, options)
14
+ @amount = -@amount if options[:inverse]
14
15
  @currency = options[:currency] || "$"
15
16
  @suffixed = options[:suffixed]
16
17
  end
@@ -19,11 +20,19 @@ module Reckon
19
20
  return @amount
20
21
  end
21
22
 
23
+ def to_s
24
+ return @options[:raw] ? "#{@amount_raw} | #{@amount}" : @amount
25
+ end
26
+
27
+ # unary minus
28
+ # ex
29
+ # m = Money.new
30
+ # -m
22
31
  def -@
23
- Money.new( -@amount, :currency => @currency, :suffixed => @suffixed )
32
+ Money.new(-@amount, :currency => @currency, :suffixed => @suffixed)
24
33
  end
25
34
 
26
- def <=>( mon )
35
+ def <=>(mon)
27
36
  other_amount = mon.to_f
28
37
  if @amount < other_amount
29
38
  -1
@@ -34,7 +43,13 @@ module Reckon
34
43
  end
35
44
  end
36
45
 
37
- def pretty( negate = false )
46
+ def pretty(negate = false)
47
+ if @raw
48
+ return @amount_raw unless negate
49
+
50
+ return @amount_raw[0] == '-' ? @amount_raw[1..-1] : "-#{@amount_raw}"
51
+ end
52
+
38
53
  if @suffixed
39
54
  (@amount >= 0 ? " " : "") + sprintf("%0.2f #{@currency}", @amount * (negate ? -1 : 1))
40
55
  else
@@ -42,34 +57,20 @@ module Reckon
42
57
  end
43
58
  end
44
59
 
45
- def Money::from_s( value, options = {} )
60
+ def parse(value, options = {})
61
+ value = value.to_s
46
62
  # Empty string is treated as money with value 0
47
- return Money.new( 0.00, options ) if value.empty?
48
-
49
- # Remove 1000 separaters and replace , with . if comma_separates_cents
50
- # 1.000,00 -> 1000.00
51
- value = value.gsub(/\./, '').gsub(/,/, '.') if options[:comma_separates_cents]
52
- value = value.gsub(/,/, '')
53
-
54
- money_format_regex = /^(.*?)(\d+\.\d\d)/ # Money has two decimal precision
55
- any_number_regex = /^(.*?)([\d\.]+)/
56
-
57
- # Prefer matching the money_format, match any number otherwise
58
- m = value.match( money_format_regex ) ||
59
- value.match( any_number_regex )
60
- if m
61
- amount = m[2].to_f
62
- # Check whether the money had a - or (, which indicates negative amounts
63
- if (m[1].match( /^[\(-]/ ) || m[1].match( /-$/ ))
64
- amount *= -1
65
- end
66
- return Money.new( amount, options )
67
- else
68
- return nil
69
- end
63
+ return value.to_f if value.to_s.empty?
64
+
65
+ invert = value.match(/^\(.*\)$/)
66
+ value = value.gsub(/[^0-9,.-]/, '')
67
+ value = value.tr('.', '').tr(',', '.') if options[:comma_separates_cents]
68
+ value = value.tr(',', '')
69
+ value = value.to_f
70
+ return invert ? -value : value
70
71
  end
71
72
 
72
- def Money::likelihood( entry )
73
+ def Money::likelihood(entry)
73
74
  money_score = 0
74
75
  # digits separated by , or . with no more than 2 trailing digits
75
76
  money_score += 40 if entry.match(/\d+[,.]\d{2}[^\d]*$/)
@@ -83,31 +84,30 @@ module Reckon
83
84
  end
84
85
 
85
86
  class MoneyColumn < Array
86
- def initialize( arr = [], options = {} )
87
- arr.each { |str| self.push( Money.from_s( str, options ) ) }
87
+ def initialize(arr = [], options = {})
88
+ arr.each { |str| push(Money.new(str, options)) }
88
89
  end
89
90
 
90
91
  def positive?
91
- self.each do |money|
92
- return false if money < 0 if money
92
+ each do |money|
93
+ return false if money && money < 0
93
94
  end
94
95
  true
95
96
  end
96
97
 
97
- def merge!( other_column )
98
+ def merge!(other_column)
98
99
  invert = false
99
- invert = true if self.positive? && other_column.positive?
100
- self.each_with_index do |mon, i|
100
+ invert = true if positive? && other_column.positive?
101
+ each_with_index do |mon, i|
101
102
  other = other_column[i]
102
- return nil if (!mon || !other)
103
- if mon != 0.00 && other == 0.0
104
- if invert
105
- self[i]= -mon
106
- end
107
- elsif mon == 0.00 && other != 0.00
103
+ return nil if !mon || !other
104
+
105
+ if mon != 0.0 && other == 0.0
106
+ self[i] = -mon if invert
107
+ elsif mon == 0.0 && other != 0.0
108
108
  self[i] = other
109
109
  else
110
- return nil
110
+ self[i] = Money.new(0)
111
111
  end
112
112
  end
113
113
  self
@@ -1,3 +1,3 @@
1
1
  module Reckon
2
- VERSION = "0.5.2"
2
+ VERSION="0.6.2"
3
3
  end
@@ -13,7 +13,7 @@ Gem::Specification.new do |s|
13
13
 
14
14
  s.files = `git ls-files`.split("\n")
15
15
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
- s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
16
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
17
17
  s.require_paths = ["lib"]
18
18
 
19
19
  s.add_development_dependency "rspec", ">= 1.2.9"
@@ -21,6 +21,5 @@ Gem::Specification.new do |s|
21
21
  s.add_development_dependency "rantly", "= 1.2.0"
22
22
  s.add_runtime_dependency "chronic", ">= 0.3.0"
23
23
  s.add_runtime_dependency "highline", ">= 1.5.2"
24
- s.add_runtime_dependency "terminal-table", ">= 1.4.2"
25
24
  s.add_runtime_dependency "rchardet", ">= 1.8.0"
26
25
  end