reckon 0.9.6 → 0.10.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: 2f1e02a1c5dde18325b138afa66518fc2516ea30e8c39eae5cdff2808f5f8bb4
4
- data.tar.gz: 3d70362d3b8c8a3e6e147f4ca3466e35db7905e2bfe60571de9d219c330e23db
3
+ metadata.gz: 9b0141e2c8428b74039967461cb7db1b06fd4acd63edb2db8921ec2c9a2895ec
4
+ data.tar.gz: 82c6d558f8e030988114db22d9ad59f580c7275a10a019b68c02355ea6639c71
5
5
  SHA512:
6
- metadata.gz: 707f23ee5da2df3837f0ae5c520e8b62d802d812dd9ab3acef72853c5e7d27d51b7aeaa269f89c66de1b73ab0e6891fb9ced9d3f59fb11c49d7679fa84c0e4f5
7
- data.tar.gz: 73bbdf6dec9a09c23735695d8c192fde6b94df1d4931e7e79a256bed20a736e46d1503985dc29689f7f4fbaefd5c11413f4a41eb58516691b77daef99c2c8c75
6
+ metadata.gz: ec2519c7bd438012ae498b4e3a9517d6c63ae3de71bf7237043bdbeb60c589a88180a9171f0bcb5b25f1e7dda35c7f6eb9d99173d6c9905e2bc0913fe244cca6
7
+ data.tar.gz: 8f3ea58e38dc864cdcbb10b8ecb50f214f0dfa903eef6029b9c48057807d6910de3928b61c7bc4e14051dd9052f01fafcefab9bffca8dc107d516f17e5e4a427
data/.rubocop.yml CHANGED
@@ -18,3 +18,15 @@ Metrics/AbcSize:
18
18
 
19
19
  Style/NumericPredicate:
20
20
  Enabled: False
21
+
22
+ Metrics/PerceivedComplexity:
23
+ Enabled: False
24
+
25
+ Metrics/CyclomaticComplexity:
26
+ Enabled: False
27
+
28
+ Style/FormatString:
29
+ Enabled: False
30
+
31
+ Naming/MethodParameterName:
32
+ Enabled: False
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## [v0.10.0](https://github.com/cantino/reckon/tree/v0.10.0) (2024-11-27)
4
+
5
+ [Full Changelog](https://github.com/cantino/reckon/compare/v0.9.6...v0.10.0)
6
+
7
+ **Closed issues:**
8
+
9
+ - Is it possible to use reckon with only no info about incoming or going out? [\#131](https://github.com/cantino/reckon/issues/131)
10
+ - Reckon fails immediately with error about uninitialized constant `Readline` [\#129](https://github.com/cantino/reckon/issues/129)
11
+
12
+ ## [v0.9.6](https://github.com/cantino/reckon/tree/v0.9.6) (2024-03-27)
13
+
14
+ [Full Changelog](https://github.com/cantino/reckon/compare/v0.9.5...v0.9.6)
15
+
16
+ **Closed issues:**
17
+
18
+ - reckon can't learn from a file with the "wrong" date format [\#130](https://github.com/cantino/reckon/issues/130)
19
+
3
20
  ## [v0.9.5](https://github.com/cantino/reckon/tree/v0.9.5) (2024-01-08)
4
21
 
5
22
  [Full Changelog](https://github.com/cantino/reckon/compare/v0.9.4...v0.9.5)
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- reckon (0.9.6)
4
+ reckon (0.10.0)
5
5
  chronic (>= 0.3.0)
6
6
  highline (~> 2.0)
7
7
  matrix (>= 0.4.2)
data/README.md CHANGED
@@ -49,6 +49,8 @@ Learn more:
49
49
  Column number of the money columns, starts from 1 (1 or 2 columns)
50
50
  --raw-money
51
51
  Don't format money column (for stocks)
52
+ --sort DATE|DESC|AMT
53
+ Sort file by date, description, or amount
52
54
  --date-column 3
53
55
  Column number of the date column, starts from 1
54
56
  --contains-header [N]
data/lib/reckon/app.rb CHANGED
@@ -10,10 +10,11 @@ module Reckon
10
10
 
11
11
  def initialize(opts = {})
12
12
  self.options = opts
13
- LOGGER.level = Logger::INFO if options[:verbose]
13
+ LOGGER.level = options[:verbose] || Logger::WARN
14
14
 
15
15
  self.regexps = {}
16
16
  self.seen = Set.new
17
+ options[:sort] ||= :date
17
18
  @cli = HighLine.new
18
19
  @csv_parser = CSVParser.new(options)
19
20
  @matcher = CosineSimilarity.new(options)
@@ -80,7 +81,7 @@ module Reckon
80
81
 
81
82
  # Add tokens from account_tokens_file to accounts
82
83
  def extract_account_tokens(subtree, account = nil)
83
- if subtree.nil?
84
+ if subtree.nil? || !subtree
84
85
  puts "Warning: empty #{account} tree"
85
86
  {}
86
87
  elsif subtree.is_a?(Array)
@@ -141,6 +142,11 @@ module Reckon
141
142
  line2 = [options[:bank_account], row[:pretty_money]]
142
143
  end
143
144
 
145
+ if answer == '~~SKIP~~'
146
+ LOGGER.info "skipping transaction: #{row}"
147
+ next
148
+ end
149
+
144
150
  finish if %w[quit q].include?(answer)
145
151
  if %w[skip s].include?(answer)
146
152
  interactive_output "Skipping"
@@ -168,18 +174,29 @@ module Reckon
168
174
  :money => @csv_parser.money_for(index),
169
175
  :description => @csv_parser.description_for(index) }
170
176
  end
171
- rows.sort_by { |n| [n[:date], -n[:money], n[:description]] }.each { |row| yield row }
177
+ rows.sort_by do |n|
178
+ [n[options[:sort]], -n[:money], n[:description]]
179
+ end.each do |row|
180
+ yield row
181
+ end
172
182
  end
173
183
 
174
184
  def print_transaction(rows, fh = $stdout)
175
185
  str = "\n"
176
- header = %w[Date Amount Description Note]
186
+ header = %w[Date Amount Description]
187
+ header += ["Note"] if rows.map { |r| r[:note] }.any?
177
188
  maxes = header.map(&:length)
178
-
179
- rows = rows.map { |r| [r[:pretty_date], r[:pretty_money], r[:description], r[:note]] }
189
+ rows = rows.map do |r|
190
+ [r[:pretty_date], r[:pretty_money], r[:description], r[:note]].compact
191
+ end
180
192
 
181
193
  rows.each do |r|
182
- r.length.times { |i| l = r[i] ? r[i].length : 0; maxes[i] = l if maxes[i] < l }
194
+ r.length.times do |i|
195
+ l = 0
196
+ l = r[i].length if r[i]
197
+ maxes[i] ||= 0
198
+ maxes[i] = l if maxes[i] < l
199
+ end
183
200
  end
184
201
 
185
202
  header.each_with_index do |n, i|
@@ -199,6 +216,15 @@ module Reckon
199
216
  end
200
217
 
201
218
  def ask_account_question(msg, row)
219
+ # return account token if it matches
220
+ token_answer = most_specific_regexp_match(row)
221
+ if token_answer.any?
222
+ row[:note] = "Matched account token"
223
+ puts "NOTE: Matched account token"
224
+ puts token_answer[0]
225
+ return token_answer[0]
226
+ end
227
+
202
228
  possible_answers = suggest(row)
203
229
  LOGGER.info "possible_answers===> #{possible_answers.inspect}"
204
230
 
@@ -12,6 +12,7 @@ require 'set'
12
12
  # These weights and measures are used to suggest which account a transaction should be
13
13
  # assigned to.
14
14
  module Reckon
15
+ # Calculates cosine similarity for tf/idf
15
16
  class CosineSimilarity
16
17
  DocumentInfo = Struct.new(:tokens, :accounts)
17
18
 
@@ -64,13 +64,13 @@ module Reckon
64
64
  private
65
65
 
66
66
  def filter_csv
67
- if options[:ignore_columns]
68
- new_columns = []
69
- columns.each_with_index do |column, index|
70
- new_columns << column unless options[:ignore_columns].include?(index + 1)
71
- end
72
- @columns = new_columns
67
+ return unless options[:ignore_columns]
68
+
69
+ new_columns = []
70
+ columns.each_with_index do |column, index|
71
+ new_columns << (options[:ignore_columns].include?(index + 1) ? [''] * column.length : column)
73
72
  end
73
+ @columns = new_columns
74
74
  end
75
75
 
76
76
  def evaluate_columns(cols)
@@ -222,7 +222,8 @@ module Reckon
222
222
  # convert to a stringio object to handle multi-line fields
223
223
  parser_opts = {
224
224
  col_sep: separator,
225
- skip_blanks: true
225
+ skip_blanks: true,
226
+ row_sep: :auto
226
227
  }
227
228
  begin
228
229
  rows = CSV.parse(StringIO.new(data), **parser_opts)
@@ -235,7 +236,7 @@ module Reckon
235
236
  index = data.index("\n", index) + 1 # skip over newline character
236
237
  count += 1
237
238
  end
238
- rows = CSV.parse(StringIO.new(data[index..-1]), **parser_opts)
239
+ rows = CSV.parse(StringIO.new(data[index..]), **parser_opts)
239
240
  rows[0..-footer_lines_to_skip]
240
241
  end
241
242
  end
@@ -175,7 +175,7 @@ module Reckon
175
175
  end
176
176
 
177
177
  def format_row(row, line1, line2)
178
- note = row[:note] ? "\t; row[:note]" : ""
178
+ note = row[:note] ? "\t; #{row[:note]}" : ""
179
179
  out = "#{row[:pretty_date]}\t#{row[:description]}#{note}\n"
180
180
  out += "\t#{line1.first}\t\t\t#{line1.last}\n"
181
181
  out += "\t#{line2.first}\t\t\t#{line2.last}\n\n"
data/lib/reckon/logger.rb CHANGED
@@ -1,4 +1,8 @@
1
1
  module Reckon
2
2
  LOGGER = Logger.new(STDERR)
3
3
  LOGGER.level = Logger::WARN
4
+
5
+ def log(tag, msg)
6
+ LOGGER.add(Logger::WARN, msg, tag)
7
+ end
4
8
  end
@@ -4,7 +4,6 @@ module Reckon
4
4
  # Singleton class for parsing command line flags
5
5
  class Options
6
6
  def self.parse_command_line_options(args = ARGV, stdin = $stdin)
7
- cli = HighLine.new
8
7
  options = { output_file: $stdout }
9
8
  OptionParser.new do |opts|
10
9
  opts.banner = "Usage: Reckon.rb [options]"
@@ -18,8 +17,17 @@ module Reckon
18
17
  options[:bank_account] = a
19
18
  end
20
19
 
21
- opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
22
- options[:verbose] = v
20
+ options[:verbose] = Logger::WARN
21
+ opts.on("-v", "--v", "Run verbosely (show info log messages)") do
22
+ options[:verbose] = Logger::INFO
23
+ end
24
+
25
+ opts.on("", "--verbose", "Run verbosely (show info log messages)") do
26
+ options[:verbose] = Logger::INFO
27
+ end
28
+
29
+ opts.on("", "--vv", "Run very verbosely (show debug log messages)") do
30
+ options[:verbose] = Logger::DEBUG
23
31
  end
24
32
 
25
33
  opts.on("-i", "--inverse", "Use the negative of each amount") do |v|
@@ -58,6 +66,19 @@ module Reckon
58
66
  options[:raw] = n
59
67
  end
60
68
 
69
+ options[:sort] = :date
70
+ opts.on("", "--sort DATE|DESC|AMT", "Sort file by date, description, or amount") do |s|
71
+ if s == 'DESC'
72
+ options[:sort] = :description
73
+ elsif s == 'AMT'
74
+ options[:sort] = :money
75
+ elsif s == 'DATE'
76
+ options[:sort] = :date
77
+ else
78
+ raise "'#{s}' is not valid. valid sort options are DATE, DESC, AMT"
79
+ end
80
+ end
81
+
61
82
  opts.on("", "--date-column 3", Integer,
62
83
  "Column number of the date column, starts from 1") do |col|
63
84
  options[:date_column] = col
@@ -161,26 +182,35 @@ module Reckon
161
182
  options[:string] = stdin.read
162
183
  end
163
184
 
185
+ validate_options(options)
186
+
187
+ return options
188
+ end
189
+
190
+ def self.validate_options(options)
191
+ cli = HighLine.new
164
192
  unless options[:file]
165
193
  options[:file] = cli.ask("What CSV file should I parse? ")
166
- unless options[:file].empty?
167
- puts "\nYou must provide a CSV file to parse.\n"
168
- puts parser
194
+ if options[:file].empty?
195
+ puts "\nERROR: You must provide a CSV file to parse.\n"
169
196
  exit
170
197
  end
171
198
  end
172
199
 
173
200
  unless options[:bank_account]
174
- raise "Must specify --account in unattended mode" if options[:unattended]
201
+ if options[:unattended]
202
+ puts "ERROR: Must specify --account in unattended mode"
203
+ exit
204
+ end
175
205
 
176
- options[:bank_account] = cli.ask("What is this account named in Ledger?\n") do |q|
206
+ options[:bank_account] = cli.ask("What is the Ledger account name?\n") do |q|
177
207
  q.readline = true
178
208
  q.validate = /^.{2,}$/
179
209
  q.default = "Assets:Bank:Checking"
180
210
  end
181
211
  end
182
212
 
183
- return options
213
+ return true
184
214
  end
185
215
  end
186
216
  end
@@ -1,3 +1,3 @@
1
1
  module Reckon
2
- VERSION = "0.9.6"
2
+ VERSION = "0.10.0"
3
3
  end
@@ -8,7 +8,7 @@ ledger_file = ARGV[0]
8
8
  account = ARGV[1]
9
9
  seed = ARGV[2] ? ARGV[2].to_i : Random.new_seed
10
10
 
11
- ledger = Reckon::LedgerParser.new(File.new(ledger_file))
11
+ ledger = Reckon::LedgerParser.new.parse(File.new(ledger_file))
12
12
  matcher = Reckon::CosineSimilarity.new({})
13
13
 
14
14
  train = []
@@ -50,3 +50,4 @@ end
50
50
  # pp result.compact
51
51
  puts "using #{seed} as random seed"
52
52
  puts "true: #{result.count(nil)} false: #{result.count { |v| !v.nil? }}"
53
+ puts(result.filter { |v| !v.nil? })
@@ -1,11 +1,11 @@
1
1
 
2
- Date | Amount | Description | Note |
3
- 2003-12-24 | $2,105.00 | CREDIT; Some Company vendorpymt PPD ID: 5KL3832735 | |
4
- 2004-12-24 | -$116.22 | CREDIT; PAYPAL TRANSFER PPD ID: PAYPALSDSL | |
5
- 2005-12-24 | -$0.96 | DEBIT; WEBSITE-BALANCE-10DEC09 12 12/10WEBSITE-BAL | |
6
- 2006-12-24 | $0.23 | DEBIT; WEBSITE-BALANCE-17DEC09 12 12/17WEBSITE-BAL | |
7
- 2007-12-24 | $1,558.52 | CREDIT; Blarg BLARG REVENUE PPD ID: 00jah78563 | |
8
- 2008-12-24 | $3,520.00 | CREDIT; Some Company vendorpymt PPD ID: 59728JSL20 | |
9
- 2009-12-24 | -$7.00 | DEBIT; GITHUB 041287430274 CA 12/22GITHUB 04 | |
10
- 2010-12-24 | -$20.00 | CHECK; CHECK 2656 | |
11
- 2011-12-24 | -$85.00 | DEBIT; HOST 037196321563 MO 12/22SLICEHOST | |
2
+ Date | Amount | Description |
3
+ 2003-12-24 | $2,105.00 | CREDIT; Some Company vendorpymt PPD ID: 5KL3832735 |
4
+ 2004-12-24 | -$116.22 | CREDIT; PAYPAL TRANSFER PPD ID: PAYPALSDSL |
5
+ 2005-12-24 | -$0.96 | DEBIT; WEBSITE-BALANCE-10DEC09 12 12/10WEBSITE-BAL |
6
+ 2006-12-24 | $0.23 | DEBIT; WEBSITE-BALANCE-17DEC09 12 12/17WEBSITE-BAL |
7
+ 2007-12-24 | $1,558.52 | CREDIT; Blarg BLARG REVENUE PPD ID: 00jah78563 |
8
+ 2008-12-24 | $3,520.00 | CREDIT; Some Company vendorpymt PPD ID: 59728JSL20 |
9
+ 2009-12-24 | -$7.00 | DEBIT; GITHUB 041287430274 CA 12/22GITHUB 04 |
10
+ 2010-12-24 | -$20.00 | CHECK; CHECK 2656 |
11
+ 2011-12-24 | -$85.00 | DEBIT; HOST 037196321563 MO 12/22SLICEHOST |
@@ -59,7 +59,7 @@ describe Reckon::LedgerParser do
59
59
  ledger += choose(*single_line_comments) + "\n"
60
60
  ledger
61
61
  end
62
- end.check(1000) do |s|
62
+ end.check(100) do |s|
63
63
  filter_format = lambda { |n|
64
64
  [n['date'], n['desc'], n['name'],
65
65
  sprintf("%.02f", n['amount'])]
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: reckon
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.6
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Cantino
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2024-03-27 00:00:00.000000000 Z
13
+ date: 2024-11-27 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rspec