reckon 0.9.5 → 0.10.0
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/.rubocop.yml +12 -0
- data/CHANGELOG.md +17 -0
- data/Gemfile.lock +1 -1
- data/README.md +2 -0
- data/Rakefile +2 -0
- data/bin/build-new-version.sh +2 -1
- data/lib/reckon/app.rb +33 -7
- data/lib/reckon/cosine_similarity.rb +1 -0
- data/lib/reckon/csv_parser.rb +9 -8
- data/lib/reckon/ledger_parser.rb +5 -5
- data/lib/reckon/logger.rb +4 -0
- data/lib/reckon/options.rb +39 -9
- data/lib/reckon/version.rb +1 -1
- data/spec/cosine_training_and_test.rb +2 -1
- data/spec/integration/ask_for_account/expected_output +10 -10
- data/spec/reckon/ledger_parser_spec.rb +106 -78
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9b0141e2c8428b74039967461cb7db1b06fd4acd63edb2db8921ec2c9a2895ec
|
4
|
+
data.tar.gz: 82c6d558f8e030988114db22d9ad59f580c7275a10a019b68c02355ea6639c71
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ec2519c7bd438012ae498b4e3a9517d6c63ae3de71bf7237043bdbeb60c589a88180a9171f0bcb5b25f1e7dda35c7f6eb9d99173d6c9905e2bc0913fe244cca6
|
7
|
+
data.tar.gz: 8f3ea58e38dc864cdcbb10b8ecb50f214f0dfa903eef6029b9c48057807d6910de3928b61c7bc4e14051dd9052f01fafcefab9bffca8dc107d516f17e5e4a427
|
data/.rubocop.yml
CHANGED
@@ -18,3 +18,15 @@ Metrics/AbcSize:
|
|
18
18
|
|
19
19
|
Style/NumericPredicate:
|
20
20
|
Enabled: False
|
21
|
+
|
22
|
+
Metrics/PerceivedComplexity:
|
23
|
+
Enabled: False
|
24
|
+
|
25
|
+
Metrics/CyclomaticComplexity:
|
26
|
+
Enabled: False
|
27
|
+
|
28
|
+
Style/FormatString:
|
29
|
+
Enabled: False
|
30
|
+
|
31
|
+
Naming/MethodParameterName:
|
32
|
+
Enabled: False
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,22 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [v0.10.0](https://github.com/cantino/reckon/tree/v0.10.0) (2024-11-27)
|
4
|
+
|
5
|
+
[Full Changelog](https://github.com/cantino/reckon/compare/v0.9.6...v0.10.0)
|
6
|
+
|
7
|
+
**Closed issues:**
|
8
|
+
|
9
|
+
- Is it possible to use reckon with only no info about incoming or going out? [\#131](https://github.com/cantino/reckon/issues/131)
|
10
|
+
- Reckon fails immediately with error about uninitialized constant `Readline` [\#129](https://github.com/cantino/reckon/issues/129)
|
11
|
+
|
12
|
+
## [v0.9.6](https://github.com/cantino/reckon/tree/v0.9.6) (2024-03-27)
|
13
|
+
|
14
|
+
[Full Changelog](https://github.com/cantino/reckon/compare/v0.9.5...v0.9.6)
|
15
|
+
|
16
|
+
**Closed issues:**
|
17
|
+
|
18
|
+
- reckon can't learn from a file with the "wrong" date format [\#130](https://github.com/cantino/reckon/issues/130)
|
19
|
+
|
3
20
|
## [v0.9.5](https://github.com/cantino/reckon/tree/v0.9.5) (2024-01-08)
|
4
21
|
|
5
22
|
[Full Changelog](https://github.com/cantino/reckon/compare/v0.9.4...v0.9.5)
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -49,6 +49,8 @@ Learn more:
|
|
49
49
|
Column number of the money columns, starts from 1 (1 or 2 columns)
|
50
50
|
--raw-money
|
51
51
|
Don't format money column (for stocks)
|
52
|
+
--sort DATE|DESC|AMT
|
53
|
+
Sort file by date, description, or amount
|
52
54
|
--date-column 3
|
53
55
|
Column number of the date column, starts from 1
|
54
56
|
--contains-header [N]
|
data/Rakefile
CHANGED
@@ -8,6 +8,7 @@ RSpec::Core::RakeTask.new(:spec)
|
|
8
8
|
|
9
9
|
task default: :spec
|
10
10
|
|
11
|
+
desc "Run specs and integration tests"
|
11
12
|
task :test_all do
|
12
13
|
puts "#{`ledger --version |head -n1`}"
|
13
14
|
puts "Running unit tests"
|
@@ -16,6 +17,7 @@ task :test_all do
|
|
16
17
|
Rake::Task["test_integration"].invoke
|
17
18
|
end
|
18
19
|
|
20
|
+
desc "Run integration tests"
|
19
21
|
task :test_integration do
|
20
22
|
cmd = 'prove -v ./spec/integration/test.sh'
|
21
23
|
raise 'Integration tests failed' unless system(cmd)
|
data/bin/build-new-version.sh
CHANGED
@@ -28,5 +28,6 @@ gem build reckon.gemspec
|
|
28
28
|
echo "Push changes and tags"
|
29
29
|
echo "git push && git push --tags"
|
30
30
|
echo "Push new gem"
|
31
|
-
echo "gem push reckon-$VERSION.gem"
|
31
|
+
echo "gem push reckon-$VERSION.gem --otp (ykman oath accounts code -s rubygems.org)"
|
32
|
+
echo "Publish draft github release"
|
32
33
|
gh release create "v$VERSION" "reckon-$VERSION.gem" --draft --generate-notes
|
data/lib/reckon/app.rb
CHANGED
@@ -10,10 +10,11 @@ module Reckon
|
|
10
10
|
|
11
11
|
def initialize(opts = {})
|
12
12
|
self.options = opts
|
13
|
-
LOGGER.level =
|
13
|
+
LOGGER.level = options[:verbose] || Logger::WARN
|
14
14
|
|
15
15
|
self.regexps = {}
|
16
16
|
self.seen = Set.new
|
17
|
+
options[:sort] ||= :date
|
17
18
|
@cli = HighLine.new
|
18
19
|
@csv_parser = CSVParser.new(options)
|
19
20
|
@matcher = CosineSimilarity.new(options)
|
@@ -80,7 +81,7 @@ module Reckon
|
|
80
81
|
|
81
82
|
# Add tokens from account_tokens_file to accounts
|
82
83
|
def extract_account_tokens(subtree, account = nil)
|
83
|
-
if subtree.nil?
|
84
|
+
if subtree.nil? || !subtree
|
84
85
|
puts "Warning: empty #{account} tree"
|
85
86
|
{}
|
86
87
|
elsif subtree.is_a?(Array)
|
@@ -141,6 +142,11 @@ module Reckon
|
|
141
142
|
line2 = [options[:bank_account], row[:pretty_money]]
|
142
143
|
end
|
143
144
|
|
145
|
+
if answer == '~~SKIP~~'
|
146
|
+
LOGGER.info "skipping transaction: #{row}"
|
147
|
+
next
|
148
|
+
end
|
149
|
+
|
144
150
|
finish if %w[quit q].include?(answer)
|
145
151
|
if %w[skip s].include?(answer)
|
146
152
|
interactive_output "Skipping"
|
@@ -168,18 +174,29 @@ module Reckon
|
|
168
174
|
:money => @csv_parser.money_for(index),
|
169
175
|
:description => @csv_parser.description_for(index) }
|
170
176
|
end
|
171
|
-
rows.sort_by
|
177
|
+
rows.sort_by do |n|
|
178
|
+
[n[options[:sort]], -n[:money], n[:description]]
|
179
|
+
end.each do |row|
|
180
|
+
yield row
|
181
|
+
end
|
172
182
|
end
|
173
183
|
|
174
184
|
def print_transaction(rows, fh = $stdout)
|
175
185
|
str = "\n"
|
176
|
-
header = %w[Date Amount Description
|
186
|
+
header = %w[Date Amount Description]
|
187
|
+
header += ["Note"] if rows.map { |r| r[:note] }.any?
|
177
188
|
maxes = header.map(&:length)
|
178
|
-
|
179
|
-
|
189
|
+
rows = rows.map do |r|
|
190
|
+
[r[:pretty_date], r[:pretty_money], r[:description], r[:note]].compact
|
191
|
+
end
|
180
192
|
|
181
193
|
rows.each do |r|
|
182
|
-
r.length.times
|
194
|
+
r.length.times do |i|
|
195
|
+
l = 0
|
196
|
+
l = r[i].length if r[i]
|
197
|
+
maxes[i] ||= 0
|
198
|
+
maxes[i] = l if maxes[i] < l
|
199
|
+
end
|
183
200
|
end
|
184
201
|
|
185
202
|
header.each_with_index do |n, i|
|
@@ -199,6 +216,15 @@ module Reckon
|
|
199
216
|
end
|
200
217
|
|
201
218
|
def ask_account_question(msg, row)
|
219
|
+
# return account token if it matches
|
220
|
+
token_answer = most_specific_regexp_match(row)
|
221
|
+
if token_answer.any?
|
222
|
+
row[:note] = "Matched account token"
|
223
|
+
puts "NOTE: Matched account token"
|
224
|
+
puts token_answer[0]
|
225
|
+
return token_answer[0]
|
226
|
+
end
|
227
|
+
|
202
228
|
possible_answers = suggest(row)
|
203
229
|
LOGGER.info "possible_answers===> #{possible_answers.inspect}"
|
204
230
|
|
data/lib/reckon/csv_parser.rb
CHANGED
@@ -64,13 +64,13 @@ module Reckon
|
|
64
64
|
private
|
65
65
|
|
66
66
|
def filter_csv
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
@columns = new_columns
|
67
|
+
return unless options[:ignore_columns]
|
68
|
+
|
69
|
+
new_columns = []
|
70
|
+
columns.each_with_index do |column, index|
|
71
|
+
new_columns << (options[:ignore_columns].include?(index + 1) ? [''] * column.length : column)
|
73
72
|
end
|
73
|
+
@columns = new_columns
|
74
74
|
end
|
75
75
|
|
76
76
|
def evaluate_columns(cols)
|
@@ -222,7 +222,8 @@ module Reckon
|
|
222
222
|
# convert to a stringio object to handle multi-line fields
|
223
223
|
parser_opts = {
|
224
224
|
col_sep: separator,
|
225
|
-
skip_blanks: true
|
225
|
+
skip_blanks: true,
|
226
|
+
row_sep: :auto
|
226
227
|
}
|
227
228
|
begin
|
228
229
|
rows = CSV.parse(StringIO.new(data), **parser_opts)
|
@@ -235,7 +236,7 @@ module Reckon
|
|
235
236
|
index = data.index("\n", index) + 1 # skip over newline character
|
236
237
|
count += 1
|
237
238
|
end
|
238
|
-
rows = CSV.parse(StringIO.new(data[index
|
239
|
+
rows = CSV.parse(StringIO.new(data[index..]), **parser_opts)
|
239
240
|
rows[0..-footer_lines_to_skip]
|
240
241
|
end
|
241
242
|
end
|
data/lib/reckon/ledger_parser.rb
CHANGED
@@ -107,8 +107,8 @@
|
|
107
107
|
require 'rubygems'
|
108
108
|
|
109
109
|
module Reckon
|
110
|
+
# Parses ledger files
|
110
111
|
class LedgerParser
|
111
|
-
|
112
112
|
# ledger is an object that response to #each_line,
|
113
113
|
# (i.e. a StringIO or an IO object)
|
114
114
|
def initialize(options = {})
|
@@ -130,7 +130,7 @@ module Reckon
|
|
130
130
|
next if entry =~ /^\s*[#{comment_chars}]/
|
131
131
|
|
132
132
|
# (date, type, code, description), type and code are optional
|
133
|
-
if (m = entry.match(%r{^(\d+[
|
133
|
+
if (m = entry.match(%r{^(\d+[^\s]+)\s+([*!])?\s*(\([^)]+\))?\s*(.*)$}))
|
134
134
|
add_entry(entries, new_entry)
|
135
135
|
new_entry = {
|
136
136
|
date: try_parse_date(m[1]),
|
@@ -140,7 +140,7 @@ module Reckon
|
|
140
140
|
accounts: []
|
141
141
|
}
|
142
142
|
elsif entry =~ /^\s*$/ && new_entry[:date]
|
143
|
-
add_entry(entries,new_entry)
|
143
|
+
add_entry(entries, new_entry)
|
144
144
|
new_entry = {}
|
145
145
|
elsif new_entry[:date] && entry =~ /^\s+/
|
146
146
|
LOGGER.info("Adding new account #{entry}")
|
@@ -175,13 +175,13 @@ module Reckon
|
|
175
175
|
end
|
176
176
|
|
177
177
|
def format_row(row, line1, line2)
|
178
|
-
|
178
|
+
note = row[:note] ? "\t; #{row[:note]}" : ""
|
179
|
+
out = "#{row[:pretty_date]}\t#{row[:description]}#{note}\n"
|
179
180
|
out += "\t#{line1.first}\t\t\t#{line1.last}\n"
|
180
181
|
out += "\t#{line2.first}\t\t\t#{line2.last}\n\n"
|
181
182
|
out
|
182
183
|
end
|
183
184
|
|
184
|
-
|
185
185
|
private
|
186
186
|
|
187
187
|
def add_entry(entries, entry)
|
data/lib/reckon/logger.rb
CHANGED
data/lib/reckon/options.rb
CHANGED
@@ -4,7 +4,6 @@ module Reckon
|
|
4
4
|
# Singleton class for parsing command line flags
|
5
5
|
class Options
|
6
6
|
def self.parse_command_line_options(args = ARGV, stdin = $stdin)
|
7
|
-
cli = HighLine.new
|
8
7
|
options = { output_file: $stdout }
|
9
8
|
OptionParser.new do |opts|
|
10
9
|
opts.banner = "Usage: Reckon.rb [options]"
|
@@ -18,8 +17,17 @@ module Reckon
|
|
18
17
|
options[:bank_account] = a
|
19
18
|
end
|
20
19
|
|
21
|
-
|
22
|
-
|
20
|
+
options[:verbose] = Logger::WARN
|
21
|
+
opts.on("-v", "--v", "Run verbosely (show info log messages)") do
|
22
|
+
options[:verbose] = Logger::INFO
|
23
|
+
end
|
24
|
+
|
25
|
+
opts.on("", "--verbose", "Run verbosely (show info log messages)") do
|
26
|
+
options[:verbose] = Logger::INFO
|
27
|
+
end
|
28
|
+
|
29
|
+
opts.on("", "--vv", "Run very verbosely (show debug log messages)") do
|
30
|
+
options[:verbose] = Logger::DEBUG
|
23
31
|
end
|
24
32
|
|
25
33
|
opts.on("-i", "--inverse", "Use the negative of each amount") do |v|
|
@@ -58,6 +66,19 @@ module Reckon
|
|
58
66
|
options[:raw] = n
|
59
67
|
end
|
60
68
|
|
69
|
+
options[:sort] = :date
|
70
|
+
opts.on("", "--sort DATE|DESC|AMT", "Sort file by date, description, or amount") do |s|
|
71
|
+
if s == 'DESC'
|
72
|
+
options[:sort] = :description
|
73
|
+
elsif s == 'AMT'
|
74
|
+
options[:sort] = :money
|
75
|
+
elsif s == 'DATE'
|
76
|
+
options[:sort] = :date
|
77
|
+
else
|
78
|
+
raise "'#{s}' is not valid. valid sort options are DATE, DESC, AMT"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
61
82
|
opts.on("", "--date-column 3", Integer,
|
62
83
|
"Column number of the date column, starts from 1") do |col|
|
63
84
|
options[:date_column] = col
|
@@ -161,26 +182,35 @@ module Reckon
|
|
161
182
|
options[:string] = stdin.read
|
162
183
|
end
|
163
184
|
|
185
|
+
validate_options(options)
|
186
|
+
|
187
|
+
return options
|
188
|
+
end
|
189
|
+
|
190
|
+
def self.validate_options(options)
|
191
|
+
cli = HighLine.new
|
164
192
|
unless options[:file]
|
165
193
|
options[:file] = cli.ask("What CSV file should I parse? ")
|
166
|
-
|
167
|
-
puts "\
|
168
|
-
puts parser
|
194
|
+
if options[:file].empty?
|
195
|
+
puts "\nERROR: You must provide a CSV file to parse.\n"
|
169
196
|
exit
|
170
197
|
end
|
171
198
|
end
|
172
199
|
|
173
200
|
unless options[:bank_account]
|
174
|
-
|
201
|
+
if options[:unattended]
|
202
|
+
puts "ERROR: Must specify --account in unattended mode"
|
203
|
+
exit
|
204
|
+
end
|
175
205
|
|
176
|
-
options[:bank_account] = cli.ask("What is
|
206
|
+
options[:bank_account] = cli.ask("What is the Ledger account name?\n") do |q|
|
177
207
|
q.readline = true
|
178
208
|
q.validate = /^.{2,}$/
|
179
209
|
q.default = "Assets:Bank:Checking"
|
180
210
|
end
|
181
211
|
end
|
182
212
|
|
183
|
-
return
|
213
|
+
return true
|
184
214
|
end
|
185
215
|
end
|
186
216
|
end
|
data/lib/reckon/version.rb
CHANGED
@@ -8,7 +8,7 @@ ledger_file = ARGV[0]
|
|
8
8
|
account = ARGV[1]
|
9
9
|
seed = ARGV[2] ? ARGV[2].to_i : Random.new_seed
|
10
10
|
|
11
|
-
ledger = Reckon::LedgerParser.new(File.new(ledger_file))
|
11
|
+
ledger = Reckon::LedgerParser.new.parse(File.new(ledger_file))
|
12
12
|
matcher = Reckon::CosineSimilarity.new({})
|
13
13
|
|
14
14
|
train = []
|
@@ -50,3 +50,4 @@ end
|
|
50
50
|
# pp result.compact
|
51
51
|
puts "using #{seed} as random seed"
|
52
52
|
puts "true: #{result.count(nil)} false: #{result.count { |v| !v.nil? }}"
|
53
|
+
puts(result.filter { |v| !v.nil? })
|
@@ -1,11 +1,11 @@
|
|
1
1
|
|
2
|
-
Date | Amount | Description |
|
3
|
-
2003-12-24 | $2,105.00 | CREDIT; Some Company vendorpymt PPD ID: 5KL3832735 |
|
4
|
-
2004-12-24 | -$116.22 | CREDIT; PAYPAL TRANSFER PPD ID: PAYPALSDSL |
|
5
|
-
2005-12-24 | -$0.96 | DEBIT; WEBSITE-BALANCE-10DEC09 12 12/10WEBSITE-BAL |
|
6
|
-
2006-12-24 | $0.23 | DEBIT; WEBSITE-BALANCE-17DEC09 12 12/17WEBSITE-BAL |
|
7
|
-
2007-12-24 | $1,558.52 | CREDIT; Blarg BLARG REVENUE PPD ID: 00jah78563 |
|
8
|
-
2008-12-24 | $3,520.00 | CREDIT; Some Company vendorpymt PPD ID: 59728JSL20 |
|
9
|
-
2009-12-24 | -$7.00 | DEBIT; GITHUB 041287430274 CA 12/22GITHUB 04 |
|
10
|
-
2010-12-24 | -$20.00 | CHECK; CHECK 2656 |
|
11
|
-
2011-12-24 | -$85.00 | DEBIT; HOST 037196321563 MO 12/22SLICEHOST |
|
2
|
+
Date | Amount | Description |
|
3
|
+
2003-12-24 | $2,105.00 | CREDIT; Some Company vendorpymt PPD ID: 5KL3832735 |
|
4
|
+
2004-12-24 | -$116.22 | CREDIT; PAYPAL TRANSFER PPD ID: PAYPALSDSL |
|
5
|
+
2005-12-24 | -$0.96 | DEBIT; WEBSITE-BALANCE-10DEC09 12 12/10WEBSITE-BAL |
|
6
|
+
2006-12-24 | $0.23 | DEBIT; WEBSITE-BALANCE-17DEC09 12 12/17WEBSITE-BAL |
|
7
|
+
2007-12-24 | $1,558.52 | CREDIT; Blarg BLARG REVENUE PPD ID: 00jah78563 |
|
8
|
+
2008-12-24 | $3,520.00 | CREDIT; Some Company vendorpymt PPD ID: 59728JSL20 |
|
9
|
+
2009-12-24 | -$7.00 | DEBIT; GITHUB 041287430274 CA 12/22GITHUB 04 |
|
10
|
+
2010-12-24 | -$20.00 | CHECK; CHECK 2656 |
|
11
|
+
2011-12-24 | -$85.00 | DEBIT; HOST 037196321563 MO 12/22SLICEHOST |
|
@@ -1,5 +1,5 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
#encoding: utf-8
|
2
|
+
# encoding: utf-8
|
3
3
|
|
4
4
|
require_relative "../spec_helper"
|
5
5
|
require 'rubygems'
|
@@ -28,35 +28,42 @@ describe Reckon::LedgerParser do
|
|
28
28
|
property_of do
|
29
29
|
Rantly do
|
30
30
|
description = Proc.new do
|
31
|
-
sized(15)
|
31
|
+
sized(15) {
|
32
|
+
string
|
33
|
+
}.tr(%q{'`:*\\}, '').gsub(/\s+/, ' ').gsub(/^[!;<\[( #{comment_chars}]+/, '')
|
32
34
|
end
|
33
35
|
currency = choose(*currencies) # to be consistent within the transaction
|
34
|
-
single_line_comments = ";#|%*".split('').map { |n|
|
36
|
+
single_line_comments = ";#|%*".split('').map { |n|
|
37
|
+
"#{n} #{call(description)}"
|
38
|
+
}
|
35
39
|
comments = ['', '; ', "\t;#{call(description)}", " ; #{call(description)}"]
|
36
40
|
date = Time.at(range(0, 1_581_389_644)).strftime(choose(*formats))
|
37
41
|
codes = [' ', " (#{string(:alnum).tr('()', '')}) "]
|
38
42
|
account = Proc.new { choose(*delimiters) + call(description) }
|
39
43
|
account_money = Proc.new do
|
40
|
-
|
44
|
+
sprintf("%.02f", (float * range(5, 10) + 1) * choose(1, -1))
|
41
45
|
end
|
42
46
|
account_line = Proc.new do
|
43
47
|
call(account) + \
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
48
|
+
choose(*delimiters) + \
|
49
|
+
currency + \
|
50
|
+
choose(*currency_delimiters) + \
|
51
|
+
call(account_money) + \
|
52
|
+
choose(*comments)
|
49
53
|
end
|
50
54
|
ledger = "#{date}#{choose(*types)}#{choose(*codes)}#{call(description)}\n"
|
51
|
-
range(1,5).times do
|
55
|
+
range(1, 5).times do
|
52
56
|
ledger += "#{call(account_line)}\n"
|
53
57
|
end
|
54
58
|
ledger += "#{call(account)}\n"
|
55
59
|
ledger += choose(*single_line_comments) + "\n"
|
56
60
|
ledger
|
57
61
|
end
|
58
|
-
end.check(
|
59
|
-
filter_format = lambda { |n|
|
62
|
+
end.check(100) do |s|
|
63
|
+
filter_format = lambda { |n|
|
64
|
+
[n['date'], n['desc'], n['name'],
|
65
|
+
sprintf("%.02f", n['amount'])]
|
66
|
+
}
|
60
67
|
headers = %w[date code desc name currency amount type commend]
|
61
68
|
safe_s = Shellwords.escape(s)
|
62
69
|
|
@@ -64,7 +71,8 @@ describe Reckon::LedgerParser do
|
|
64
71
|
actual = CSV.parse(lp_csv, headers: headers).map(&filter_format)
|
65
72
|
|
66
73
|
ledger_csv = `echo #{safe_s} | ledger csv --date-format '%Y/%m/%d' -f - `
|
67
|
-
expected = CSV.parse(ledger_csv.gsub('\"', '""'),
|
74
|
+
expected = CSV.parse(ledger_csv.gsub('\"', '""'),
|
75
|
+
headers: headers).map(&filter_format)
|
68
76
|
expected.length.times do |i|
|
69
77
|
expect(actual[i]).to eq(expected[i])
|
70
78
|
end
|
@@ -72,34 +80,35 @@ describe Reckon::LedgerParser do
|
|
72
80
|
end
|
73
81
|
|
74
82
|
it 'should filter block comments' do
|
75
|
-
ledger =
|
76
|
-
1970/11/01 Dinner should show up
|
77
|
-
|
78
|
-
|
83
|
+
ledger = <<~HERE
|
84
|
+
1970/11/01 Dinner should show up
|
85
|
+
Assets:Checking -123.00
|
86
|
+
Expenses:Restaurants
|
79
87
|
|
80
|
-
comment
|
88
|
+
comment
|
81
89
|
|
82
|
-
1970/11/01 Lunch should NOT show up
|
83
|
-
|
84
|
-
|
90
|
+
1970/11/01 Lunch should NOT show up
|
91
|
+
Assets:Checking -12.00
|
92
|
+
Expenses:Restaurants
|
85
93
|
|
86
|
-
end comment
|
87
|
-
HERE
|
94
|
+
end comment
|
95
|
+
HERE
|
88
96
|
entries = Reckon::LedgerParser.new.parse(StringIO.new(ledger))
|
89
97
|
expect(entries.length).to eq(1)
|
90
98
|
expect(entries.first[:desc]).to eq('Dinner should show up')
|
91
|
-
|
92
99
|
end
|
93
100
|
|
94
101
|
it 'should transaction comments' do
|
95
|
-
ledger =
|
96
|
-
2020-03-27 AMZN Mktp USX999H3203; Shopping; Sale
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
HERE
|
102
|
+
ledger = <<~HERE
|
103
|
+
2020-03-27 AMZN Mktp USX999H3203; Shopping; Sale
|
104
|
+
Expenses:Household $82.77
|
105
|
+
Liabilities:ChaseSapphire -$81.77
|
106
|
+
# END FINANCE SCRIPT OUTPUT Thu 02 Apr 2020 12:05:54 PM EDT
|
107
|
+
HERE
|
101
108
|
entries = Reckon::LedgerParser.new.parse(StringIO.new(ledger))
|
102
|
-
expect(entries.first[:accounts].map { |n|
|
109
|
+
expect(entries.first[:accounts].map { |n|
|
110
|
+
n[:name]
|
111
|
+
}).to eq(['Expenses:Household', 'Liabilities:ChaseSapphire'])
|
103
112
|
expect(entries.first[:accounts].size).to eq(2)
|
104
113
|
expect(entries.length).to eq(1)
|
105
114
|
end
|
@@ -123,80 +132,99 @@ HERE
|
|
123
132
|
@entries.last[:accounts].last[:name].should == "Assets:Bank:Checking"
|
124
133
|
@entries.last[:accounts].last[:amount].should == -20.24
|
125
134
|
end
|
135
|
+
|
136
|
+
it "should parse dot-separated dates" do
|
137
|
+
ledger = <<~HERE
|
138
|
+
2024.03.12 groceries; 11223344556; 32095205940
|
139
|
+
assets:bank:spending 530.00 NOK
|
140
|
+
assets:bank:co:groceries
|
141
|
+
|
142
|
+
2024.03.13 autosave; 11223344555; 11223344556
|
143
|
+
assets:bank:savings
|
144
|
+
assets:bank:spending -10.00 NOK
|
145
|
+
HERE
|
146
|
+
options = { ledger_date_format: '%Y.%m.%d' }
|
147
|
+
entries = Reckon::LedgerParser.new(options).parse(StringIO.new(ledger))
|
148
|
+
expect(entries.first[:date]).to eq(Date.new(2024, 3, 12))
|
149
|
+
expect(entries.last[:date]).to eq(Date.new(2024, 3, 13))
|
150
|
+
expect(entries.length).to eq(2)
|
151
|
+
end
|
126
152
|
end
|
127
153
|
|
128
154
|
describe "balance" do
|
129
155
|
it "it should balance out missing account values" do
|
130
156
|
@ledger.send(:balance, [
|
131
|
-
|
132
|
-
|
133
|
-
|
157
|
+
{ :name => "Account1", :amount => 1000 },
|
158
|
+
{ :name => "Account2", :amount => nil }
|
159
|
+
]).should == [{ :name => "Account1", :amount => 1000 },
|
160
|
+
{ :name => "Account2", :amount => -1000 }]
|
134
161
|
end
|
135
162
|
|
136
163
|
it "it should balance out missing account values" do
|
137
164
|
@ledger.send(:balance, [
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
165
|
+
{ :name => "Account1", :amount => 1000 },
|
166
|
+
{ :name => "Account2", :amount => 100 },
|
167
|
+
{ :name => "Account3", :amount => -200 },
|
168
|
+
{ :name => "Account4", :amount => nil }
|
169
|
+
]).should == [
|
170
|
+
{ :name => "Account1", :amount => 1000 },
|
171
|
+
{ :name => "Account2", :amount => 100 },
|
172
|
+
{ :name => "Account3", :amount => -200 },
|
173
|
+
{ :name => "Account4", :amount => -900 }
|
174
|
+
]
|
148
175
|
end
|
149
176
|
|
150
177
|
it "it should work on normal values too" do
|
151
178
|
@ledger.send(:balance, [
|
152
|
-
|
153
|
-
|
154
|
-
|
179
|
+
{ :name => "Account1", :amount => 1000 },
|
180
|
+
{ :name => "Account2", :amount => -1000 }
|
181
|
+
]).should == [{ :name => "Account1", :amount => 1000 },
|
182
|
+
{ :name => "Account2", :amount => -1000 }]
|
155
183
|
end
|
156
184
|
end
|
157
185
|
|
158
186
|
# Data
|
159
187
|
|
160
|
-
EXAMPLE_LEDGER = (
|
161
|
-
= /^Expenses:Books/
|
162
|
-
|
188
|
+
EXAMPLE_LEDGER = (<<~LEDGER).strip
|
189
|
+
= /^Expenses:Books/
|
190
|
+
(Liabilities:Taxes) -0.10
|
163
191
|
|
164
|
-
~ Monthly
|
165
|
-
|
166
|
-
|
192
|
+
~ Monthly
|
193
|
+
Assets:Bank:Checking $500.00
|
194
|
+
Income:Salary
|
167
195
|
|
168
|
-
2004-05-01 * Checking balance
|
169
|
-
|
170
|
-
|
196
|
+
2004-05-01 * Checking balance
|
197
|
+
Assets:Bank:Checking $1,000.00
|
198
|
+
Equity:Opening Balances
|
171
199
|
|
172
|
-
2004-05-01 * Checking balance
|
173
|
-
|
174
|
-
|
200
|
+
2004-05-01 * Checking balance
|
201
|
+
Assets:Bank:Checking €1,000.00
|
202
|
+
Equity:Opening Balances
|
175
203
|
|
176
|
-
2004-05-01 * Checking balance
|
177
|
-
|
178
|
-
|
204
|
+
2004-05-01 * Checking balance
|
205
|
+
Assets:Bank:Checking 1,000.00 SEK
|
206
|
+
Equity:Opening Balances
|
179
207
|
|
180
|
-
2004/05/01 * Investment balance
|
181
|
-
|
182
|
-
|
208
|
+
2004/05/01 * Investment balance
|
209
|
+
Assets:Brokerage 50 AAPL @ $30.00
|
210
|
+
Equity:Opening Balances
|
183
211
|
|
184
|
-
; blah
|
185
|
-
!account blah
|
212
|
+
; blah
|
213
|
+
!account blah
|
186
214
|
|
187
|
-
!end
|
215
|
+
!end
|
188
216
|
|
189
|
-
D $1,000
|
217
|
+
D $1,000
|
190
218
|
|
191
|
-
2004/05/14 * Pay day
|
192
|
-
|
193
|
-
|
219
|
+
2004/05/14 * Pay day
|
220
|
+
Assets:Bank:Checking $500.00
|
221
|
+
Income:Salary
|
194
222
|
|
195
|
-
2004/05/27 Book Store
|
196
|
-
|
197
|
-
|
198
|
-
2004/05/27 (100) Credit card company
|
199
|
-
|
200
|
-
|
223
|
+
2004/05/27 Book Store
|
224
|
+
Expenses:Books $20.00
|
225
|
+
Liabilities:MasterCard
|
226
|
+
2004/05/27 (100) Credit card company
|
227
|
+
Liabilities:MasterCard $20.24
|
228
|
+
Assets:Bank:Checking
|
201
229
|
LEDGER
|
202
230
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: reckon
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.10.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Cantino
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2024-
|
13
|
+
date: 2024-11-27 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: rspec
|