reckon 0.5.2 → 0.6.2

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 (112) 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 +66 -2
  6. data/Gemfile.lock +1 -5
  7. data/README.md +76 -16
  8. data/Rakefile +17 -1
  9. data/bin/reckon +6 -1
  10. data/lib/reckon.rb +2 -5
  11. data/lib/reckon/app.rb +156 -73
  12. data/lib/reckon/cosine_similarity.rb +91 -89
  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 +11 -1
  16. data/lib/reckon/logger.rb +4 -0
  17. data/lib/reckon/money.rb +48 -48
  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 +82 -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 +18 -2
  107. data/spec/reckon/csv_parser_spec.rb +5 -0
  108. data/spec/reckon/ledger_parser_spec.rb +42 -5
  109. data/spec/reckon/money_column_spec.rb +24 -24
  110. data/spec/reckon/money_spec.rb +13 -32
  111. metadata +94 -21
  112. data/.travis.yml +0 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 70bc1d3d98a4ba08a3bca57069073f165e68041a5e628475b6c6076550f6e419
4
- data.tar.gz: 99daf95abf45fd4dd5549d08bb9f3816af596cd3790667ca224baf7d46053ed0
3
+ metadata.gz: 5debbf17f216c822d753863d7c4adf18318beaf06f608d9d145e7f07b2a0b91b
4
+ data.tar.gz: f1d393d4aae4cd646794c2b6388a77f2dcf0b154874b6a379922ab7b05a8da92
5
5
  SHA512:
6
- metadata.gz: 6c0790695ec045e5210d20de85e17da49868b396ad78e334f24fdae7511969552a72df90b649b781c8b0e7ddf046cf8f84e2b47e212c576048f86798389bb449
7
- data.tar.gz: 18babc20d956315d9abed0cec59503f0859844a48e3cf5ee59d743dda487de762a9ab9787074e7553128d41834938f5eedeb2f92e5fda4fb1ab73939f81d0eb2
6
+ metadata.gz: e35912bb53131baa9a150ce05eede60946fd0101566e84fd9ab21476fbed54cadc011e56d18b29502d3b2c020cd325d6e8883f9f7827485bab1d8e4e6907a853
7
+ data.tar.gz: 4e67777ef46d71051372c41ec5be831cf8a6945d2da12d84bfc346b58db5604f79c1b1cfcc1471dd445a8767cc5ec47c4a465af9c41c9c830f38879ed3c3b6e4
@@ -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,12 +1,76 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.6.2](https://github.com/cantino/reckon/tree/0.6.2) (2021-01-24)
4
+
5
+ [Full Changelog](https://github.com/cantino/reckon/compare/v0.6.1...0.6.2)
6
+
7
+ **Closed issues:**
8
+
9
+ - spaces in tokens [\#97](https://github.com/cantino/reckon/issues/97)
10
+ - read from stdin [\#95](https://github.com/cantino/reckon/issues/95)
11
+
12
+ **Merged pull requests:**
13
+
14
+ - 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))
15
+
16
+ ## [v0.6.1](https://github.com/cantino/reckon/tree/v0.6.1) (2021-01-23)
17
+
18
+ [Full Changelog](https://github.com/cantino/reckon/compare/v0.6.0...v0.6.1)
19
+
20
+ **Implemented enhancements:**
21
+
22
+ - \[Feature Request\] Note flag --add-notes in CLI to allow additional notes for each ledger entry [\#86](https://github.com/cantino/reckon/issues/86)
23
+
24
+ **Closed issues:**
25
+
26
+ - Migrate CI system from travis-ci.org [\#93](https://github.com/cantino/reckon/issues/93)
27
+ - \[Feature Request\] Pipe ledger file input to the bayesian predictor \(instead of csv\) [\#91](https://github.com/cantino/reckon/issues/91)
28
+
29
+ **Merged pull requests:**
30
+
31
+ - Add github actions [\#100](https://github.com/cantino/reckon/pull/100) ([benprew](https://github.com/benprew))
32
+ - Add documentation for doing a substring match. Fixes \#97 [\#99](https://github.com/cantino/reckon/pull/99) ([benprew](https://github.com/benprew))
33
+ - Test fixes [\#94](https://github.com/cantino/reckon/pull/94) ([benprew](https://github.com/benprew))
34
+
35
+ ## [v0.6.0](https://github.com/cantino/reckon/tree/v0.6.0) (2020-09-04)
36
+
37
+ [Full Changelog](https://github.com/cantino/reckon/compare/v0.5.4...v0.6.0)
38
+
39
+ **Fixed bugs:**
40
+
41
+ - \[BUG\] Reckon appears not to be parsing ISO standard date yyyy-mm-dd? [\#85](https://github.com/cantino/reckon/issues/85)
42
+
43
+ **Closed issues:**
44
+
45
+ - duplicate detection [\#16](https://github.com/cantino/reckon/issues/16)
46
+
47
+ **Merged pull requests:**
48
+
49
+ - Add ability to add note to transaction when entering it [\#89](https://github.com/cantino/reckon/pull/89) ([benprew](https://github.com/benprew))
50
+
51
+ ## [v0.5.4](https://github.com/cantino/reckon/tree/v0.5.4) (2020-06-05)
52
+
53
+ [Full Changelog](https://github.com/cantino/reckon/compare/v0.5.3...v0.5.4)
54
+
55
+ **Fixed bugs:**
56
+
57
+ - order of transactions [\#88](https://github.com/cantino/reckon/issues/88)
58
+ - Is reckon failing to handle comments when learning? [\#87](https://github.com/cantino/reckon/issues/87)
59
+
60
+ ## [v0.5.3](https://github.com/cantino/reckon/tree/v0.5.3) (2020-05-02)
61
+
62
+ [Full Changelog](https://github.com/cantino/reckon/compare/v0.5.2...v0.5.3)
63
+
64
+ **Closed issues:**
65
+
66
+ - \[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)
67
+
3
68
  ## [v0.5.2](https://github.com/cantino/reckon/tree/v0.5.2) (2020-03-07)
4
69
 
5
70
  [Full Changelog](https://github.com/cantino/reckon/compare/v0.5.1...v0.5.2)
6
71
 
7
72
  **Closed issues:**
8
73
 
9
- - \[BUG\] Reckon appears not to be parsing ISO standard date yyyy-mm-dd? [\#85](https://github.com/cantino/reckon/issues/85)
10
74
  - \[Bug\]? Reckon fails to run on ruby 2.7.0 on Catalina [\#83](https://github.com/cantino/reckon/issues/83)
11
75
  - --account-tokens issue [\#51](https://github.com/cantino/reckon/issues/51)
12
76
 
@@ -114,7 +178,6 @@
114
178
 
115
179
  **Merged pull requests:**
116
180
 
117
- - Fix --encoding option [\#41](https://github.com/cantino/reckon/pull/41) ([mamciek](https://github.com/mamciek))
118
181
  - Bumped version number [\#37](https://github.com/cantino/reckon/pull/37) ([BlackEdder](https://github.com/BlackEdder))
119
182
 
120
183
  ## [v0.3.9](https://github.com/cantino/reckon/tree/v0.3.9) (2014-02-20)
@@ -128,6 +191,7 @@
128
191
 
129
192
  **Merged pull requests:**
130
193
 
194
+ - Fix --encoding option [\#41](https://github.com/cantino/reckon/pull/41) ([mamciek](https://github.com/mamciek))
131
195
  - Added spec for csv files from Broker Canada [\#36](https://github.com/cantino/reckon/pull/36) ([BlackEdder](https://github.com/BlackEdder))
132
196
  - Date format [\#35](https://github.com/cantino/reckon/pull/35) ([BlackEdder](https://github.com/BlackEdder))
133
197
  - Added example from a french bank [\#34](https://github.com/cantino/reckon/pull/34) ([BlackEdder](https://github.com/BlackEdder))
@@ -1,11 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- reckon (0.5.1)
4
+ reckon (0.6.2)
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,21 +115,62 @@ 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.
133
+
134
+ ### Patches/Pull Requests Process
101
135
 
102
- ## Note on Patches/Pull Requests
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
139
+ 4. future version unintentionally.
140
+ 5. Commit, do not mess with rakefile, version, or history.
141
+ - (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)
142
+ 6. Send me a pull request. Bonus points for topic branches.
143
+
144
+ ### Integration Tests
145
+
146
+ Reckon has integration test located in `spec/integration`. These are integration and regression tests for reckon.
147
+
148
+ Run all the tests:
149
+
150
+ ./spec/integration/test.sh
151
+
152
+ Run a single test
153
+
154
+ ./spec/integration/test.sh chase/account_tokens_and_regex
155
+
156
+ #### Add a new integration test
157
+
158
+ Each test has it's own directory, which you can add any files you want, but the following files are required:
159
+
160
+ - `test_args` - arguments to add to the reckon command to test against, can specify `--unattended`, `-f input.csv`, etc
161
+ - `output.ledger` - the expected ledger file output
162
+
163
+ If the result of running reckon with `test_args` does not match `output.ledger`, then the test fails.
164
+
165
+ Most tests will specify `--unattended`, otherwise reckon prompts for keyboard input.
166
+
167
+ The convention is to use `input.csv` as the input file, and `tokens.yml` as the tokens file, but it is not required.
103
168
 
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
169
 
112
170
  ## Copyright
113
171
 
114
- Copyright (c) 2013 Andrew Cantino. See LICENSE for details.
172
+ Copyright (c) 2013 Andrew Cantino (@cantino). See LICENSE for details.
115
173
 
116
174
  Thanks to @BlackEdder for many contributions!
175
+
176
+ 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
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::App.parse_opts
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]
@@ -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,18 +279,16 @@ 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
- def self.parse_opts(args = ARGV)
289
+ def self.parse_opts(args=ARGV, stdin=STDIN)
220
290
  options = { :output_file => STDOUT }
221
- parser = OptionParser.new do |opts|
291
+ OptionParser.new do |opts|
222
292
  opts.banner = "Usage: Reckon.rb [options]"
223
293
  opts.separator ""
224
294
 
@@ -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
@@ -320,8 +394,17 @@ module Reckon
320
394
  opts.parse!(args)
321
395
  end
322
396
 
397
+ if options[:file] == '-'
398
+ unless options[:unattended]
399
+ raise "--unattended is required to use STDIN as CSV source."
400
+ end
401
+
402
+ puts "Reading csv from STDIN"
403
+ options[:string] = stdin.read
404
+ end
405
+
323
406
  unless options[:file]
324
- options[:file] = ask("What CSV file should I parse? ")
407
+ options[:file] = @@cli.ask("What CSV file should I parse? ")
325
408
  unless options[:file].length > 0
326
409
  puts "\nYou must provide a CSV file to parse.\n"
327
410
  puts parser
@@ -330,9 +413,9 @@ module Reckon
330
413
  end
331
414
 
332
415
  unless options[:bank_account]
333
- fail "Please specify --account for the unattended mode" if options[:unattended]
416
+ fail "Please specify --account in unattended mode" if options[:unattended]
334
417
 
335
- options[:bank_account] = ask("What is the account name of this bank account in Ledger? ") do |q|
418
+ options[:bank_account] = @@cli.ask("What is the account name of this bank account in Ledger? ") do |q|
336
419
  q.readline = true
337
420
  q.validate = /^.{2,}$/
338
421
  q.default = "Assets:Bank:Checking"