reckon 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b58a745c730c0dfaf022a98c19dfaf8568c0b3091548d70c9202f8f7c0d78ada
4
- data.tar.gz: 030d2036524a19c5eff981608133e8aac15f185b57a891e91a6f1fdabd3796c7
3
+ metadata.gz: f25c7df49774d944ebc40a73374ac593ebeac2e0b0a201e0d29f9c5ace2642e9
4
+ data.tar.gz: 27acd960b5210e7e0fb88c50acf35def784411d0b8112f0f0b8e6c4e2f5ab621
5
5
  SHA512:
6
- metadata.gz: 1daf76a19078f1707e847a7ac54aad5836ee3d49ce9c2f612bfefeb86f8c1bf9c9d1f97b7bbecb309f9f44f8097f8f2dbe0d6b6062bc376a62fc88f45bab1dfc
7
- data.tar.gz: 4784ac871dbdb6160dc53ef8a7f5d69f31ccc7f7f7ea876b97bf60941abb2d13a968f5229747dd74d0b574ecc12326485e1eaba7ea368a91162d36d3ef4f9f86
6
+ metadata.gz: ddd5fe0ef08b2718e8083cdbd9695e936737b9ff6b258808787192496675f9a371c302e9936069019821f1c865a3280488aa4396185f1e0bf6e7d84b78dc5c0a
7
+ data.tar.gz: f611a48dad98a6a4764518dcb2e72969898d6dfbdaf5e06a77f36ab071429ac6f3bd0b0f51200dc8fbaaca913a95519a6bb38e6ec203dc0af61d861802d101f6
data/.gitignore CHANGED
@@ -25,3 +25,6 @@ pkg
25
25
  .idea
26
26
  reckon_local
27
27
  private_tests
28
+
29
+ ## Bundler
30
+ vendor
@@ -1 +1 @@
1
- 2.5
1
+ 2.0.0-p648
@@ -1,5 +1,59 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.6.0](https://github.com/cantino/reckon/tree/0.6.0) (2020-09-03)
4
+
5
+ [Full Changelog](https://github.com/cantino/reckon/compare/v0.5.4...0.6.0)
6
+
7
+ **Fixed bugs:**
8
+
9
+ - \[BUG\] Reckon appears not to be parsing ISO standard date yyyy-mm-dd? [\#85](https://github.com/cantino/reckon/issues/85)
10
+
11
+ **Closed issues:**
12
+
13
+ - duplicate detection [\#16](https://github.com/cantino/reckon/issues/16)
14
+
15
+ **Merged pull requests:**
16
+
17
+ - Add ability to add note to transaction when entering it [\#89](https://github.com/cantino/reckon/pull/89) ([benprew](https://github.com/benprew))
18
+
19
+ ## [v0.5.4](https://github.com/cantino/reckon/tree/v0.5.4) (2020-06-05)
20
+
21
+ [Full Changelog](https://github.com/cantino/reckon/compare/v0.5.3...v0.5.4)
22
+
23
+ **Fixed bugs:**
24
+
25
+ - order of transactions [\#88](https://github.com/cantino/reckon/issues/88)
26
+ - Is reckon failing to handle comments when learning? [\#87](https://github.com/cantino/reckon/issues/87)
27
+
28
+ ## [v0.5.3](https://github.com/cantino/reckon/tree/v0.5.3) (2020-05-02)
29
+
30
+ [Full Changelog](https://github.com/cantino/reckon/compare/v0.5.2...v0.5.3)
31
+
32
+ **Closed issues:**
33
+
34
+ - \[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)
35
+
36
+ ## [v0.5.2](https://github.com/cantino/reckon/tree/v0.5.2) (2020-03-07)
37
+
38
+ [Full Changelog](https://github.com/cantino/reckon/compare/v0.5.1...v0.5.2)
39
+
40
+ **Closed issues:**
41
+
42
+ - \[Bug\]? Reckon fails to run on ruby 2.7.0 on Catalina [\#83](https://github.com/cantino/reckon/issues/83)
43
+ - --account-tokens issue [\#51](https://github.com/cantino/reckon/issues/51)
44
+
45
+ ## [v0.5.1](https://github.com/cantino/reckon/tree/v0.5.1) (2020-02-25)
46
+
47
+ [Full Changelog](https://github.com/cantino/reckon/compare/v0.5.0...v0.5.1)
48
+
49
+ **Closed issues:**
50
+
51
+ - Error Importing [\#64](https://github.com/cantino/reckon/issues/64)
52
+
53
+ **Merged pull requests:**
54
+
55
+ - guard against rows that don't parse dates [\#82](https://github.com/cantino/reckon/pull/82) ([benprew](https://github.com/benprew))
56
+
3
57
  ## [v0.5.0](https://github.com/cantino/reckon/tree/v0.5.0) (2020-02-19)
4
58
 
5
59
  [Full Changelog](https://github.com/cantino/reckon/compare/v0.4.4...v0.5.0)
@@ -144,7 +198,6 @@
144
198
 
145
199
  **Merged pull requests:**
146
200
 
147
- - Updated the sources to allow for custom curreny [\#11](https://github.com/cantino/reckon/pull/11) ([ghost](https://github.com/ghost))
148
201
  - Add --account option on the commandline [\#10](https://github.com/cantino/reckon/pull/10) ([copiousfreetime](https://github.com/copiousfreetime))
149
202
 
150
203
  ## [v0.3.6](https://github.com/cantino/reckon/tree/v0.3.6) (2013-04-30)
@@ -178,6 +231,7 @@
178
231
 
179
232
  **Merged pull requests:**
180
233
 
234
+ - Updated the sources to allow for custom curreny [\#11](https://github.com/cantino/reckon/pull/11) ([ghost](https://github.com/ghost))
181
235
  - adds support for Nordea csv files [\#1](https://github.com/cantino/reckon/pull/1) ([x2q](https://github.com/x2q))
182
236
 
183
237
  ## [v0.3.3](https://github.com/cantino/reckon/tree/v0.3.3) (2013-01-13)
@@ -1,11 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- reckon (0.5.0)
4
+ reckon (0.6.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
@@ -8,7 +8,7 @@ Reckon automagically converts CSV files for use with the command-line accounting
8
8
 
9
9
  Assuming you have Ruby and [Rubygems](http://rubygems.org/pages/download) installed on your system, simply run
10
10
 
11
- (sudo) gem install reckon
11
+ gem install --user reckon
12
12
 
13
13
  ## Example Usage
14
14
 
@@ -4,18 +4,16 @@ 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(STDOUT)
14
- LOGGER.level = Logger::ERROR
15
-
16
12
  require_relative 'reckon/version'
13
+ require_relative 'reckon/logger'
17
14
  require_relative 'reckon/cosine_similarity'
18
- require File.expand_path(File.join(File.dirname(__FILE__), "reckon", "app"))
19
- require File.expand_path(File.join(File.dirname(__FILE__), "reckon", "ledger_parser"))
20
- require File.expand_path(File.join(File.dirname(__FILE__), "reckon", "csv_parser"))
21
- require File.expand_path(File.join(File.dirname(__FILE__), "reckon", "money"))
15
+ require_relative 'reckon/date_column'
16
+ require_relative 'reckon/money'
17
+ require_relative 'reckon/ledger_parser'
18
+ require_relative 'reckon/csv_parser'
19
+ require_relative 'reckon/app'
@@ -1,18 +1,19 @@
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
11
  def initialize(options = {})
10
12
  LOGGER.level = Logger::INFO if options[:verbose]
11
13
  self.options = options
12
14
  self.regexps = {}
13
- self.seen = {}
15
+ self.seen = Set.new
14
16
  self.options[:currency] ||= '$'
15
- options[:string] = File.read(options[:file]) unless options[:string]
16
17
  @csv_parser = CSVParser.new( options )
17
18
  @matcher = CosineSimilarity.new(options)
18
19
  learn!
@@ -20,22 +21,19 @@ module Reckon
20
21
 
21
22
  def interactive_output(str)
22
23
  return if options[:unattended]
24
+
23
25
  puts str
24
26
  end
25
27
 
26
28
  def learn!
27
29
  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))
30
+ learn_from_ledger_file(options[:existing_ledger_file])
33
31
  end
34
32
 
35
33
  def learn_from_account_tokens(filename)
36
34
  return unless filename
37
35
 
38
- fail "#{filename} doesn't exist!" unless File.exists?(filename)
36
+ raise "#{filename} doesn't exist!" unless File.exist?(filename)
39
37
 
40
38
  extract_account_tokens(YAML.load_file(filename)).each do |account, tokens|
41
39
  tokens.each do |t|
@@ -48,14 +46,27 @@ module Reckon
48
46
  end
49
47
  end
50
48
 
51
- def learn_from(ledger)
49
+ def learn_from_ledger_file(ledger_file)
50
+ return unless ledger_file
51
+
52
+ raise "#{ledger_file} doesn't exist!" unless File.exist?(ledger_file)
53
+
54
+ learn_from_ledger(File.read(ledger_file))
55
+ end
56
+
57
+ def learn_from_ledger(ledger)
58
+ LOGGER.info "learning from #{ledger}"
52
59
  LedgerParser.new(ledger).entries.each do |entry|
53
60
  entry[:accounts].each do |account|
54
61
  str = [entry[:desc], account[:amount]].join(" ")
55
- @matcher.add_document(account[:name], str) unless account[:name] == options[:bank_account]
62
+ if account[:name] != options[:bank_account]
63
+ LOGGER.info "adding document #{account[:name]} #{str}"
64
+ @matcher.add_document(account[:name], str)
65
+ end
56
66
  pretty_date = entry[:date].iso8601
57
- seen[pretty_date] ||= {}
58
- seen[pretty_date][@csv_parser.pretty_money(account[:amount])] = true
67
+ if account[:name] == options[:bank_account]
68
+ seen << seen_key(pretty_date, @csv_parser.pretty_money(account[:amount]))
69
+ end
59
70
  end
60
71
  end
61
72
  end
@@ -91,9 +102,10 @@ module Reckon
91
102
  end
92
103
 
93
104
  def walk_backwards
105
+ cmd_options = "[account]/[q]uit/[s]kip/[n]ote/[d]escription"
94
106
  seen_anything_new = false
95
107
  each_row_backwards do |row|
96
- interactive_output Terminal::Table.new(:rows => [ [ row[:pretty_date], row[:pretty_money], row[:description] ] ])
108
+ print_transaction([row])
97
109
 
98
110
  if already_seen?(row)
99
111
  interactive_output "NOTE: This row is very similar to a previous one!"
@@ -105,50 +117,28 @@ module Reckon
105
117
  seen_anything_new = true
106
118
  end
107
119
 
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]] )
120
+ if row[:money] > 0
121
+ # out_of_account
122
+ answer = ask_account_question("Which account provided this income? (#{cmd_options})", row)
123
+ line1 = [options[:bank_account], row[:pretty_money]]
124
+ line2 = [answer, ""]
130
125
  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
126
+ # into_account
127
+ answer = ask_account_question("To which account did this money go? (#{cmd_options})", row)
128
+ # line1 = [answer, row[:pretty_money_negated]]
129
+ line1 = [answer, ""]
130
+ line2 = [options[:bank_account], row[:pretty_money]]
131
+ end
145
132
 
146
- ledger_format( row,
147
- [into_account, row[:pretty_money_negated]],
148
- [options[:bank_account], row[:pretty_money]] )
133
+ finish if %w[quit q].include?(answer)
134
+ if %w[skip s].include?(answer)
135
+ interactive_output "Skipping"
136
+ next
149
137
  end
150
138
 
151
- learn_from(ledger) unless options[:account_tokens_file]
139
+ ledger = ledger_format(row, line1, line2)
140
+ LOGGER.info "ledger line: #{ledger}"
141
+ learn_from_ledger(ledger) unless options[:account_tokens_file]
152
142
  output(ledger)
153
143
  end
154
144
  end
@@ -156,6 +146,10 @@ module Reckon
156
146
  def each_row_backwards
157
147
  rows = []
158
148
  (0...@csv_parser.columns.first.length).to_a.each do |index|
149
+ if @csv_parser.date_for(index).nil?
150
+ LOGGER.warn("Skipping row: '#{@csv_parser.row(index)}' that doesn't have a valid date")
151
+ next
152
+ end
159
153
  rows << { :date => @csv_parser.date_for(index),
160
154
  :pretty_date => @csv_parser.pretty_date_for(index),
161
155
  :pretty_money => @csv_parser.pretty_money_for(index),
@@ -163,18 +157,93 @@ module Reckon
163
157
  :money => @csv_parser.money_for(index),
164
158
  :description => @csv_parser.description_for(index) }
165
159
  end
166
- rows.sort { |a, b| a[:date] <=> b[:date] }.each do |row|
167
- yield row
160
+ rows.sort_by { |n| n[:date] }.each { |row| yield row }
161
+ end
162
+
163
+ def print_transaction(rows)
164
+ str = "\n"
165
+ header = %w[Date Amount Description Note]
166
+ maxes = header.map(&:length)
167
+
168
+ rows = rows.map { |r| [r[:pretty_date], r[:pretty_money], r[:description], r[:note]] }
169
+
170
+ rows.each do |r|
171
+ r.length.times { |i| l = r[i] ? r[i].length : 0; maxes[i] = l if maxes[i] < l }
168
172
  end
173
+
174
+ header.each_with_index do |n, i|
175
+ str += " #{n.center(maxes[i])} |"
176
+ end
177
+ str += "\n"
178
+
179
+ rows.each do |row|
180
+ row.each_with_index do |_, i|
181
+ just = maxes[i]
182
+ str += sprintf(" %#{just}s |", row[i])
183
+ end
184
+ str += "\n"
185
+ end
186
+
187
+ interactive_output str
188
+ end
189
+
190
+ def ask_account_question(msg, row)
191
+ possible_answers = suggest(row)
192
+ LOGGER.info "possible_answers===> #{possible_answers.inspect}"
193
+
194
+ if options[:unattended]
195
+ default = if row[:pretty_money][0] == '-'
196
+ options[:default_into_account] || 'Expenses:Unknown'
197
+ else
198
+ options[:default_outof_account] || 'Income:Unknown'
199
+ end
200
+ return possible_answers[0] || default
201
+ end
202
+
203
+ answer = @@cli.ask(msg) do |q|
204
+ q.completion = possible_answers
205
+ q.readline = true
206
+ q.default = possible_answers.first
207
+ end
208
+
209
+ # if answer isn't n/note/d/description, must be an account name, or skip, or quit
210
+ return answer unless %w[n note d description].include?(answer)
211
+
212
+ add_description(row) if %w[d description].include?(answer)
213
+ add_note(row) if %w[n note].include?(answer)
214
+
215
+ print_transaction([row])
216
+ # give user a chance to set account name or retry description
217
+ return ask_account_question(msg, row)
169
218
  end
170
219
 
171
- def most_specific_regexp_match( row )
220
+ def add_description(row)
221
+ desc_answer = @@cli.ask("Enter a new description for this transaction (empty line aborts)\n") do |q|
222
+ q.overwrite = true
223
+ q.readline = true
224
+ q.default = row[:description]
225
+ end
226
+
227
+ row[:description] = desc_answer unless desc_answer.empty?
228
+ end
229
+
230
+ def add_note(row)
231
+ desc_answer = @@cli.ask("Enter a new note for this transaction (empty line aborts)\n") do |q|
232
+ q.overwrite = true
233
+ q.readline = true
234
+ q.default = row[:note]
235
+ end
236
+
237
+ row[:note] = desc_answer unless desc_answer.empty?
238
+ end
239
+
240
+ def most_specific_regexp_match(row)
172
241
  matches = regexps.map { |regexp, account|
173
242
  if match = regexp.match(row[:description])
174
243
  [account, match[0]]
175
244
  end
176
245
  }.compact
177
- matches.sort_by! { |account, matched_text| matched_text.length }.map(&:first)
246
+ matches.sort_by! { |_account, matched_text| matched_text.length }.map(&:first)
178
247
  end
179
248
 
180
249
  def suggest(row)
@@ -183,9 +252,9 @@ module Reckon
183
252
  end
184
253
 
185
254
  def ledger_format(row, line1, line2)
186
- out = "#{row[:pretty_date]}\t#{row[:description]}\n"
187
- out += "\t#{line1.first}\t\t\t\t\t#{line1.last}\n"
188
- out += "\t#{line2.first}\t\t\t\t\t#{line2.last}\n\n"
255
+ out = "#{row[:pretty_date]}\t#{row[:description]}\t; #{row[:note]}\n"
256
+ out += "\t#{line1.first}\t\t\t#{line1.last}\n"
257
+ out += "\t#{line2.first}\t\t\t#{line2.last}\n\n"
189
258
  out
190
259
  end
191
260
 
@@ -194,8 +263,12 @@ module Reckon
194
263
  options[:output_file].flush
195
264
  end
196
265
 
266
+ def seen_key(date, amount)
267
+ return [date, amount].join("|")
268
+ end
269
+
197
270
  def already_seen?(row)
198
- seen[row[:pretty_date]] && seen[row[:pretty_date]][row[:pretty_money]]
271
+ seen.include?(seen_key(row[:pretty_date], row[:pretty_money]))
199
272
  end
200
273
 
201
274
  def finish
@@ -205,13 +278,11 @@ module Reckon
205
278
  end
206
279
 
207
280
  def output_table
208
- output = Terminal::Table.new do |t|
209
- t.headings = 'Date', 'Amount', 'Description'
210
- each_row_backwards do |row|
211
- t << [ row[:pretty_date], row[:pretty_money], row[:description] ]
212
- end
281
+ rows = []
282
+ each_row_backwards do |row|
283
+ rows << row
213
284
  end
214
- interactive_output output
285
+ print_transaction(rows)
215
286
  end
216
287
 
217
288
  def self.parse_opts(args = ARGV)
@@ -319,7 +390,7 @@ module Reckon
319
390
  end
320
391
 
321
392
  unless options[:file]
322
- options[:file] = ask("What CSV file should I parse? ")
393
+ options[:file] = @@cli.ask("What CSV file should I parse? ")
323
394
  unless options[:file].length > 0
324
395
  puts "\nYou must provide a CSV file to parse.\n"
325
396
  puts parser
@@ -330,7 +401,7 @@ module Reckon
330
401
  unless options[:bank_account]
331
402
  fail "Please specify --account for the unattended mode" if options[:unattended]
332
403
 
333
- options[:bank_account] = ask("What is the account name of this bank account in Ledger? ") do |q|
404
+ options[:bank_account] = @@cli.ask("What is the account name of this bank account in Ledger? ") do |q|
334
405
  q.readline = true
335
406
  q.validate = /^.{2,}$/
336
407
  q.default = "Assets:Bank:Checking"