reckon 0.5.3 → 0.7.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.
Files changed (114) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +50 -0
  3. data/.gitignore +3 -0
  4. data/.ruby-version +1 -1
  5. data/CHANGELOG.md +77 -8
  6. data/Gemfile.lock +1 -5
  7. data/README.md +74 -21
  8. data/Rakefile +17 -1
  9. data/bin/build-new-version.sh +26 -0
  10. data/bin/reckon +6 -1
  11. data/lib/reckon.rb +2 -2
  12. data/lib/reckon/app.rb +140 -194
  13. data/lib/reckon/csv_parser.rb +8 -8
  14. data/lib/reckon/date_column.rb +10 -0
  15. data/lib/reckon/ledger_parser.rb +5 -1
  16. data/lib/reckon/money.rb +48 -48
  17. data/lib/reckon/options.rb +147 -0
  18. data/lib/reckon/version.rb +1 -1
  19. data/reckon.gemspec +1 -2
  20. data/spec/integration/another_bank_example/input.csv +9 -0
  21. data/spec/integration/another_bank_example/output.ledger +36 -0
  22. data/spec/integration/another_bank_example/test_args +1 -0
  23. data/spec/integration/austrian_example/input.csv +13 -0
  24. data/spec/integration/austrian_example/output.ledger +52 -0
  25. data/spec/integration/austrian_example/test_args +2 -0
  26. data/spec/integration/bom_utf8_file/input.csv +3 -0
  27. data/spec/integration/bom_utf8_file/output.ledger +4 -0
  28. data/spec/integration/bom_utf8_file/test_args +3 -0
  29. data/spec/integration/broker_canada_example/input.csv +12 -0
  30. data/spec/integration/broker_canada_example/output.ledger +48 -0
  31. data/spec/integration/broker_canada_example/test_args +1 -0
  32. data/spec/integration/chase/account_tokens_and_regex/output.ledger +36 -0
  33. data/spec/integration/chase/account_tokens_and_regex/test_args +2 -0
  34. data/spec/integration/chase/account_tokens_and_regex/tokens.yml +16 -0
  35. data/spec/integration/chase/default_account_names/output.ledger +36 -0
  36. data/spec/integration/chase/default_account_names/test_args +3 -0
  37. data/spec/integration/chase/input.csv +9 -0
  38. data/spec/integration/chase/learn_from_existing/learn.ledger +7 -0
  39. data/spec/integration/chase/learn_from_existing/output.ledger +36 -0
  40. data/spec/integration/chase/learn_from_existing/test_args +1 -0
  41. data/spec/integration/chase/simple/output.ledger +36 -0
  42. data/spec/integration/chase/simple/test_args +1 -0
  43. data/spec/integration/danish_kroner_nordea_example/input.csv +6 -0
  44. data/spec/integration/danish_kroner_nordea_example/output.ledger +24 -0
  45. data/spec/integration/danish_kroner_nordea_example/test_args +1 -0
  46. data/spec/integration/english_date_example/input.csv +3 -0
  47. data/spec/integration/english_date_example/output.ledger +12 -0
  48. data/spec/integration/english_date_example/test_args +1 -0
  49. data/spec/integration/extratofake/input.csv +24 -0
  50. data/spec/integration/extratofake/output.ledger +92 -0
  51. data/spec/integration/extratofake/test_args +1 -0
  52. data/spec/integration/french_example/input.csv +9 -0
  53. data/spec/integration/french_example/output.ledger +36 -0
  54. data/spec/integration/french_example/test_args +2 -0
  55. data/spec/integration/german_date_example/input.csv +3 -0
  56. data/spec/integration/german_date_example/output.ledger +12 -0
  57. data/spec/integration/german_date_example/test_args +1 -0
  58. data/spec/integration/harder_date_example/input.csv +5 -0
  59. data/spec/integration/harder_date_example/output.ledger +20 -0
  60. data/spec/integration/harder_date_example/test_args +1 -0
  61. data/spec/integration/ing/input.csv +3 -0
  62. data/spec/integration/ing/output.ledger +12 -0
  63. data/spec/integration/ing/test_args +1 -0
  64. data/spec/integration/intuit_mint_example/input.csv +7 -0
  65. data/spec/integration/intuit_mint_example/output.ledger +28 -0
  66. data/spec/integration/intuit_mint_example/test_args +1 -0
  67. data/spec/integration/invalid_header_example/input.csv +6 -0
  68. data/spec/integration/invalid_header_example/output.ledger +8 -0
  69. data/spec/integration/invalid_header_example/test_args +1 -0
  70. data/spec/integration/inversed_credit_card/input.csv +16 -0
  71. data/spec/integration/inversed_credit_card/output.ledger +64 -0
  72. data/spec/integration/inversed_credit_card/test_args +1 -0
  73. data/spec/integration/nationwide/input.csv +4 -0
  74. data/spec/integration/nationwide/output.ledger +16 -0
  75. data/spec/integration/nationwide/test_args +1 -0
  76. data/spec/integration/regression/issue_51_account_tokens/input.csv +8 -0
  77. data/spec/integration/regression/issue_51_account_tokens/output.ledger +32 -0
  78. data/spec/integration/regression/issue_51_account_tokens/test_args +4 -0
  79. data/spec/integration/regression/issue_51_account_tokens/tokens.yml +9 -0
  80. data/spec/integration/regression/issue_64_date_column/input.csv +3 -0
  81. data/spec/integration/regression/issue_64_date_column/output.ledger +8 -0
  82. data/spec/integration/regression/issue_64_date_column/test_args +1 -0
  83. data/spec/integration/regression/issue_73_account_token_matching/input.csv +2 -0
  84. data/spec/integration/regression/issue_73_account_token_matching/output.ledger +4 -0
  85. data/spec/integration/regression/issue_73_account_token_matching/test_args +6 -0
  86. data/spec/integration/regression/issue_73_account_token_matching/tokens.yml +8 -0
  87. data/spec/integration/regression/issue_85_date_example/input.csv +2 -0
  88. data/spec/integration/regression/issue_85_date_example/output.ledger +8 -0
  89. data/spec/integration/regression/issue_85_date_example/test_args +1 -0
  90. data/spec/integration/spanish_date_example/input.csv +3 -0
  91. data/spec/integration/spanish_date_example/output.ledger +12 -0
  92. data/spec/integration/spanish_date_example/test_args +1 -0
  93. data/spec/integration/suntrust/input.csv +7 -0
  94. data/spec/integration/suntrust/output.ledger +28 -0
  95. data/spec/integration/suntrust/test_args +1 -0
  96. data/spec/integration/test.sh +83 -0
  97. data/spec/integration/test_money_column/input.csv +3 -0
  98. data/spec/integration/test_money_column/output.ledger +8 -0
  99. data/spec/integration/test_money_column/test_args +1 -0
  100. data/spec/integration/two_money_columns/input.csv +5 -0
  101. data/spec/integration/two_money_columns/output.ledger +20 -0
  102. data/spec/integration/two_money_columns/test_args +1 -0
  103. data/spec/integration/yyyymmdd_date_example/input.csv +1 -0
  104. data/spec/integration/yyyymmdd_date_example/output.ledger +4 -0
  105. data/spec/integration/yyyymmdd_date_example/test_args +1 -0
  106. data/spec/reckon/app_spec.rb +25 -7
  107. data/spec/reckon/csv_parser_spec.rb +5 -0
  108. data/spec/reckon/ledger_parser_spec.rb +19 -4
  109. data/spec/reckon/money_column_spec.rb +24 -24
  110. data/spec/reckon/money_spec.rb +13 -32
  111. data/spec/reckon/options_spec.rb +17 -0
  112. data/spec/spec_helper.rb +6 -1
  113. metadata +98 -59
  114. data/.travis.yml +0 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 77139229b37c2dcb66ec4f8494fb9d40f036ed267cee0dad067483568b02b948
4
- data.tar.gz: 363a124cf17848e855dede2351f06946e799ead31b2440e586d5c01ae45e63f1
3
+ metadata.gz: 9e7bf4198ae8265e025b20821adea75c3acc7cc9493298ac6e7604b342e04bb1
4
+ data.tar.gz: 4e7e9be82a15f1aef0ab7667e4a751b7e52472673fe0f57359e5d7c5fdac80f6
5
5
  SHA512:
6
- metadata.gz: 3473f4f80d659d8369151a4b22310159d8b4231df5a24efefd9fc426fa4d27744f2e62fb2c2d17826b0ce4c9a96ef13176ca9e602a47377b6271da49c1324cae
7
- data.tar.gz: 323b5fe3aeafba7f04d93b91458d9982e75704c839bd763bf99371ba8f2b11f74d2b3a82a08bc57a56a1765f7a177955f441af03df63f844f0ae2804f842aacc
6
+ metadata.gz: eb4ce5f4bcb0b9663b5ab89c5071086bf6b13c157f4d45d1608dc1b530b076b82402b25003b4156882b8c56441f66c1cc97867cd781cb8a6b8386804eddb0673
7
+ data.tar.gz: ec5941351ad06d6bf029af5b47a9453a611c4a17aa14bddbe8b5b2cf4584bcdceb134a0c9016d32f046832ea507a611807a159a2f85a673374bbf591a063a42a
@@ -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,6 @@ private_tests
28
28
 
29
29
  ## Bundler
30
30
  vendor
31
+
32
+ test.log
33
+ test_log.txt
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.0.0-p648
1
+ 3.0.0
data/CHANGELOG.md CHANGED
@@ -1,12 +1,81 @@
1
1
  # Changelog
2
2
 
3
- ## [v0.5.3](https://github.com/cantino/reckon/tree/v0.5.3) (2020-05-01)
3
+ ## [0.7.0](https://github.com/cantino/reckon/tree/0.7.0) (2021-02-06)
4
4
 
5
- [Full Changelog](https://github.com/cantino/reckon/compare/v0.5.2...v0.5.3)
5
+ [Full Changelog](https://github.com/cantino/reckon/compare/v0.6.2...0.7.0)
6
6
 
7
7
  **Closed issues:**
8
8
 
9
+ - fail on unknown accounts [\#96](https://github.com/cantino/reckon/issues/96)
10
+
11
+ **Merged pull requests:**
12
+
13
+ - Fail on unknown account [\#102](https://github.com/cantino/reckon/pull/102) ([benprew](https://github.com/benprew))
14
+ - Joined split sentence to one [\#101](https://github.com/cantino/reckon/pull/101) ([RidaAyed](https://github.com/RidaAyed))
15
+
16
+ ## [v0.6.2](https://github.com/cantino/reckon/tree/v0.6.2) (2021-01-25)
17
+
18
+ [Full Changelog](https://github.com/cantino/reckon/compare/v0.6.1...v0.6.2)
19
+
20
+ **Closed issues:**
21
+
22
+ - spaces in tokens [\#97](https://github.com/cantino/reckon/issues/97)
23
+ - read from stdin [\#95](https://github.com/cantino/reckon/issues/95)
24
+
25
+ **Merged pull requests:**
26
+
27
+ - Allow using '-' as filename in -f to read csv from STDIN. Fixes \#95 [\#98](https://github.com/cantino/reckon/pull/98) ([benprew](https://github.com/benprew))
28
+
29
+ ## [v0.6.1](https://github.com/cantino/reckon/tree/v0.6.1) (2021-01-23)
30
+
31
+ [Full Changelog](https://github.com/cantino/reckon/compare/v0.6.0...v0.6.1)
32
+
33
+ **Implemented enhancements:**
34
+
35
+ - \[Feature Request\] Note flag --add-notes in CLI to allow additional notes for each ledger entry [\#86](https://github.com/cantino/reckon/issues/86)
36
+
37
+ **Closed issues:**
38
+
39
+ - Migrate CI system from travis-ci.org [\#93](https://github.com/cantino/reckon/issues/93)
40
+ - \[Feature Request\] Pipe ledger file input to the bayesian predictor \(instead of csv\) [\#91](https://github.com/cantino/reckon/issues/91)
41
+
42
+ **Merged pull requests:**
43
+
44
+ - Add github actions [\#100](https://github.com/cantino/reckon/pull/100) ([benprew](https://github.com/benprew))
45
+ - Add documentation for doing a substring match. Fixes \#97 [\#99](https://github.com/cantino/reckon/pull/99) ([benprew](https://github.com/benprew))
46
+ - Test fixes [\#94](https://github.com/cantino/reckon/pull/94) ([benprew](https://github.com/benprew))
47
+
48
+ ## [v0.6.0](https://github.com/cantino/reckon/tree/v0.6.0) (2020-09-04)
49
+
50
+ [Full Changelog](https://github.com/cantino/reckon/compare/v0.5.4...v0.6.0)
51
+
52
+ **Fixed bugs:**
53
+
54
+ - \[BUG\] Reckon appears not to be parsing ISO standard date yyyy-mm-dd? [\#85](https://github.com/cantino/reckon/issues/85)
55
+
56
+ **Closed issues:**
57
+
58
+ - duplicate detection [\#16](https://github.com/cantino/reckon/issues/16)
59
+
60
+ **Merged pull requests:**
61
+
62
+ - Add ability to add note to transaction when entering it [\#89](https://github.com/cantino/reckon/pull/89) ([benprew](https://github.com/benprew))
63
+
64
+ ## [v0.5.4](https://github.com/cantino/reckon/tree/v0.5.4) (2020-06-05)
65
+
66
+ [Full Changelog](https://github.com/cantino/reckon/compare/v0.5.3...v0.5.4)
67
+
68
+ **Fixed bugs:**
69
+
70
+ - order of transactions [\#88](https://github.com/cantino/reckon/issues/88)
9
71
  - Is reckon failing to handle comments when learning? [\#87](https://github.com/cantino/reckon/issues/87)
72
+
73
+ ## [v0.5.3](https://github.com/cantino/reckon/tree/v0.5.3) (2020-05-02)
74
+
75
+ [Full Changelog](https://github.com/cantino/reckon/compare/v0.5.2...v0.5.3)
76
+
77
+ **Closed issues:**
78
+
10
79
  - \[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)
11
80
 
12
81
  ## [v0.5.2](https://github.com/cantino/reckon/tree/v0.5.2) (2020-03-07)
@@ -112,7 +181,6 @@
112
181
  **Merged pull requests:**
113
182
 
114
183
  - Better ISO 8601 dates support [\#49](https://github.com/cantino/reckon/pull/49) ([vzctl](https://github.com/vzctl))
115
- - Unattended mode and custom tokens support [\#47](https://github.com/cantino/reckon/pull/47) ([vzctl](https://github.com/vzctl))
116
184
  - \[RFC\] Implement issue \#40: Tab completion [\#46](https://github.com/cantino/reckon/pull/46) ([BlackEdder](https://github.com/BlackEdder))
117
185
  - set readline to allow for backspace in ask dialog [\#44](https://github.com/cantino/reckon/pull/44) ([mrtazz](https://github.com/mrtazz))
118
186
 
@@ -136,6 +204,7 @@
136
204
 
137
205
  **Merged pull requests:**
138
206
 
207
+ - Unattended mode and custom tokens support [\#47](https://github.com/cantino/reckon/pull/47) ([vzctl](https://github.com/vzctl))
139
208
  - Added spec for csv files from Broker Canada [\#36](https://github.com/cantino/reckon/pull/36) ([BlackEdder](https://github.com/BlackEdder))
140
209
  - Date format [\#35](https://github.com/cantino/reckon/pull/35) ([BlackEdder](https://github.com/BlackEdder))
141
210
  - Added example from a french bank [\#34](https://github.com/cantino/reckon/pull/34) ([BlackEdder](https://github.com/BlackEdder))
@@ -212,15 +281,15 @@
212
281
 
213
282
  ## [v0.3.3](https://github.com/cantino/reckon/tree/v0.3.3) (2013-01-13)
214
283
 
215
- [Full Changelog](https://github.com/cantino/reckon/compare/v0.3.2...v0.3.3)
284
+ [Full Changelog](https://github.com/cantino/reckon/compare/v0.3.1...v0.3.3)
216
285
 
217
- ## [v0.3.2](https://github.com/cantino/reckon/tree/v0.3.2) (2012-07-30)
286
+ ## [v0.3.1](https://github.com/cantino/reckon/tree/v0.3.1) (2012-07-30)
218
287
 
219
- [Full Changelog](https://github.com/cantino/reckon/compare/v0.3.1...v0.3.2)
288
+ [Full Changelog](https://github.com/cantino/reckon/compare/v0.3.2...v0.3.1)
220
289
 
221
- ## [v0.3.1](https://github.com/cantino/reckon/tree/v0.3.1) (2012-07-30)
290
+ ## [v0.3.2](https://github.com/cantino/reckon/tree/v0.3.2) (2012-07-30)
222
291
 
223
- [Full Changelog](https://github.com/cantino/reckon/compare/5c07bea3fe63f9b909b4b76bd49f22fd8faf7a29...v0.3.1)
292
+ [Full Changelog](https://github.com/cantino/reckon/compare/5c07bea3fe63f9b909b4b76bd49f22fd8faf7a29...v0.3.2)
224
293
 
225
294
 
226
295
 
data/Gemfile.lock CHANGED
@@ -1,11 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- reckon (0.5.2)
4
+ reckon (0.7.0)
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,20 @@ 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
+ In unattended mode, you can use STDIN to read your csv data, by specifying `-` as the argument to `-f`.
86
+
87
+ `csv_file_generator | reckon --unattended -l 2010.dat -o ledger.dat -f -`
88
+
89
+ ### Account Tokens
90
+
91
+ 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:
92
+
93
+ Expenses:
94
+ Bank:
95
+ - 'ING Direct Deposit'
96
+
97
+ Would tokenize to 'ING', 'Direct' and 'Deposit'. The matcher would then suggest matches to transactions that included those tokens. (ex 'Chase Direct Deposit')
98
+
80
99
  Here's an example of `tokens.yaml`:
81
100
 
82
101
  ```
@@ -96,27 +115,61 @@ Expenses:
96
115
  - '4433221100' # Your own account number
97
116
  ```
98
117
 
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.
118
+ Reckon will use `Income:Unknown` or `Expenses:Unknown` if it can't match a transaction to an account.
119
+
120
+ You can override these names with the `--default_outof_account` and `--default_into_account` options.
121
+
122
+ ### Substring Match
123
+
124
+ 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:
125
+
126
+ Expenses:
127
+ Bank:
128
+ - /ING Direct Deposit/
129
+
130
+ ## Contributing
131
+
132
+ We encourage you to contribute to Reckon! Here is some information to help you.
101
133
 
102
- ## Note on Patches/Pull Requests
134
+ ### Patches/Pull Requests Process
103
135
 
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.
136
+ 1. Fork the project.
137
+ 2. Make your feature addition or bug fix.
138
+ 3. Add tests for it. This is important so I don't break it in a future version unintentionally.
139
+ 4. Commit, do not mess with rakefile, version, or history.
140
+ - (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)
141
+ 5. Send me a pull request. Bonus points for topic branches.
142
+
143
+ ### Integration Tests
144
+
145
+ Reckon has integration test located in `spec/integration`. These are integration and regression tests for reckon.
146
+
147
+ Run all the tests:
148
+
149
+ ./spec/integration/test.sh
150
+
151
+ Run a single test
152
+
153
+ ./spec/integration/test.sh chase/account_tokens_and_regex
154
+
155
+ #### Add a new integration test
156
+
157
+ Each test has it's own directory, which you can add any files you want, but the following files are required:
158
+
159
+ - `test_args` - arguments to add to the reckon command to test against, can specify `--unattended`, `-f input.csv`, etc
160
+ - `output.ledger` - the expected ledger file output
161
+
162
+ If the result of running reckon with `test_args` does not match `output.ledger`, then the test fails.
163
+
164
+ Most tests will specify `--unattended`, otherwise reckon prompts for keyboard input.
165
+
166
+ The convention is to use `input.csv` as the input file, and `tokens.yml` as the tokens file, but it is not required.
111
167
 
112
- ## Making a release
113
- * Update lib/reckon/version.rb
114
- * Run `github_changelog_generator --future-release v$(egrep '"[^"]+"' -o lib/reckon/version.rb |sed -e 's/"//g') --user cantino --project reckon -t $(cat ~/.github_token)`
115
- * Commit
116
- * Tag the commit same as in version.rb vX.XX.XX (ex v0.5.2)
117
168
 
118
169
  ## Copyright
119
170
 
120
- Copyright (c) 2013 Andrew Cantino. See LICENSE for details.
171
+ Copyright (c) 2013 Andrew Cantino (@cantino). See LICENSE for details.
121
172
 
122
173
  Thanks to @BlackEdder for many contributions!
174
+
175
+ 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
@@ -0,0 +1,26 @@
1
+ #!/bin/bash
2
+
3
+ set -e
4
+
5
+ VERSION=$1
6
+
7
+ echo "Install github_changelog_generator"
8
+ gem install --user github_changelog_generator
9
+
10
+ echo "Update 'lib/reckon/version.rb'"
11
+ echo -e "module Reckon\n VERSION=\"$VERSION\"\nend" > lib/reckon/version.rb
12
+ echo "Run `bundle install` to build updated Gemfile.lock"
13
+ bundle install
14
+ echo "3. Run changelog generator (requires $TOKEN to be your github token)"
15
+ github_changelog_generator -u cantino -p reckon -t $TOKEN --future-release $VERSION
16
+ echo "4. Commit changes"
17
+ git add CHANGELOG.md lib/reckon/version.rb Gemfile.lock
18
+ git commit -m "Release $VERSION"
19
+ echo "7. Build new gem"
20
+ gem build reckon.gemspec
21
+ echo "5. Tag release"
22
+ git tag v$VERSION
23
+ echo "Push changes and tags"
24
+ echo git push && git push --tags
25
+ echo "Push new gem"
26
+ echo gem push reckon-$VERSION.gem
data/bin/reckon CHANGED
@@ -3,7 +3,12 @@
3
3
  require 'rubygems'
4
4
  require 'reckon'
5
5
 
6
- options = Reckon::App.parse_opts
6
+ begin
7
+ options = Reckon::Options.parse
8
+ rescue RuntimeError => e
9
+ puts("ERROR: #{e}")
10
+ exit(1)
11
+ end
7
12
  reckon = Reckon::App.new(options)
8
13
 
9
14
  if options[:print_table]
data/lib/reckon.rb CHANGED
@@ -4,9 +4,8 @@ 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
 
@@ -17,4 +16,5 @@ require_relative 'reckon/date_column'
17
16
  require_relative 'reckon/money'
18
17
  require_relative 'reckon/ledger_parser'
19
18
  require_relative 'reckon/csv_parser'
19
+ require_relative 'reckon/options'
20
20
  require_relative 'reckon/app'
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(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,95 @@ 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
+ if options[:fail_on_unknown_account] && possible_answers.empty?
197
+ raise %(Couldn't find any matches for '#{row[:description]}'
198
+ Try adding an account token with --account-tokens)
199
+ end
200
+
201
+ default = options[:default_outof_account]
202
+ default = options[:default_into_account] if row[:pretty_money][0] == '-'
203
+ return possible_answers[0] || default
204
+ end
205
+
206
+ answer = @@cli.ask(msg) do |q|
207
+ q.completion = possible_answers
208
+ q.readline = true
209
+ q.default = possible_answers.first
210
+ end
211
+
212
+ # if answer isn't n/note/d/description, must be an account name, or skip, or quit
213
+ return answer unless %w[n note d description].include?(answer)
214
+
215
+ add_description(row) if %w[d description].include?(answer)
216
+ add_note(row) if %w[n note].include?(answer)
217
+
218
+ print_transaction([row])
219
+ # give user a chance to set account name or retry description
220
+ return ask_account_question(msg, row)
221
+ end
222
+
223
+ def add_description(row)
224
+ desc_answer = @@cli.ask("Enter a new description for this transaction (empty line aborts)\n") do |q|
225
+ q.overwrite = true
226
+ q.readline = true
227
+ q.default = row[:description]
228
+ end
229
+
230
+ row[:description] = desc_answer unless desc_answer.empty?
231
+ end
232
+
233
+ def add_note(row)
234
+ desc_answer = @@cli.ask("Enter a new note for this transaction (empty line aborts)\n") do |q|
235
+ q.overwrite = true
236
+ q.readline = true
237
+ q.default = row[:note]
238
+ end
239
+
240
+ row[:note] = desc_answer unless desc_answer.empty?
241
+ end
242
+
243
+ def most_specific_regexp_match(row)
174
244
  matches = regexps.map { |regexp, account|
175
245
  if match = regexp.match(row[:description])
176
246
  [account, match[0]]
177
247
  end
178
248
  }.compact
179
- matches.sort_by! { |account, matched_text| matched_text.length }.map(&:first)
249
+ matches.sort_by! { |_account, matched_text| matched_text.length }.map(&:first)
180
250
  end
181
251
 
182
252
  def suggest(row)
@@ -185,9 +255,9 @@ module Reckon
185
255
  end
186
256
 
187
257
  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"
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"
191
261
  out
192
262
  end
193
263
 
@@ -196,8 +266,12 @@ module Reckon
196
266
  options[:output_file].flush
197
267
  end
198
268
 
269
+ def seen_key(date, amount)
270
+ return [date, amount].join("|")
271
+ end
272
+
199
273
  def already_seen?(row)
200
- seen[row[:pretty_date]] && seen[row[:pretty_date]][row[:pretty_money]]
274
+ seen.include?(seen_key(row[:pretty_date], row[:pretty_money]))
201
275
  end
202
276
 
203
277
  def finish
@@ -207,139 +281,11 @@ module Reckon
207
281
  end
208
282
 
209
283
  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
215
- end
216
- interactive_output output
217
- end
218
-
219
- def self.parse_opts(args = ARGV)
220
- options = { :output_file => STDOUT }
221
- parser = OptionParser.new do |opts|
222
- opts.banner = "Usage: Reckon.rb [options]"
223
- opts.separator ""
224
-
225
- opts.on("-f", "--file FILE", "The CSV file to parse") do |file|
226
- options[:file] = file
227
- end
228
-
229
- opts.on("-a", "--account NAME", "The Ledger Account this file is for") do |a|
230
- options[:bank_account] = a
231
- end
232
-
233
- opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
234
- options[:verbose] = v
235
- end
236
-
237
- opts.on("-i", "--inverse", "Use the negative of each amount") do |v|
238
- options[:inverse] = v
239
- end
240
-
241
- opts.on("-p", "--print-table", "Print out the parsed CSV in table form") do |p|
242
- options[:print_table] = p
243
- end
244
-
245
- opts.on("-o", "--output-file FILE", "The ledger file to append to") do |o|
246
- options[:output_file] = File.open(o, 'a')
247
- end
248
-
249
- opts.on("-l", "--learn-from FILE", "An existing ledger file to learn accounts from") do |l|
250
- options[:existing_ledger_file] = l
251
- end
252
-
253
- opts.on("", "--ignore-columns 1,2,5", "Columns to ignore in the CSV file - the first column is column 1") do |ignore|
254
- options[:ignore_columns] = ignore.split(",").map { |i| i.to_i }
255
- end
256
-
257
- opts.on("", "--money-column 2", Integer, "Specify the money column instead of letting Reckon guess - the first column is column 1") do |column_number|
258
- options[:money_column] = column_number
259
- end
260
-
261
- 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
- options[:date_column] = column_number
263
- end
264
-
265
- opts.on("", "--contains-header [N]", "The first row of the CSV is a header and should be skipped. Optionally add the number of rows to skip.") do |contains_header|
266
- options[:contains_header] = 1
267
- options[:contains_header] = contains_header.to_i if contains_header
268
- end
269
-
270
- opts.on("", "--csv-separator ','", "Separator for parsing the CSV - default is comma.") do |csv_separator|
271
- options[:csv_separator] = csv_separator
272
- end
273
-
274
- opts.on("", "--comma-separates-cents", "Use comma instead of period to deliminate dollars from cents when parsing ($100,50 instead of $100.50)") do |c|
275
- options[:comma_separates_cents] = c
276
- end
277
-
278
- opts.on("", "--encoding 'UTF-8'", "Specify an encoding for the CSV file; not usually needed") do |e|
279
- options[:encoding] = e
280
- end
281
-
282
- opts.on("-c", "--currency '$'", "Currency symbol to use, defaults to $ (£, EUR)") do |e|
283
- options[:currency] = e
284
- end
285
-
286
- opts.on("", "--date-format '%d/%m/%Y'", "Force the date format (see Ruby DateTime strftime)") do |d|
287
- options[:date_format] = d
288
- end
289
-
290
- opts.on("-u", "--unattended", "Don't ask questions and guess all the accounts automatically. Used with --learn-from or --account-tokens options.") do |n|
291
- options[:unattended] = n
292
- end
293
-
294
- opts.on("-t", "--account-tokens FILE", "YAML file with manually-assigned tokens for each account (see README)") do |a|
295
- options[:account_tokens_file] = a
296
- end
297
-
298
- opts.on("", "--default-into-account NAME", "Default into account") do |a|
299
- options[:default_into_account] = a
300
- end
301
-
302
- opts.on("", "--default-outof-account NAME", "Default 'out of' account") do |a|
303
- options[:default_outof_account] = a
304
- end
305
-
306
- opts.on("", "--suffixed", "If --currency should be used as a suffix. Defaults to false.") do |e|
307
- options[:suffixed] = e
308
- end
309
-
310
- opts.on_tail("-h", "--help", "Show this message") do
311
- puts opts
312
- exit
313
- end
314
-
315
- opts.on_tail("--version", "Show version") do
316
- puts VERSION
317
- exit
318
- end
319
-
320
- opts.parse!(args)
321
- end
322
-
323
- unless options[:file]
324
- options[:file] = ask("What CSV file should I parse? ")
325
- unless options[:file].length > 0
326
- puts "\nYou must provide a CSV file to parse.\n"
327
- puts parser
328
- exit
329
- end
330
- end
331
-
332
- unless options[:bank_account]
333
- fail "Please specify --account for the unattended mode" if options[:unattended]
334
-
335
- options[:bank_account] = ask("What is the account name of this bank account in Ledger? ") do |q|
336
- q.readline = true
337
- q.validate = /^.{2,}$/
338
- q.default = "Assets:Bank:Checking"
339
- end
284
+ rows = []
285
+ each_row_backwards do |row|
286
+ rows << row
340
287
  end
341
-
342
- options
288
+ print_transaction(rows)
343
289
  end
344
290
  end
345
291
  end