reckon 0.8.1 → 0.9.1
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.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +1 -1
- data/.gitignore +5 -0
- data/.rubocop.yml +20 -0
- data/CHANGELOG.md +22 -0
- data/Gemfile.lock +21 -21
- data/README.md +2 -0
- data/Rakefile +2 -2
- data/bin/build-new-version.sh +3 -2
- data/bin/reckon +1 -1
- data/lib/reckon/app.rb +27 -24
- data/lib/reckon/beancount_parser.rb +150 -0
- data/lib/reckon/cosine_similarity.rb +0 -1
- data/lib/reckon/csv_parser.rb +89 -44
- data/lib/reckon/date_column.rb +18 -7
- data/lib/reckon/ledger_parser.rb +23 -15
- data/lib/reckon/money.rb +18 -16
- data/lib/reckon/options.rb +47 -18
- data/lib/reckon/version.rb +1 -1
- data/lib/reckon.rb +1 -0
- data/spec/cosine_training_and_test.rb +1 -1
- data/spec/data_fixtures/multi-line-field.csv +5 -0
- data/spec/integration/ask_for_account/cli_input.txt +1 -0
- data/spec/integration/invalid_header_example/output.ledger +6 -7
- data/spec/integration/invalid_header_example/test_args +1 -1
- data/spec/integration/tab_delimited_file/input.csv +2 -0
- data/spec/integration/tab_delimited_file/output.ledger +8 -0
- data/spec/integration/tab_delimited_file/test_args +1 -0
- data/spec/integration/test.sh +3 -5
- data/spec/integration/two_money_columns_manual/input.csv +5 -0
- data/spec/integration/two_money_columns_manual/output.ledger +16 -0
- data/spec/integration/two_money_columns_manual/test_args +1 -0
- data/spec/reckon/csv_parser_spec.rb +85 -26
- data/spec/reckon/date_column_spec.rb +6 -0
- data/spec/reckon/ledger_parser_spec.rb +25 -23
- data/spec/reckon/options_spec.rb +2 -2
- data/spec/spec_helper.rb +2 -0
- metadata +17 -141
- data/spec/integration/ask_for_account/cli_input.exp +0 -33
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 03c20b48d4333969c8304a5bb9a3c01fc6053050ab9146329ce14ae6a9886b38
|
4
|
+
data.tar.gz: 27a2ce4e8db5c7818cc4cefb19f180a7c727190f0a990403f565fad503e749a9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2f569b3d5cf4038714065a6d184d6c07f57d10598e5efc610eeb9919e8b18c65aff5e5329ab89a9ed30f72cabce9d11f5645af4d0df3bda6d05ad9afd988f7e7
|
7
|
+
data.tar.gz: 1783a63ba138c2b87a0756d6b9bcfbce068daf977e582a4c920a37ff50358328f8514f308dbbf932ef5cc4111e9e52dadfaed5876b9d30f4759d4a1eb31299fa
|
data/.github/workflows/ruby.yml
CHANGED
@@ -31,7 +31,7 @@ jobs:
|
|
31
31
|
- name: Update package
|
32
32
|
run: sudo apt-get update
|
33
33
|
- name: Install packages
|
34
|
-
run: sudo apt-get -y install ledger hledger
|
34
|
+
run: sudo apt-get -y install ledger hledger
|
35
35
|
- name: Set up Ruby
|
36
36
|
# To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
|
37
37
|
# change this to (see https://github.com/ruby/setup-ruby#versioning):
|
data/.gitignore
CHANGED
data/.rubocop.yml
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Layout/LineLength:
|
2
|
+
Max: 88
|
3
|
+
|
4
|
+
Style/StringLiterals:
|
5
|
+
Enabled: false
|
6
|
+
|
7
|
+
Style/RedundantReturn:
|
8
|
+
Enabled: false
|
9
|
+
|
10
|
+
Metrics/ClassLength:
|
11
|
+
Enabled: False
|
12
|
+
|
13
|
+
Metrics/MethodLength:
|
14
|
+
Enabled: False
|
15
|
+
|
16
|
+
Metrics/AbcSize:
|
17
|
+
Enabled: False
|
18
|
+
|
19
|
+
Style/NumericPredicate:
|
20
|
+
Enabled: False
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,27 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [v0.9.1](https://github.com/cantino/reckon/tree/v0.9.1) (2023-03-19)
|
4
|
+
|
5
|
+
[Full Changelog](https://github.com/cantino/reckon/compare/v0.9.0...v0.9.1)
|
6
|
+
|
7
|
+
**Closed issues:**
|
8
|
+
|
9
|
+
- More than one column support [\#120](https://github.com/cantino/reckon/issues/120)
|
10
|
+
- Beancount support [\#119](https://github.com/cantino/reckon/issues/119)
|
11
|
+
- Problem with importing CSV [\#60](https://github.com/cantino/reckon/issues/60)
|
12
|
+
|
13
|
+
## [v0.9.0](https://github.com/cantino/reckon/tree/v0.9.0) (2023-02-23)
|
14
|
+
|
15
|
+
[Full Changelog](https://github.com/cantino/reckon/compare/v0.9.0-beta...v0.9.0)
|
16
|
+
|
17
|
+
**Merged pull requests:**
|
18
|
+
|
19
|
+
- Add support for multiple money columns [\#118](https://github.com/cantino/reckon/pull/118) ([oskarth](https://github.com/oskarth))
|
20
|
+
|
21
|
+
## [v0.9.0-beta](https://github.com/cantino/reckon/tree/v0.9.0-beta) (2023-02-21)
|
22
|
+
|
23
|
+
[Full Changelog](https://github.com/cantino/reckon/compare/v0.8.1...v0.9.0-beta)
|
24
|
+
|
3
25
|
## [v0.8.1](https://github.com/cantino/reckon/tree/v0.8.1) (2022-07-02)
|
4
26
|
|
5
27
|
[Full Changelog](https://github.com/cantino/reckon/compare/v0.8.0...v0.8.1)
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
reckon (0.
|
4
|
+
reckon (0.9.1)
|
5
5
|
chronic (>= 0.3.0)
|
6
6
|
highline (>= 1.5.2)
|
7
7
|
matrix (>= 0.4.2)
|
@@ -11,30 +11,30 @@ GEM
|
|
11
11
|
remote: http://rubygems.org/
|
12
12
|
specs:
|
13
13
|
chronic (0.10.2)
|
14
|
-
coderay (1.1.
|
15
|
-
diff-lcs (1.
|
16
|
-
highline (2.0
|
14
|
+
coderay (1.1.3)
|
15
|
+
diff-lcs (1.5.0)
|
16
|
+
highline (2.1.0)
|
17
17
|
matrix (0.4.2)
|
18
|
-
method_source (0.
|
19
|
-
pry (0.
|
20
|
-
coderay (~> 1.1
|
21
|
-
method_source (~>
|
22
|
-
rake (
|
18
|
+
method_source (1.0.0)
|
19
|
+
pry (0.14.2)
|
20
|
+
coderay (~> 1.1)
|
21
|
+
method_source (~> 1.0)
|
22
|
+
rake (13.0.6)
|
23
23
|
rantly (1.2.0)
|
24
24
|
rchardet (1.8.0)
|
25
|
-
rspec (3.
|
26
|
-
rspec-core (~> 3.
|
27
|
-
rspec-expectations (~> 3.
|
28
|
-
rspec-mocks (~> 3.
|
29
|
-
rspec-core (3.
|
30
|
-
rspec-support (~> 3.
|
31
|
-
rspec-expectations (3.
|
25
|
+
rspec (3.12.0)
|
26
|
+
rspec-core (~> 3.12.0)
|
27
|
+
rspec-expectations (~> 3.12.0)
|
28
|
+
rspec-mocks (~> 3.12.0)
|
29
|
+
rspec-core (3.12.1)
|
30
|
+
rspec-support (~> 3.12.0)
|
31
|
+
rspec-expectations (3.12.2)
|
32
32
|
diff-lcs (>= 1.2.0, < 2.0)
|
33
|
-
rspec-support (~> 3.
|
34
|
-
rspec-mocks (3.
|
33
|
+
rspec-support (~> 3.12.0)
|
34
|
+
rspec-mocks (3.12.3)
|
35
35
|
diff-lcs (>= 1.2.0, < 2.0)
|
36
|
-
rspec-support (~> 3.
|
37
|
-
rspec-support (3.
|
36
|
+
rspec-support (~> 3.12.0)
|
37
|
+
rspec-support (3.12.0)
|
38
38
|
|
39
39
|
PLATFORMS
|
40
40
|
ruby
|
@@ -47,4 +47,4 @@ DEPENDENCIES
|
|
47
47
|
rspec (>= 1.2.9)
|
48
48
|
|
49
49
|
BUNDLED WITH
|
50
|
-
|
50
|
+
2.3.5
|
data/README.md
CHANGED
@@ -45,6 +45,8 @@ Learn more:
|
|
45
45
|
Columns to ignore, starts from 1
|
46
46
|
--money-column 2
|
47
47
|
Column number of the money column, starts from 1
|
48
|
+
--money-columns 2,3
|
49
|
+
Column number of the money columns, starts from 1 (1 or 2 columns)
|
48
50
|
--raw-money
|
49
51
|
Don't format money column (for stocks)
|
50
52
|
--date-column 3
|
data/Rakefile
CHANGED
@@ -13,10 +13,10 @@ task :test_all do
|
|
13
13
|
puts "Running unit tests"
|
14
14
|
Rake::Task["spec"].invoke
|
15
15
|
puts "Running integration tests"
|
16
|
-
Rake::Task["
|
16
|
+
Rake::Task["test_integration"].invoke
|
17
17
|
end
|
18
18
|
|
19
|
-
task :
|
19
|
+
task :test_integration do
|
20
20
|
cmd = 'prove -v ./spec/integration/test.sh'
|
21
21
|
raise 'Integration tests failed' unless system(cmd)
|
22
22
|
end
|
data/bin/build-new-version.sh
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
#!/bin/bash
|
2
2
|
|
3
|
-
set -
|
3
|
+
set -xe
|
4
4
|
|
5
5
|
VERSION=$1
|
6
6
|
|
@@ -8,7 +8,7 @@ echo "Install github_changelog_generator"
|
|
8
8
|
gem install --user github_changelog_generator
|
9
9
|
|
10
10
|
echo "Update 'lib/reckon/version.rb'"
|
11
|
-
echo -e "module Reckon\n VERSION
|
11
|
+
echo -e "module Reckon\n VERSION = \"$VERSION\"\nend" > lib/reckon/version.rb
|
12
12
|
echo "Run `bundle install` to build updated Gemfile.lock"
|
13
13
|
bundle install
|
14
14
|
echo "Run changelog generator (requires $TOKEN to be your github token)"
|
@@ -24,3 +24,4 @@ echo "Push changes and tags"
|
|
24
24
|
echo "git push && git push --tags"
|
25
25
|
echo "Push new gem"
|
26
26
|
echo "gem push reckon-$VERSION.gem"
|
27
|
+
gh release create v$VERSION reckon-$VERSION.gem --draft --generate-notes
|
data/bin/reckon
CHANGED
data/lib/reckon/app.rb
CHANGED
@@ -1,12 +1,12 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'pp'
|
4
3
|
require 'yaml'
|
4
|
+
require 'stringio'
|
5
5
|
|
6
6
|
module Reckon
|
7
|
+
# The main app
|
7
8
|
class App
|
8
9
|
attr_accessor :options, :seen, :csv_parser, :regexps, :matcher
|
9
|
-
@@cli = HighLine.new
|
10
10
|
|
11
11
|
def initialize(opts = {})
|
12
12
|
self.options = opts
|
@@ -14,9 +14,10 @@ module Reckon
|
|
14
14
|
|
15
15
|
self.regexps = {}
|
16
16
|
self.seen = Set.new
|
17
|
-
|
18
|
-
@csv_parser = CSVParser.new(
|
17
|
+
@cli = HighLine.new
|
18
|
+
@csv_parser = CSVParser.new(options)
|
19
19
|
@matcher = CosineSimilarity.new(options)
|
20
|
+
@parser = options[:format] =~ /beancount/i ? BeancountParser.new : LedgerParser.new
|
20
21
|
learn!
|
21
22
|
end
|
22
23
|
|
@@ -26,9 +27,13 @@ module Reckon
|
|
26
27
|
fh.puts str
|
27
28
|
end
|
28
29
|
|
30
|
+
# Learn from previous transactions. Used to recommend accounts for a transaction.
|
29
31
|
def learn!
|
30
32
|
learn_from_account_tokens(options[:account_tokens_file])
|
31
33
|
learn_from_ledger_file(options[:existing_ledger_file])
|
34
|
+
# TODO: make this work
|
35
|
+
# this doesn't work because output_file is an IO object
|
36
|
+
# learn_from_ledger_file(options[:output_file]) if File.exist?(options[:output_file])
|
32
37
|
end
|
33
38
|
|
34
39
|
def learn_from_account_tokens(filename)
|
@@ -52,12 +57,13 @@ module Reckon
|
|
52
57
|
|
53
58
|
raise "#{ledger_file} doesn't exist!" unless File.exist?(ledger_file)
|
54
59
|
|
55
|
-
learn_from_ledger(File.
|
60
|
+
learn_from_ledger(File.new(ledger_file))
|
56
61
|
end
|
57
62
|
|
63
|
+
# Takes an IO-like object
|
58
64
|
def learn_from_ledger(ledger)
|
59
65
|
LOGGER.info "learning from #{ledger}"
|
60
|
-
|
66
|
+
@parser.parse(ledger).each do |entry|
|
61
67
|
entry[:accounts].each do |account|
|
62
68
|
str = [entry[:desc], account[:amount]].join(" ")
|
63
69
|
if account[:name] != options[:bank_account]
|
@@ -84,7 +90,7 @@ module Reckon
|
|
84
90
|
merged_acct = [account, k].compact.join(':')
|
85
91
|
extract_account_tokens(v, merged_acct)
|
86
92
|
end
|
87
|
-
at.inject({}) { |memo, e| memo.merge!(e)}
|
93
|
+
at.inject({}) { |memo, e| memo.merge!(e) }
|
88
94
|
end
|
89
95
|
end
|
90
96
|
|
@@ -92,6 +98,7 @@ module Reckon
|
|
92
98
|
# https://github.com/tenderlove/psych/blob/master/lib/psych/visitors/to_ruby.rb
|
93
99
|
match = regex_str.match(/^\/(.*)\/([ix]*)$/m)
|
94
100
|
fail "failed to parse regexp #{regex_str}" unless match
|
101
|
+
|
95
102
|
options = 0
|
96
103
|
(match[2] || '').split('').each do |option|
|
97
104
|
case option
|
@@ -120,13 +127,16 @@ module Reckon
|
|
120
127
|
|
121
128
|
if row[:money] > 0
|
122
129
|
# out_of_account
|
123
|
-
answer = ask_account_question(
|
130
|
+
answer = ask_account_question(
|
131
|
+
"Which account provided this income? (#{cmd_options})", row
|
132
|
+
)
|
124
133
|
line1 = [options[:bank_account], row[:pretty_money]]
|
125
134
|
line2 = [answer, ""]
|
126
135
|
else
|
127
136
|
# into_account
|
128
|
-
answer = ask_account_question(
|
129
|
-
|
137
|
+
answer = ask_account_question(
|
138
|
+
"To which account did this money go? (#{cmd_options})", row
|
139
|
+
)
|
130
140
|
line1 = [answer, ""]
|
131
141
|
line2 = [options[:bank_account], row[:pretty_money]]
|
132
142
|
end
|
@@ -137,9 +147,9 @@ module Reckon
|
|
137
147
|
next
|
138
148
|
end
|
139
149
|
|
140
|
-
ledger =
|
150
|
+
ledger = @parser.format_row(row, line1, line2)
|
141
151
|
LOGGER.info "ledger line: #{ledger}"
|
142
|
-
learn_from_ledger(ledger) unless options[:account_tokens_file]
|
152
|
+
learn_from_ledger(StringIO.new(ledger)) unless options[:account_tokens_file]
|
143
153
|
output(ledger)
|
144
154
|
end
|
145
155
|
end
|
@@ -203,7 +213,7 @@ module Reckon
|
|
203
213
|
return possible_answers[0] || default
|
204
214
|
end
|
205
215
|
|
206
|
-
answer =
|
216
|
+
answer = @cli.ask(msg) do |q|
|
207
217
|
q.completion = possible_answers
|
208
218
|
q.readline = true
|
209
219
|
q.default = possible_answers.first
|
@@ -221,7 +231,7 @@ module Reckon
|
|
221
231
|
end
|
222
232
|
|
223
233
|
def add_description(row)
|
224
|
-
desc_answer =
|
234
|
+
desc_answer = @cli.ask("Enter a new description for this transaction (empty line aborts)\n") do |q|
|
225
235
|
q.overwrite = true
|
226
236
|
q.readline = true
|
227
237
|
q.default = row[:description]
|
@@ -231,7 +241,7 @@ module Reckon
|
|
231
241
|
end
|
232
242
|
|
233
243
|
def add_note(row)
|
234
|
-
desc_answer =
|
244
|
+
desc_answer = @cli.ask("Enter a new note for this transaction (empty line aborts)\n") do |q|
|
235
245
|
q.overwrite = true
|
236
246
|
q.readline = true
|
237
247
|
q.default = row[:note]
|
@@ -246,7 +256,7 @@ module Reckon
|
|
246
256
|
[account, match[0]]
|
247
257
|
end
|
248
258
|
}.compact
|
249
|
-
matches.sort_by
|
259
|
+
matches.sort_by { |_account, matched_text| matched_text.length }.map(&:first)
|
250
260
|
end
|
251
261
|
|
252
262
|
def suggest(row)
|
@@ -254,13 +264,6 @@ module Reckon
|
|
254
264
|
@matcher.find_similar(row[:description]).map { |n| n[:account] }
|
255
265
|
end
|
256
266
|
|
257
|
-
def ledger_format(row, line1, line2)
|
258
|
-
out = "#{row[:pretty_date]}\t#{row[:description]}#{row[:note] ? "\t; " + row[:note]: ""}\n"
|
259
|
-
out += "\t#{line1.first}\t\t\t#{line1.last}\n"
|
260
|
-
out += "\t#{line2.first}\t\t\t#{line2.last}\n\n"
|
261
|
-
out
|
262
|
-
end
|
263
|
-
|
264
267
|
def output(ledger_line)
|
265
268
|
options[:output_file].puts ledger_line
|
266
269
|
options[:output_file].flush
|
@@ -0,0 +1,150 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'date'
|
3
|
+
|
4
|
+
module Reckon
|
5
|
+
class BeancountParser
|
6
|
+
|
7
|
+
attr_accessor :entries
|
8
|
+
|
9
|
+
def initialize(options = {})
|
10
|
+
@options = options
|
11
|
+
@date_format = options[:ledger_date_format] || options[:date_format] || '%Y-%m-%d'
|
12
|
+
end
|
13
|
+
|
14
|
+
# 2015-01-01 * "Opening Balance for checking account"
|
15
|
+
# Assets:US:BofA:Checking 3490.52 USD
|
16
|
+
# Equity:Opening-Balances -3490.52 USD
|
17
|
+
|
18
|
+
# input is an object that response to #each_line,
|
19
|
+
# (i.e. a StringIO or an IO object)
|
20
|
+
def parse(input)
|
21
|
+
entries = []
|
22
|
+
comment_chars = ';#%*|'
|
23
|
+
new_entry = {}
|
24
|
+
|
25
|
+
input.each_line do |entry|
|
26
|
+
|
27
|
+
next if entry =~ /^\s*[#{comment_chars}]/
|
28
|
+
|
29
|
+
m = entry.match(%r{
|
30
|
+
^
|
31
|
+
(\d+[\d/-]+) # date
|
32
|
+
\s+
|
33
|
+
([*!])? # type
|
34
|
+
\s*
|
35
|
+
("[^"]*")? # description (optional)
|
36
|
+
\s*
|
37
|
+
("[^"]*")? # notes (optional)
|
38
|
+
# tags (not implemented)
|
39
|
+
}x)
|
40
|
+
|
41
|
+
# (date, type, code, description), type and code are optional
|
42
|
+
if (m)
|
43
|
+
add_entry(entries, new_entry)
|
44
|
+
new_entry = {
|
45
|
+
date: try_parse_date(m[1]),
|
46
|
+
type: m[2] || "",
|
47
|
+
desc: trim_quote(m[3]),
|
48
|
+
notes: trim_quote(m[4]),
|
49
|
+
accounts: []
|
50
|
+
}
|
51
|
+
elsif entry =~ /^\s*$/ && new_entry[:date]
|
52
|
+
add_entry(entries, new_entry)
|
53
|
+
new_entry = {}
|
54
|
+
elsif new_entry[:date] && entry =~ /^\s+/
|
55
|
+
LOGGER.info("Adding new account #{entry}")
|
56
|
+
new_entry[:accounts] << parse_account_line(entry)
|
57
|
+
else
|
58
|
+
LOGGER.info("Unknown entry type: #{entry}")
|
59
|
+
add_entry(entries, new_entry)
|
60
|
+
new_entry = {}
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
entries
|
65
|
+
end
|
66
|
+
|
67
|
+
def format_row(row, line1, line2)
|
68
|
+
out = %Q{#{row[:pretty_date]} * "#{row[:description]}" "#{row[:note]}\n}
|
69
|
+
out += "\t#{line1.first}\t\t\t#{line1.last}\n"
|
70
|
+
out += "\t#{line2.first}\t\t\t#{line2.last}\n\n"
|
71
|
+
out
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
# remove leading and trailing quote character (")
|
77
|
+
def trim_quote(str)
|
78
|
+
return str if !str
|
79
|
+
str.gsub(/^"([^"]*)"$/, '\1')
|
80
|
+
end
|
81
|
+
|
82
|
+
def add_entry(entries, entry)
|
83
|
+
return unless entry[:date] && entry[:accounts].length > 1
|
84
|
+
|
85
|
+
entry[:accounts] = balance(entry[:accounts])
|
86
|
+
entries << entry
|
87
|
+
end
|
88
|
+
|
89
|
+
def try_parse_date(date_str)
|
90
|
+
date = Date.parse(date_str)
|
91
|
+
return nil if date.year > 9999 || date.year < 1000
|
92
|
+
|
93
|
+
date
|
94
|
+
rescue ArgumentError
|
95
|
+
nil
|
96
|
+
end
|
97
|
+
|
98
|
+
def parse_account_line(entry)
|
99
|
+
# TODO handle buying stocks
|
100
|
+
# Assets:US:ETrade:VHT 19 VHT {132.32 USD, 2017-08-27}
|
101
|
+
(account_name, rest) = entry.strip.split(/\s{2,}|\t+/, 2)
|
102
|
+
|
103
|
+
if rest.nil? || rest.empty?
|
104
|
+
return {
|
105
|
+
name: account_name,
|
106
|
+
amount: clean_money("")
|
107
|
+
}
|
108
|
+
end
|
109
|
+
|
110
|
+
value = if rest =~ /{/
|
111
|
+
(qty, dollar_value, date) = rest.split(/[{,]/)
|
112
|
+
(qty.to_f * dollar_value.to_f).to_s
|
113
|
+
else
|
114
|
+
rest
|
115
|
+
end
|
116
|
+
|
117
|
+
return {
|
118
|
+
name: account_name,
|
119
|
+
amount: clean_money(value || "")
|
120
|
+
}
|
121
|
+
end
|
122
|
+
|
123
|
+
def balance(accounts)
|
124
|
+
return accounts unless accounts.any? { |i| i[:amount].nil? }
|
125
|
+
|
126
|
+
sum = accounts.reduce(0) { |m, n| m + (n[:amount] || 0) }
|
127
|
+
count = 0
|
128
|
+
accounts.each do |account|
|
129
|
+
next unless account[:amount].nil?
|
130
|
+
|
131
|
+
count += 1
|
132
|
+
account[:amount] = -sum
|
133
|
+
end
|
134
|
+
if count > 1
|
135
|
+
puts "Warning: unparsable entry due to more than one missing money value."
|
136
|
+
p accounts
|
137
|
+
puts
|
138
|
+
end
|
139
|
+
|
140
|
+
accounts
|
141
|
+
end
|
142
|
+
|
143
|
+
def clean_money(money)
|
144
|
+
return nil if money.nil? || money.empty?
|
145
|
+
|
146
|
+
money.gsub(/[^0-9.-]/, '').to_f
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|