reckon 0.5.1 → 0.6.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 +50 -0
- data/.gitignore +2 -0
- data/.ruby-version +1 -1
- data/CHANGELOG.md +74 -0
- data/Gemfile.lock +1 -5
- data/README.md +72 -16
- data/Rakefile +17 -1
- data/lib/reckon.rb +2 -5
- data/lib/reckon/app.rb +145 -71
- data/lib/reckon/cosine_similarity.rb +92 -89
- data/lib/reckon/csv_parser.rb +67 -122
- data/lib/reckon/date_column.rb +10 -0
- data/lib/reckon/ledger_parser.rb +11 -1
- data/lib/reckon/logger.rb +4 -0
- data/lib/reckon/money.rb +52 -51
- data/lib/reckon/version.rb +1 -1
- data/reckon.gemspec +1 -2
- data/spec/data_fixtures/51-sample.csv +8 -0
- data/spec/data_fixtures/51-tokens.yml +9 -0
- data/spec/data_fixtures/85-date-example.csv +2 -0
- data/spec/integration/another_bank_example/input.csv +9 -0
- data/spec/integration/another_bank_example/output.ledger +36 -0
- data/spec/integration/another_bank_example/test_args +1 -0
- data/spec/integration/austrian_example/input.csv +13 -0
- data/spec/integration/austrian_example/output.ledger +52 -0
- data/spec/integration/austrian_example/test_args +2 -0
- data/spec/integration/bom_utf8_file/input.csv +3 -0
- data/spec/integration/bom_utf8_file/output.ledger +4 -0
- data/spec/integration/bom_utf8_file/test_args +3 -0
- data/spec/integration/broker_canada_example/input.csv +12 -0
- data/spec/integration/broker_canada_example/output.ledger +48 -0
- data/spec/integration/broker_canada_example/test_args +1 -0
- data/spec/integration/chase/account_tokens_and_regex/output.ledger +36 -0
- data/spec/integration/chase/account_tokens_and_regex/test_args +2 -0
- data/spec/integration/chase/account_tokens_and_regex/tokens.yml +16 -0
- data/spec/integration/chase/default_account_names/output.ledger +36 -0
- data/spec/integration/chase/default_account_names/test_args +3 -0
- data/spec/integration/chase/input.csv +9 -0
- data/spec/integration/chase/learn_from_existing/learn.ledger +7 -0
- data/spec/integration/chase/learn_from_existing/output.ledger +36 -0
- data/spec/integration/chase/learn_from_existing/test_args +1 -0
- data/spec/integration/chase/simple/output.ledger +36 -0
- data/spec/integration/chase/simple/test_args +1 -0
- data/spec/integration/danish_kroner_nordea_example/input.csv +6 -0
- data/spec/integration/danish_kroner_nordea_example/output.ledger +24 -0
- data/spec/integration/danish_kroner_nordea_example/test_args +1 -0
- data/spec/integration/english_date_example/input.csv +3 -0
- data/spec/integration/english_date_example/output.ledger +12 -0
- data/spec/integration/english_date_example/test_args +1 -0
- data/spec/integration/extratofake/input.csv +24 -0
- data/spec/integration/extratofake/output.ledger +92 -0
- data/spec/integration/extratofake/test_args +1 -0
- data/spec/integration/french_example/input.csv +9 -0
- data/spec/integration/french_example/output.ledger +36 -0
- data/spec/integration/french_example/test_args +2 -0
- data/spec/integration/german_date_example/input.csv +3 -0
- data/spec/integration/german_date_example/output.ledger +12 -0
- data/spec/integration/german_date_example/test_args +1 -0
- data/spec/integration/harder_date_example/input.csv +5 -0
- data/spec/integration/harder_date_example/output.ledger +20 -0
- data/spec/integration/harder_date_example/test_args +1 -0
- data/spec/integration/ing/input.csv +3 -0
- data/spec/integration/ing/output.ledger +12 -0
- data/spec/integration/ing/test_args +1 -0
- data/spec/integration/intuit_mint_example/input.csv +7 -0
- data/spec/integration/intuit_mint_example/output.ledger +28 -0
- data/spec/integration/intuit_mint_example/test_args +1 -0
- data/spec/integration/invalid_header_example/input.csv +6 -0
- data/spec/integration/invalid_header_example/output.ledger +8 -0
- data/spec/integration/invalid_header_example/test_args +1 -0
- data/spec/integration/inversed_credit_card/input.csv +16 -0
- data/spec/integration/inversed_credit_card/output.ledger +64 -0
- data/spec/integration/inversed_credit_card/test_args +1 -0
- data/spec/integration/nationwide/input.csv +4 -0
- data/spec/integration/nationwide/output.ledger +16 -0
- data/spec/integration/nationwide/test_args +1 -0
- data/spec/integration/regression/issue_51_account_tokens/input.csv +8 -0
- data/spec/integration/regression/issue_51_account_tokens/output.ledger +32 -0
- data/spec/integration/regression/issue_51_account_tokens/test_args +4 -0
- data/spec/integration/regression/issue_51_account_tokens/tokens.yml +9 -0
- data/spec/integration/regression/issue_64_date_column/input.csv +3 -0
- data/spec/integration/regression/issue_64_date_column/output.ledger +8 -0
- data/spec/integration/regression/issue_64_date_column/test_args +1 -0
- data/spec/integration/regression/issue_73_account_token_matching/input.csv +2 -0
- data/spec/integration/regression/issue_73_account_token_matching/output.ledger +4 -0
- data/spec/integration/regression/issue_73_account_token_matching/test_args +6 -0
- data/spec/integration/regression/issue_73_account_token_matching/tokens.yml +8 -0
- data/spec/integration/regression/issue_85_date_example/input.csv +2 -0
- data/spec/integration/regression/issue_85_date_example/output.ledger +8 -0
- data/spec/integration/regression/issue_85_date_example/test_args +1 -0
- data/spec/integration/spanish_date_example/input.csv +3 -0
- data/spec/integration/spanish_date_example/output.ledger +12 -0
- data/spec/integration/spanish_date_example/test_args +1 -0
- data/spec/integration/suntrust/input.csv +7 -0
- data/spec/integration/suntrust/output.ledger +28 -0
- data/spec/integration/suntrust/test_args +1 -0
- data/spec/integration/test.sh +82 -0
- data/spec/integration/test_money_column/input.csv +3 -0
- data/spec/integration/test_money_column/output.ledger +8 -0
- data/spec/integration/test_money_column/test_args +1 -0
- data/spec/integration/two_money_columns/input.csv +5 -0
- data/spec/integration/two_money_columns/output.ledger +20 -0
- data/spec/integration/two_money_columns/test_args +1 -0
- data/spec/integration/yyyymmdd_date_example/input.csv +1 -0
- data/spec/integration/yyyymmdd_date_example/output.ledger +4 -0
- data/spec/integration/yyyymmdd_date_example/test_args +1 -0
- data/spec/reckon/app_spec.rb +18 -2
- data/spec/reckon/csv_parser_spec.rb +129 -129
- data/spec/reckon/ledger_parser_spec.rb +42 -5
- data/spec/reckon/money_column_spec.rb +24 -24
- data/spec/reckon/money_spec.rb +36 -42
- data/spec/spec_helper.rb +19 -0
- metadata +97 -22
- data/.travis.yml +0 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: aebd4114b69fd94cc45e9cf425da57269ae80508b90f781fbbc5c96c6912b100
|
|
4
|
+
data.tar.gz: 0de1e3fb308bc2dad8cfa679056189092a4f07ece989b8e3b57c8acca100c77c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c59e8ae6ecbf11534086bd9fc179ff5e8aa52066b8fb0cd5ddbce28b6a4a01b32659fdf51cdaa00403a86be2b3023e390184b76924fb13a6ed5288b8d637c8c0
|
|
7
|
+
data.tar.gz: 75a5921df0c427351768c803bbe019b3bdf7a12ec8f8d714779086a73ab850ea50b49708c9430ac823b97c0ef35493b5888b5980cdbfab4691cecc05b759224e
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# This workflow uses actions that are not certified by GitHub.
|
|
2
|
+
# They are provided by a third-party and are governed by
|
|
3
|
+
# separate terms of service, privacy policy, and support
|
|
4
|
+
# documentation.
|
|
5
|
+
# This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
|
|
6
|
+
# For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
|
|
7
|
+
|
|
8
|
+
name: Build Status
|
|
9
|
+
|
|
10
|
+
on:
|
|
11
|
+
push:
|
|
12
|
+
branches: [ master ]
|
|
13
|
+
pull_request:
|
|
14
|
+
branches: [ master ]
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
test:
|
|
18
|
+
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
strategy:
|
|
21
|
+
matrix:
|
|
22
|
+
ruby-version:
|
|
23
|
+
# Current ruby stable version
|
|
24
|
+
- 3.0
|
|
25
|
+
# Ubuntu 20.10
|
|
26
|
+
- 2.7
|
|
27
|
+
# Ubuntu 19.10
|
|
28
|
+
- 2.5
|
|
29
|
+
# Mac v11 Big Sur
|
|
30
|
+
# - 2.6?
|
|
31
|
+
# Mac v10.15 Catalina
|
|
32
|
+
- 2.6
|
|
33
|
+
# Mac v10.14 Mojave
|
|
34
|
+
- 2.3.7
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/checkout@v2
|
|
37
|
+
- name: Install packages
|
|
38
|
+
run: sudo apt-get -y install ledger hledger
|
|
39
|
+
- name: Install bundler
|
|
40
|
+
run: sudo gem install -v 1.17.3 bundler
|
|
41
|
+
- name: Set up Ruby
|
|
42
|
+
# To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
|
|
43
|
+
# change this to (see https://github.com/ruby/setup-ruby#versioning):
|
|
44
|
+
uses: ruby/setup-ruby@v1
|
|
45
|
+
# uses: ruby/setup-ruby@473e4d8fe5dd94ee328fdfca9f8c9c7afc9dae5e
|
|
46
|
+
with:
|
|
47
|
+
ruby-version: ${{ matrix.ruby-version }}
|
|
48
|
+
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
|
49
|
+
- name: Run tests
|
|
50
|
+
run: bundle exec rake test_all
|
data/.gitignore
CHANGED
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
3.0.0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,79 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.1](https://github.com/cantino/reckon/tree/0.6.1) (2021-01-23)
|
|
4
|
+
|
|
5
|
+
[Full Changelog](https://github.com/cantino/reckon/compare/v0.6.0...0.6.1)
|
|
6
|
+
|
|
7
|
+
**Implemented enhancements:**
|
|
8
|
+
|
|
9
|
+
- \[Feature Request\] Note flag --add-notes in CLI to allow additional notes for each ledger entry [\#86](https://github.com/cantino/reckon/issues/86)
|
|
10
|
+
|
|
11
|
+
**Closed issues:**
|
|
12
|
+
|
|
13
|
+
- spaces in tokens [\#97](https://github.com/cantino/reckon/issues/97)
|
|
14
|
+
- Migrate CI system from travis-ci.org [\#93](https://github.com/cantino/reckon/issues/93)
|
|
15
|
+
- \[Feature Request\] Pipe ledger file input to the bayesian predictor \(instead of csv\) [\#91](https://github.com/cantino/reckon/issues/91)
|
|
16
|
+
|
|
17
|
+
**Merged pull requests:**
|
|
18
|
+
|
|
19
|
+
- Add github actions [\#100](https://github.com/cantino/reckon/pull/100) ([benprew](https://github.com/benprew))
|
|
20
|
+
- Add documentation for doing a substring match. Fixes \#97 [\#99](https://github.com/cantino/reckon/pull/99) ([benprew](https://github.com/benprew))
|
|
21
|
+
- Test fixes [\#94](https://github.com/cantino/reckon/pull/94) ([benprew](https://github.com/benprew))
|
|
22
|
+
|
|
23
|
+
## [v0.6.0](https://github.com/cantino/reckon/tree/v0.6.0) (2020-09-04)
|
|
24
|
+
|
|
25
|
+
[Full Changelog](https://github.com/cantino/reckon/compare/v0.5.4...v0.6.0)
|
|
26
|
+
|
|
27
|
+
**Fixed bugs:**
|
|
28
|
+
|
|
29
|
+
- \[BUG\] Reckon appears not to be parsing ISO standard date yyyy-mm-dd? [\#85](https://github.com/cantino/reckon/issues/85)
|
|
30
|
+
|
|
31
|
+
**Closed issues:**
|
|
32
|
+
|
|
33
|
+
- duplicate detection [\#16](https://github.com/cantino/reckon/issues/16)
|
|
34
|
+
|
|
35
|
+
**Merged pull requests:**
|
|
36
|
+
|
|
37
|
+
- Add ability to add note to transaction when entering it [\#89](https://github.com/cantino/reckon/pull/89) ([benprew](https://github.com/benprew))
|
|
38
|
+
|
|
39
|
+
## [v0.5.4](https://github.com/cantino/reckon/tree/v0.5.4) (2020-06-05)
|
|
40
|
+
|
|
41
|
+
[Full Changelog](https://github.com/cantino/reckon/compare/v0.5.3...v0.5.4)
|
|
42
|
+
|
|
43
|
+
**Fixed bugs:**
|
|
44
|
+
|
|
45
|
+
- order of transactions [\#88](https://github.com/cantino/reckon/issues/88)
|
|
46
|
+
- Is reckon failing to handle comments when learning? [\#87](https://github.com/cantino/reckon/issues/87)
|
|
47
|
+
|
|
48
|
+
## [v0.5.3](https://github.com/cantino/reckon/tree/v0.5.3) (2020-05-02)
|
|
49
|
+
|
|
50
|
+
[Full Changelog](https://github.com/cantino/reckon/compare/v0.5.2...v0.5.3)
|
|
51
|
+
|
|
52
|
+
**Closed issues:**
|
|
53
|
+
|
|
54
|
+
- \[FEATURE REQUEST\] Ask for currency of Account and output in output file in standard format of xxxx TLA for currency [\#84](https://github.com/cantino/reckon/issues/84)
|
|
55
|
+
|
|
56
|
+
## [v0.5.2](https://github.com/cantino/reckon/tree/v0.5.2) (2020-03-07)
|
|
57
|
+
|
|
58
|
+
[Full Changelog](https://github.com/cantino/reckon/compare/v0.5.1...v0.5.2)
|
|
59
|
+
|
|
60
|
+
**Closed issues:**
|
|
61
|
+
|
|
62
|
+
- \[Bug\]? Reckon fails to run on ruby 2.7.0 on Catalina [\#83](https://github.com/cantino/reckon/issues/83)
|
|
63
|
+
- --account-tokens issue [\#51](https://github.com/cantino/reckon/issues/51)
|
|
64
|
+
|
|
65
|
+
## [v0.5.1](https://github.com/cantino/reckon/tree/v0.5.1) (2020-02-25)
|
|
66
|
+
|
|
67
|
+
[Full Changelog](https://github.com/cantino/reckon/compare/v0.5.0...v0.5.1)
|
|
68
|
+
|
|
69
|
+
**Closed issues:**
|
|
70
|
+
|
|
71
|
+
- Error Importing [\#64](https://github.com/cantino/reckon/issues/64)
|
|
72
|
+
|
|
73
|
+
**Merged pull requests:**
|
|
74
|
+
|
|
75
|
+
- guard against rows that don't parse dates [\#82](https://github.com/cantino/reckon/pull/82) ([benprew](https://github.com/benprew))
|
|
76
|
+
|
|
3
77
|
## [v0.5.0](https://github.com/cantino/reckon/tree/v0.5.0) (2020-02-19)
|
|
4
78
|
|
|
5
79
|
[Full Changelog](https://github.com/cantino/reckon/compare/v0.4.4...v0.5.0)
|
data/Gemfile.lock
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
reckon (0.
|
|
4
|
+
reckon (0.6.1)
|
|
5
5
|
chronic (>= 0.3.0)
|
|
6
6
|
highline (>= 1.5.2)
|
|
7
7
|
rchardet (>= 1.8.0)
|
|
8
|
-
terminal-table (>= 1.4.2)
|
|
9
8
|
|
|
10
9
|
GEM
|
|
11
10
|
remote: http://rubygems.org/
|
|
@@ -34,9 +33,6 @@ GEM
|
|
|
34
33
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
35
34
|
rspec-support (~> 3.9.0)
|
|
36
35
|
rspec-support (3.9.2)
|
|
37
|
-
terminal-table (1.8.0)
|
|
38
|
-
unicode-display_width (~> 1.1, >= 1.1.1)
|
|
39
|
-
unicode-display_width (1.6.1)
|
|
40
36
|
|
|
41
37
|
PLATFORMS
|
|
42
38
|
ruby
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Reckon
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+

|
|
4
4
|
|
|
5
5
|
Reckon automagically converts CSV files for use with the command-line accounting tool [Ledger](http://www.ledger-cli.org/). It also helps you to select the correct accounts associated with the CSV data using Bayesian machine learning.
|
|
6
6
|
|
|
@@ -34,9 +34,8 @@ Learn more:
|
|
|
34
34
|
|
|
35
35
|
Usage: Reckon.rb [options]
|
|
36
36
|
|
|
37
|
-
|
|
38
37
|
-f, --file FILE The CSV file to parse
|
|
39
|
-
-a, --account
|
|
38
|
+
-a, --account NAME The Ledger Account this file is for
|
|
40
39
|
-v, --[no-]verbose Run verbosely
|
|
41
40
|
-i, --inverse Use the negative of each amount
|
|
42
41
|
-p, --print-table Print out the parsed CSV in table form
|
|
@@ -44,6 +43,12 @@ Learn more:
|
|
|
44
43
|
-l, --learn-from FILE An existing ledger file to learn accounts from
|
|
45
44
|
--ignore-columns 1,2,5
|
|
46
45
|
Columns to ignore in the CSV file - the first column is column 1
|
|
46
|
+
--money-column 2
|
|
47
|
+
Specify the money column instead of letting Reckon guess - the first column is column 1
|
|
48
|
+
--raw-money
|
|
49
|
+
Don't format money column (for stocks)
|
|
50
|
+
--date-column 3
|
|
51
|
+
Specify the date column instead of letting Reckon guess - the first column is column 1
|
|
47
52
|
--contains-header [N]
|
|
48
53
|
The first row of the CSV is a header and should be skipped. Optionally add the number of rows to skip.
|
|
49
54
|
--csv-separator ','
|
|
@@ -57,9 +62,9 @@ Learn more:
|
|
|
57
62
|
Force the date format (see Ruby DateTime strftime)
|
|
58
63
|
-u, --unattended Don't ask questions and guess all the accounts automatically. Used with --learn-from or --account-tokens options.
|
|
59
64
|
-t, --account-tokens FILE YAML file with manually-assigned tokens for each account (see README)
|
|
60
|
-
--default-into-account
|
|
65
|
+
--default-into-account NAME
|
|
61
66
|
Default into account
|
|
62
|
-
--default-outof-account
|
|
67
|
+
--default-outof-account NAME
|
|
63
68
|
Default 'out of' account
|
|
64
69
|
--suffixed
|
|
65
70
|
If --currency should be used as a suffix. Defaults to false.
|
|
@@ -77,6 +82,16 @@ To guess the accounts reckon can use an existing ledger file or a token file wit
|
|
|
77
82
|
|
|
78
83
|
`reckon --unattended --account-tokens tokens.yaml -f bank.csv -o ledger.dat`
|
|
79
84
|
|
|
85
|
+
### Account Tokens
|
|
86
|
+
|
|
87
|
+
The account tokens file provides a way to teach reckon about what tokens are associated with an account. As an example, this `tokens.yaml` file:
|
|
88
|
+
|
|
89
|
+
Expenses:
|
|
90
|
+
Bank:
|
|
91
|
+
- 'ING Direct Deposit'
|
|
92
|
+
|
|
93
|
+
Would tokenize to 'ING', 'Direct' and 'Deposit'. The matcher would then suggest matches to transactions that included those tokens. (ex 'Chase Direct Deposit')
|
|
94
|
+
|
|
80
95
|
Here's an example of `tokens.yaml`:
|
|
81
96
|
|
|
82
97
|
```
|
|
@@ -96,21 +111,62 @@ Expenses:
|
|
|
96
111
|
- '4433221100' # Your own account number
|
|
97
112
|
```
|
|
98
113
|
|
|
99
|
-
|
|
100
|
-
|
|
114
|
+
Reckon will use `Income:Unknown` or `Expenses:Unknown` if it can't match a transaction to an account.
|
|
115
|
+
|
|
116
|
+
You can override these names with the `--default_outof_account` and `--default_into_account` options.
|
|
117
|
+
|
|
118
|
+
### Substring Match
|
|
119
|
+
|
|
120
|
+
If, in the above example, you'd prefer to match any transaction that contains the string 'ING Direct Deposit' you have to use a regex:
|
|
121
|
+
|
|
122
|
+
Expenses:
|
|
123
|
+
Bank:
|
|
124
|
+
- /ING Direct Deposit/
|
|
125
|
+
|
|
126
|
+
## Contributing
|
|
127
|
+
|
|
128
|
+
We encourage you to contribute to Reckon! Here is some information to help you.
|
|
129
|
+
|
|
130
|
+
### Patches/Pull Requests Process
|
|
131
|
+
|
|
132
|
+
1. Fork the project.
|
|
133
|
+
2. Make your feature addition or bug fix.
|
|
134
|
+
3. Add tests for it. This is important so I don't break it in a
|
|
135
|
+
4. future version unintentionally.
|
|
136
|
+
5. Commit, do not mess with rakefile, version, or history.
|
|
137
|
+
- (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
|
138
|
+
6. Send me a pull request. Bonus points for topic branches.
|
|
101
139
|
|
|
102
|
-
|
|
140
|
+
### Integration Tests
|
|
141
|
+
|
|
142
|
+
Reckon has integration test located in `spec/integration`. These are integration and regression tests for reckon.
|
|
143
|
+
|
|
144
|
+
Run all the tests:
|
|
145
|
+
|
|
146
|
+
./spec/integration/test.sh
|
|
147
|
+
|
|
148
|
+
Run a single test
|
|
149
|
+
|
|
150
|
+
./spec/integration/test.sh chase/account_tokens_and_regex
|
|
151
|
+
|
|
152
|
+
#### Add a new integration test
|
|
153
|
+
|
|
154
|
+
Each test has it's own directory, which you can add any files you want, but the following files are required:
|
|
155
|
+
|
|
156
|
+
- `test_args` - arguments to add to the reckon command to test against, can specify `--unattended`, `-f input.csv`, etc
|
|
157
|
+
- `output.ledger` - the expected ledger file output
|
|
158
|
+
|
|
159
|
+
If the result of running reckon with `test_args` does not match `output.ledger`, then the test fails.
|
|
160
|
+
|
|
161
|
+
Most tests will specify `--unattended`, otherwise reckon prompts for keyboard input.
|
|
162
|
+
|
|
163
|
+
The convention is to use `input.csv` as the input file, and `tokens.yml` as the tokens file, but it is not required.
|
|
103
164
|
|
|
104
|
-
* Fork the project.
|
|
105
|
-
* Make your feature addition or bug fix.
|
|
106
|
-
* Add tests for it. This is important so I don't break it in a
|
|
107
|
-
future version unintentionally.
|
|
108
|
-
* Commit, do not mess with rakefile, version, or history.
|
|
109
|
-
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
|
110
|
-
* Send me a pull request. Bonus points for topic branches.
|
|
111
165
|
|
|
112
166
|
## Copyright
|
|
113
167
|
|
|
114
|
-
Copyright (c) 2013 Andrew Cantino. See LICENSE for details.
|
|
168
|
+
Copyright (c) 2013 Andrew Cantino (@cantino). See LICENSE for details.
|
|
115
169
|
|
|
116
170
|
Thanks to @BlackEdder for many contributions!
|
|
171
|
+
|
|
172
|
+
Currently maintained by @benprew. Thank you!
|
data/Rakefile
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "bundler/gem_tasks"
|
|
2
4
|
require 'rspec/core/rake_task'
|
|
5
|
+
require 'English'
|
|
3
6
|
|
|
4
7
|
RSpec::Core::RakeTask.new(:spec)
|
|
5
8
|
|
|
6
|
-
task :
|
|
9
|
+
task default: :spec
|
|
10
|
+
|
|
11
|
+
task :test_all do
|
|
12
|
+
puts "#{`ledger --version |head -n1`}"
|
|
13
|
+
puts "Running unit tests"
|
|
14
|
+
Rake::Task["spec"].invoke
|
|
15
|
+
puts "Running integration tests"
|
|
16
|
+
Rake::Task["integration_tests"].invoke
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
task :integration_tests do
|
|
20
|
+
puts `./spec/integration/test.sh`
|
|
21
|
+
raise 'Integration tests failed' if $CHILD_STATUS.exitstatus != 0
|
|
22
|
+
end
|
data/lib/reckon.rb
CHANGED
|
@@ -4,16 +4,13 @@ require 'rubygems'
|
|
|
4
4
|
require 'rchardet'
|
|
5
5
|
require 'chronic'
|
|
6
6
|
require 'csv'
|
|
7
|
-
require 'highline
|
|
7
|
+
require 'highline'
|
|
8
8
|
require 'optparse'
|
|
9
|
-
require 'terminal-table'
|
|
10
9
|
require 'time'
|
|
11
10
|
require 'logger'
|
|
12
11
|
|
|
13
|
-
LOGGER = Logger.new(STDERR)
|
|
14
|
-
LOGGER.level = Logger::WARN
|
|
15
|
-
|
|
16
12
|
require_relative 'reckon/version'
|
|
13
|
+
require_relative 'reckon/logger'
|
|
17
14
|
require_relative 'reckon/cosine_similarity'
|
|
18
15
|
require_relative 'reckon/date_column'
|
|
19
16
|
require_relative 'reckon/money'
|
data/lib/reckon/app.rb
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
# coding: utf-8
|
|
2
|
+
|
|
2
3
|
require 'pp'
|
|
3
4
|
require 'yaml'
|
|
4
5
|
|
|
5
6
|
module Reckon
|
|
6
7
|
class App
|
|
7
8
|
attr_accessor :options, :seen, :csv_parser, :regexps, :matcher
|
|
9
|
+
@@cli = HighLine.new
|
|
8
10
|
|
|
9
|
-
def initialize(
|
|
11
|
+
def initialize(opts = {})
|
|
12
|
+
self.options = opts
|
|
10
13
|
LOGGER.level = Logger::INFO if options[:verbose]
|
|
11
|
-
|
|
14
|
+
|
|
12
15
|
self.regexps = {}
|
|
13
|
-
self.seen =
|
|
16
|
+
self.seen = Set.new
|
|
14
17
|
self.options[:currency] ||= '$'
|
|
15
|
-
options[:string] = File.read(options[:file]) unless options[:string]
|
|
16
18
|
@csv_parser = CSVParser.new( options )
|
|
17
19
|
@matcher = CosineSimilarity.new(options)
|
|
18
20
|
learn!
|
|
@@ -20,22 +22,19 @@ module Reckon
|
|
|
20
22
|
|
|
21
23
|
def interactive_output(str)
|
|
22
24
|
return if options[:unattended]
|
|
25
|
+
|
|
23
26
|
puts str
|
|
24
27
|
end
|
|
25
28
|
|
|
26
29
|
def learn!
|
|
27
30
|
learn_from_account_tokens(options[:account_tokens_file])
|
|
28
|
-
|
|
29
|
-
ledger_file = options[:existing_ledger_file]
|
|
30
|
-
return unless ledger_file
|
|
31
|
-
fail "#{ledger_file} doesn't exist!" unless File.exists?(ledger_file)
|
|
32
|
-
learn_from(File.read(ledger_file))
|
|
31
|
+
learn_from_ledger_file(options[:existing_ledger_file])
|
|
33
32
|
end
|
|
34
33
|
|
|
35
34
|
def learn_from_account_tokens(filename)
|
|
36
35
|
return unless filename
|
|
37
36
|
|
|
38
|
-
|
|
37
|
+
raise "#{filename} doesn't exist!" unless File.exist?(filename)
|
|
39
38
|
|
|
40
39
|
extract_account_tokens(YAML.load_file(filename)).each do |account, tokens|
|
|
41
40
|
tokens.each do |t|
|
|
@@ -48,14 +47,27 @@ module Reckon
|
|
|
48
47
|
end
|
|
49
48
|
end
|
|
50
49
|
|
|
51
|
-
def
|
|
50
|
+
def learn_from_ledger_file(ledger_file)
|
|
51
|
+
return unless ledger_file
|
|
52
|
+
|
|
53
|
+
raise "#{ledger_file} doesn't exist!" unless File.exist?(ledger_file)
|
|
54
|
+
|
|
55
|
+
learn_from_ledger(File.read(ledger_file))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def learn_from_ledger(ledger)
|
|
59
|
+
LOGGER.info "learning from #{ledger}"
|
|
52
60
|
LedgerParser.new(ledger).entries.each do |entry|
|
|
53
61
|
entry[:accounts].each do |account|
|
|
54
62
|
str = [entry[:desc], account[:amount]].join(" ")
|
|
55
|
-
|
|
63
|
+
if account[:name] != options[:bank_account]
|
|
64
|
+
LOGGER.info "adding document #{account[:name]} #{str}"
|
|
65
|
+
@matcher.add_document(account[:name], str)
|
|
66
|
+
end
|
|
56
67
|
pretty_date = entry[:date].iso8601
|
|
57
|
-
|
|
58
|
-
|
|
68
|
+
if account[:name] == options[:bank_account]
|
|
69
|
+
seen << seen_key(pretty_date, @csv_parser.pretty_money(account[:amount]))
|
|
70
|
+
end
|
|
59
71
|
end
|
|
60
72
|
end
|
|
61
73
|
end
|
|
@@ -91,9 +103,10 @@ module Reckon
|
|
|
91
103
|
end
|
|
92
104
|
|
|
93
105
|
def walk_backwards
|
|
106
|
+
cmd_options = "[account]/[q]uit/[s]kip/[n]ote/[d]escription"
|
|
94
107
|
seen_anything_new = false
|
|
95
108
|
each_row_backwards do |row|
|
|
96
|
-
|
|
109
|
+
print_transaction([row])
|
|
97
110
|
|
|
98
111
|
if already_seen?(row)
|
|
99
112
|
interactive_output "NOTE: This row is very similar to a previous one!"
|
|
@@ -105,50 +118,28 @@ module Reckon
|
|
|
105
118
|
seen_anything_new = true
|
|
106
119
|
end
|
|
107
120
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
else
|
|
114
|
-
out_of_account = ask("Which account provided this income? ([account]/[q]uit/[s]kip) ") { |q|
|
|
115
|
-
q.completion = possible_answers
|
|
116
|
-
q.readline = true
|
|
117
|
-
q.default = possible_answers.first
|
|
118
|
-
}
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
finish if out_of_account == "quit" || out_of_account == "q"
|
|
122
|
-
if out_of_account == "skip" || out_of_account == "s"
|
|
123
|
-
interactive_output "Skipping"
|
|
124
|
-
next
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
ledger_format( row,
|
|
128
|
-
[options[:bank_account], row[:pretty_money]],
|
|
129
|
-
[out_of_account, row[:pretty_money_negated]] )
|
|
121
|
+
if row[:money] > 0
|
|
122
|
+
# out_of_account
|
|
123
|
+
answer = ask_account_question("Which account provided this income? (#{cmd_options})", row)
|
|
124
|
+
line1 = [options[:bank_account], row[:pretty_money]]
|
|
125
|
+
line2 = [answer, ""]
|
|
130
126
|
else
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
q.default = possible_answers.first
|
|
138
|
-
}
|
|
139
|
-
end
|
|
140
|
-
finish if into_account == "quit" || into_account == 'q'
|
|
141
|
-
if into_account == "skip" || into_account == 's'
|
|
142
|
-
interactive_output "Skipping"
|
|
143
|
-
next
|
|
144
|
-
end
|
|
127
|
+
# into_account
|
|
128
|
+
answer = ask_account_question("To which account did this money go? (#{cmd_options})", row)
|
|
129
|
+
# line1 = [answer, row[:pretty_money_negated]]
|
|
130
|
+
line1 = [answer, ""]
|
|
131
|
+
line2 = [options[:bank_account], row[:pretty_money]]
|
|
132
|
+
end
|
|
145
133
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
134
|
+
finish if %w[quit q].include?(answer)
|
|
135
|
+
if %w[skip s].include?(answer)
|
|
136
|
+
interactive_output "Skipping"
|
|
137
|
+
next
|
|
149
138
|
end
|
|
150
139
|
|
|
151
|
-
|
|
140
|
+
ledger = ledger_format(row, line1, line2)
|
|
141
|
+
LOGGER.info "ledger line: #{ledger}"
|
|
142
|
+
learn_from_ledger(ledger) unless options[:account_tokens_file]
|
|
152
143
|
output(ledger)
|
|
153
144
|
end
|
|
154
145
|
end
|
|
@@ -167,16 +158,93 @@ module Reckon
|
|
|
167
158
|
:money => @csv_parser.money_for(index),
|
|
168
159
|
:description => @csv_parser.description_for(index) }
|
|
169
160
|
end
|
|
170
|
-
rows.sort_by { |n| n[:date] }.each {|row| yield row }
|
|
161
|
+
rows.sort_by { |n| [n[:date], -n[:money], n[:description]] }.each { |row| yield row }
|
|
171
162
|
end
|
|
172
163
|
|
|
173
|
-
def
|
|
164
|
+
def print_transaction(rows)
|
|
165
|
+
str = "\n"
|
|
166
|
+
header = %w[Date Amount Description Note]
|
|
167
|
+
maxes = header.map(&:length)
|
|
168
|
+
|
|
169
|
+
rows = rows.map { |r| [r[:pretty_date], r[:pretty_money], r[:description], r[:note]] }
|
|
170
|
+
|
|
171
|
+
rows.each do |r|
|
|
172
|
+
r.length.times { |i| l = r[i] ? r[i].length : 0; maxes[i] = l if maxes[i] < l }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
header.each_with_index do |n, i|
|
|
176
|
+
str += " #{n.center(maxes[i])} |"
|
|
177
|
+
end
|
|
178
|
+
str += "\n"
|
|
179
|
+
|
|
180
|
+
rows.each do |row|
|
|
181
|
+
row.each_with_index do |_, i|
|
|
182
|
+
just = maxes[i]
|
|
183
|
+
str += sprintf(" %#{just}s |", row[i])
|
|
184
|
+
end
|
|
185
|
+
str += "\n"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
interactive_output str
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def ask_account_question(msg, row)
|
|
192
|
+
possible_answers = suggest(row)
|
|
193
|
+
LOGGER.info "possible_answers===> #{possible_answers.inspect}"
|
|
194
|
+
|
|
195
|
+
if options[:unattended]
|
|
196
|
+
default = if row[:pretty_money][0] == '-'
|
|
197
|
+
options[:default_into_account] || 'Expenses:Unknown'
|
|
198
|
+
else
|
|
199
|
+
options[:default_outof_account] || 'Income:Unknown'
|
|
200
|
+
end
|
|
201
|
+
return possible_answers[0] || default
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
answer = @@cli.ask(msg) do |q|
|
|
205
|
+
q.completion = possible_answers
|
|
206
|
+
q.readline = true
|
|
207
|
+
q.default = possible_answers.first
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# if answer isn't n/note/d/description, must be an account name, or skip, or quit
|
|
211
|
+
return answer unless %w[n note d description].include?(answer)
|
|
212
|
+
|
|
213
|
+
add_description(row) if %w[d description].include?(answer)
|
|
214
|
+
add_note(row) if %w[n note].include?(answer)
|
|
215
|
+
|
|
216
|
+
print_transaction([row])
|
|
217
|
+
# give user a chance to set account name or retry description
|
|
218
|
+
return ask_account_question(msg, row)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def add_description(row)
|
|
222
|
+
desc_answer = @@cli.ask("Enter a new description for this transaction (empty line aborts)\n") do |q|
|
|
223
|
+
q.overwrite = true
|
|
224
|
+
q.readline = true
|
|
225
|
+
q.default = row[:description]
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
row[:description] = desc_answer unless desc_answer.empty?
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def add_note(row)
|
|
232
|
+
desc_answer = @@cli.ask("Enter a new note for this transaction (empty line aborts)\n") do |q|
|
|
233
|
+
q.overwrite = true
|
|
234
|
+
q.readline = true
|
|
235
|
+
q.default = row[:note]
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
row[:note] = desc_answer unless desc_answer.empty?
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def most_specific_regexp_match(row)
|
|
174
242
|
matches = regexps.map { |regexp, account|
|
|
175
243
|
if match = regexp.match(row[:description])
|
|
176
244
|
[account, match[0]]
|
|
177
245
|
end
|
|
178
246
|
}.compact
|
|
179
|
-
matches.sort_by! { |
|
|
247
|
+
matches.sort_by! { |_account, matched_text| matched_text.length }.map(&:first)
|
|
180
248
|
end
|
|
181
249
|
|
|
182
250
|
def suggest(row)
|
|
@@ -185,9 +253,9 @@ module Reckon
|
|
|
185
253
|
end
|
|
186
254
|
|
|
187
255
|
def ledger_format(row, line1, line2)
|
|
188
|
-
out = "#{row[:pretty_date]}\t#{row[:description]}\n"
|
|
189
|
-
out += "\t#{line1.first}\t\t\t
|
|
190
|
-
out += "\t#{line2.first}\t\t\t
|
|
256
|
+
out = "#{row[:pretty_date]}\t#{row[:description]}#{row[:note] ? "\t; " + row[:note]: ""}\n"
|
|
257
|
+
out += "\t#{line1.first}\t\t\t#{line1.last}\n"
|
|
258
|
+
out += "\t#{line2.first}\t\t\t#{line2.last}\n\n"
|
|
191
259
|
out
|
|
192
260
|
end
|
|
193
261
|
|
|
@@ -196,8 +264,12 @@ module Reckon
|
|
|
196
264
|
options[:output_file].flush
|
|
197
265
|
end
|
|
198
266
|
|
|
267
|
+
def seen_key(date, amount)
|
|
268
|
+
return [date, amount].join("|")
|
|
269
|
+
end
|
|
270
|
+
|
|
199
271
|
def already_seen?(row)
|
|
200
|
-
seen
|
|
272
|
+
seen.include?(seen_key(row[:pretty_date], row[:pretty_money]))
|
|
201
273
|
end
|
|
202
274
|
|
|
203
275
|
def finish
|
|
@@ -207,13 +279,11 @@ module Reckon
|
|
|
207
279
|
end
|
|
208
280
|
|
|
209
281
|
def output_table
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
t << [ row[:pretty_date], row[:pretty_money], row[:description] ]
|
|
214
|
-
end
|
|
282
|
+
rows = []
|
|
283
|
+
each_row_backwards do |row|
|
|
284
|
+
rows << row
|
|
215
285
|
end
|
|
216
|
-
|
|
286
|
+
print_transaction(rows)
|
|
217
287
|
end
|
|
218
288
|
|
|
219
289
|
def self.parse_opts(args = ARGV)
|
|
@@ -258,6 +328,10 @@ module Reckon
|
|
|
258
328
|
options[:money_column] = column_number
|
|
259
329
|
end
|
|
260
330
|
|
|
331
|
+
opts.on("", "--raw-money", "Don't format money column (for stocks)") do |n|
|
|
332
|
+
options[:raw] = n
|
|
333
|
+
end
|
|
334
|
+
|
|
261
335
|
opts.on("", "--date-column 3", Integer, "Specify the date column instead of letting Reckon guess - the first column is column 1") do |column_number|
|
|
262
336
|
options[:date_column] = column_number
|
|
263
337
|
end
|
|
@@ -321,7 +395,7 @@ module Reckon
|
|
|
321
395
|
end
|
|
322
396
|
|
|
323
397
|
unless options[:file]
|
|
324
|
-
options[:file] = ask("What CSV file should I parse? ")
|
|
398
|
+
options[:file] = @@cli.ask("What CSV file should I parse? ")
|
|
325
399
|
unless options[:file].length > 0
|
|
326
400
|
puts "\nYou must provide a CSV file to parse.\n"
|
|
327
401
|
puts parser
|
|
@@ -330,9 +404,9 @@ module Reckon
|
|
|
330
404
|
end
|
|
331
405
|
|
|
332
406
|
unless options[:bank_account]
|
|
333
|
-
fail "Please specify --account
|
|
407
|
+
fail "Please specify --account in unattended mode" if options[:unattended]
|
|
334
408
|
|
|
335
|
-
options[:bank_account] = ask("What is the account name of this bank account in Ledger? ") do |q|
|
|
409
|
+
options[:bank_account] = @@cli.ask("What is the account name of this bank account in Ledger? ") do |q|
|
|
336
410
|
q.readline = true
|
|
337
411
|
q.validate = /^.{2,}$/
|
|
338
412
|
q.default = "Assets:Bank:Checking"
|