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.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +50 -0
  3. data/.gitignore +2 -0
  4. data/.ruby-version +1 -1
  5. data/CHANGELOG.md +74 -0
  6. data/Gemfile.lock +1 -5
  7. data/README.md +72 -16
  8. data/Rakefile +17 -1
  9. data/lib/reckon.rb +2 -5
  10. data/lib/reckon/app.rb +145 -71
  11. data/lib/reckon/cosine_similarity.rb +92 -89
  12. data/lib/reckon/csv_parser.rb +67 -122
  13. data/lib/reckon/date_column.rb +10 -0
  14. data/lib/reckon/ledger_parser.rb +11 -1
  15. data/lib/reckon/logger.rb +4 -0
  16. data/lib/reckon/money.rb +52 -51
  17. data/lib/reckon/version.rb +1 -1
  18. data/reckon.gemspec +1 -2
  19. data/spec/data_fixtures/51-sample.csv +8 -0
  20. data/spec/data_fixtures/51-tokens.yml +9 -0
  21. data/spec/data_fixtures/85-date-example.csv +2 -0
  22. data/spec/integration/another_bank_example/input.csv +9 -0
  23. data/spec/integration/another_bank_example/output.ledger +36 -0
  24. data/spec/integration/another_bank_example/test_args +1 -0
  25. data/spec/integration/austrian_example/input.csv +13 -0
  26. data/spec/integration/austrian_example/output.ledger +52 -0
  27. data/spec/integration/austrian_example/test_args +2 -0
  28. data/spec/integration/bom_utf8_file/input.csv +3 -0
  29. data/spec/integration/bom_utf8_file/output.ledger +4 -0
  30. data/spec/integration/bom_utf8_file/test_args +3 -0
  31. data/spec/integration/broker_canada_example/input.csv +12 -0
  32. data/spec/integration/broker_canada_example/output.ledger +48 -0
  33. data/spec/integration/broker_canada_example/test_args +1 -0
  34. data/spec/integration/chase/account_tokens_and_regex/output.ledger +36 -0
  35. data/spec/integration/chase/account_tokens_and_regex/test_args +2 -0
  36. data/spec/integration/chase/account_tokens_and_regex/tokens.yml +16 -0
  37. data/spec/integration/chase/default_account_names/output.ledger +36 -0
  38. data/spec/integration/chase/default_account_names/test_args +3 -0
  39. data/spec/integration/chase/input.csv +9 -0
  40. data/spec/integration/chase/learn_from_existing/learn.ledger +7 -0
  41. data/spec/integration/chase/learn_from_existing/output.ledger +36 -0
  42. data/spec/integration/chase/learn_from_existing/test_args +1 -0
  43. data/spec/integration/chase/simple/output.ledger +36 -0
  44. data/spec/integration/chase/simple/test_args +1 -0
  45. data/spec/integration/danish_kroner_nordea_example/input.csv +6 -0
  46. data/spec/integration/danish_kroner_nordea_example/output.ledger +24 -0
  47. data/spec/integration/danish_kroner_nordea_example/test_args +1 -0
  48. data/spec/integration/english_date_example/input.csv +3 -0
  49. data/spec/integration/english_date_example/output.ledger +12 -0
  50. data/spec/integration/english_date_example/test_args +1 -0
  51. data/spec/integration/extratofake/input.csv +24 -0
  52. data/spec/integration/extratofake/output.ledger +92 -0
  53. data/spec/integration/extratofake/test_args +1 -0
  54. data/spec/integration/french_example/input.csv +9 -0
  55. data/spec/integration/french_example/output.ledger +36 -0
  56. data/spec/integration/french_example/test_args +2 -0
  57. data/spec/integration/german_date_example/input.csv +3 -0
  58. data/spec/integration/german_date_example/output.ledger +12 -0
  59. data/spec/integration/german_date_example/test_args +1 -0
  60. data/spec/integration/harder_date_example/input.csv +5 -0
  61. data/spec/integration/harder_date_example/output.ledger +20 -0
  62. data/spec/integration/harder_date_example/test_args +1 -0
  63. data/spec/integration/ing/input.csv +3 -0
  64. data/spec/integration/ing/output.ledger +12 -0
  65. data/spec/integration/ing/test_args +1 -0
  66. data/spec/integration/intuit_mint_example/input.csv +7 -0
  67. data/spec/integration/intuit_mint_example/output.ledger +28 -0
  68. data/spec/integration/intuit_mint_example/test_args +1 -0
  69. data/spec/integration/invalid_header_example/input.csv +6 -0
  70. data/spec/integration/invalid_header_example/output.ledger +8 -0
  71. data/spec/integration/invalid_header_example/test_args +1 -0
  72. data/spec/integration/inversed_credit_card/input.csv +16 -0
  73. data/spec/integration/inversed_credit_card/output.ledger +64 -0
  74. data/spec/integration/inversed_credit_card/test_args +1 -0
  75. data/spec/integration/nationwide/input.csv +4 -0
  76. data/spec/integration/nationwide/output.ledger +16 -0
  77. data/spec/integration/nationwide/test_args +1 -0
  78. data/spec/integration/regression/issue_51_account_tokens/input.csv +8 -0
  79. data/spec/integration/regression/issue_51_account_tokens/output.ledger +32 -0
  80. data/spec/integration/regression/issue_51_account_tokens/test_args +4 -0
  81. data/spec/integration/regression/issue_51_account_tokens/tokens.yml +9 -0
  82. data/spec/integration/regression/issue_64_date_column/input.csv +3 -0
  83. data/spec/integration/regression/issue_64_date_column/output.ledger +8 -0
  84. data/spec/integration/regression/issue_64_date_column/test_args +1 -0
  85. data/spec/integration/regression/issue_73_account_token_matching/input.csv +2 -0
  86. data/spec/integration/regression/issue_73_account_token_matching/output.ledger +4 -0
  87. data/spec/integration/regression/issue_73_account_token_matching/test_args +6 -0
  88. data/spec/integration/regression/issue_73_account_token_matching/tokens.yml +8 -0
  89. data/spec/integration/regression/issue_85_date_example/input.csv +2 -0
  90. data/spec/integration/regression/issue_85_date_example/output.ledger +8 -0
  91. data/spec/integration/regression/issue_85_date_example/test_args +1 -0
  92. data/spec/integration/spanish_date_example/input.csv +3 -0
  93. data/spec/integration/spanish_date_example/output.ledger +12 -0
  94. data/spec/integration/spanish_date_example/test_args +1 -0
  95. data/spec/integration/suntrust/input.csv +7 -0
  96. data/spec/integration/suntrust/output.ledger +28 -0
  97. data/spec/integration/suntrust/test_args +1 -0
  98. data/spec/integration/test.sh +82 -0
  99. data/spec/integration/test_money_column/input.csv +3 -0
  100. data/spec/integration/test_money_column/output.ledger +8 -0
  101. data/spec/integration/test_money_column/test_args +1 -0
  102. data/spec/integration/two_money_columns/input.csv +5 -0
  103. data/spec/integration/two_money_columns/output.ledger +20 -0
  104. data/spec/integration/two_money_columns/test_args +1 -0
  105. data/spec/integration/yyyymmdd_date_example/input.csv +1 -0
  106. data/spec/integration/yyyymmdd_date_example/output.ledger +4 -0
  107. data/spec/integration/yyyymmdd_date_example/test_args +1 -0
  108. data/spec/reckon/app_spec.rb +18 -2
  109. data/spec/reckon/csv_parser_spec.rb +129 -129
  110. data/spec/reckon/ledger_parser_spec.rb +42 -5
  111. data/spec/reckon/money_column_spec.rb +24 -24
  112. data/spec/reckon/money_spec.rb +36 -42
  113. data/spec/spec_helper.rb +19 -0
  114. metadata +97 -22
  115. data/.travis.yml +0 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a7e9512d0b15c14548a04e80f5dd3a87f50ae9ff2654827b5970def94e174667
4
- data.tar.gz: f858c96b4b5f2a7b2c85845803109e566281c0fc945c46ba39758121701d0e9a
3
+ metadata.gz: aebd4114b69fd94cc45e9cf425da57269ae80508b90f781fbbc5c96c6912b100
4
+ data.tar.gz: 0de1e3fb308bc2dad8cfa679056189092a4f07ece989b8e3b57c8acca100c77c
5
5
  SHA512:
6
- metadata.gz: 15526cfe3504d50859de7b652285b31f639b11daecd8536732b97e1f84d03814cb8040c8db830386f803de7836e11712b7069758a77bf290fab26699436440f2
7
- data.tar.gz: 99ae732793adeaa40f5c7235e690dfb80d839a31ecba52f989e0dea2d339cf012623f858c926261e4bd5eb731e2a3ff508370509d03560c64c2bd7cf4a737a7e
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
@@ -28,3 +28,5 @@ private_tests
28
28
 
29
29
  ## Bundler
30
30
  vendor
31
+
32
+ test.log
@@ -1 +1 @@
1
- 2.5
1
+ 3.0.0
@@ -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)
@@ -1,11 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- reckon (0.5.1)
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
- [![Build Status](https://travis-ci.org/cantino/reckon.png?branch=master)](https://travis-ci.org/cantino/reckon)
3
+ ![Build Status](https://github.com/cantino/reckon/workflows/Build%20Status/badge.svg)
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 name The Ledger Account this file is for
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 name
65
+ --default-into-account NAME
61
66
  Default into account
62
- --default-outof-account name
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
- If reckon can not guess the accounts it will use `Income:Unknown` or `Expenses:Unknown` names.
100
- You can override them with `--default_outof_account` and `--default_into_account` options.
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
- ## Note on Patches/Pull Requests
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 :default => :spec
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
@@ -4,16 +4,13 @@ require 'rubygems'
4
4
  require 'rchardet'
5
5
  require 'chronic'
6
6
  require 'csv'
7
- require 'highline/import'
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'
@@ -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(options = {})
11
+ def initialize(opts = {})
12
+ self.options = opts
10
13
  LOGGER.level = Logger::INFO if options[:verbose]
11
- self.options = options
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
- fail "#{filename} doesn't exist!" unless File.exists?(filename)
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 learn_from(ledger)
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
- @matcher.add_document(account[:name], str) unless account[:name] == options[:bank_account]
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
- seen[pretty_date] ||= {}
58
- seen[pretty_date][@csv_parser.pretty_money(account[:amount])] = true
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
- interactive_output Terminal::Table.new(:rows => [ [ row[:pretty_date], row[:pretty_money], row[:description] ] ])
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
- possible_answers = suggest(row)
109
-
110
- ledger = if row[:money] > 0
111
- if options[:unattended]
112
- out_of_account = possible_answers.first || options[:default_outof_account] || 'Income:Unknown'
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
- if options[:unattended]
132
- into_account = possible_answers.first || options[:default_into_account] || 'Expenses:Unknown'
133
- else
134
- into_account = ask("To which account did this money go? ([account]/[q]uit/[s]kip) ") { |q|
135
- q.completion = possible_answers
136
- q.readline = true
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
- ledger_format( row,
147
- [into_account, row[:pretty_money_negated]],
148
- [options[:bank_account], row[:pretty_money]] )
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
- learn_from(ledger) unless options[:account_tokens_file]
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 most_specific_regexp_match( row )
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! { |account, matched_text| matched_text.length }.map(&:first)
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\t\t#{line1.last}\n"
190
- out += "\t#{line2.first}\t\t\t\t\t#{line2.last}\n\n"
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[row[:pretty_date]] && seen[row[:pretty_date]][row[:pretty_money]]
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
- output = Terminal::Table.new do |t|
211
- t.headings = 'Date', 'Amount', 'Description'
212
- each_row_backwards do |row|
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
- interactive_output output
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 for the unattended mode" if options[:unattended]
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"