lib/reckon/app.rb in reckon-0.9.0 vs lib/reckon/app.rb in reckon-0.9.1
- old
+ new
@@ -1,36 +1,41 @@
-# coding: utf-8
+# frozen_string_literal: true
-require 'pp'
require 'yaml'
+require 'stringio'
module Reckon
+ # The main app
class App
attr_accessor :options, :seen, :csv_parser, :regexps, :matcher
- @@cli = HighLine.new
def initialize(opts = {})
self.options = opts
LOGGER.level = Logger::INFO if options[:verbose]
self.regexps = {}
self.seen = Set.new
- self.options[:currency] ||= '$'
- @csv_parser = CSVParser.new( options )
+ @cli = HighLine.new
+ @csv_parser = CSVParser.new(options)
@matcher = CosineSimilarity.new(options)
+ @parser = options[:format] =~ /beancount/i ? BeancountParser.new : LedgerParser.new
learn!
end
def interactive_output(str, fh = $stdout)
return if options[:unattended]
fh.puts str
end
+ # Learn from previous transactions. Used to recommend accounts for a transaction.
def learn!
learn_from_account_tokens(options[:account_tokens_file])
learn_from_ledger_file(options[:existing_ledger_file])
+ # TODO: make this work
+ # this doesn't work because output_file is an IO object
+ # learn_from_ledger_file(options[:output_file]) if File.exist?(options[:output_file])
end
def learn_from_account_tokens(filename)
return unless filename
@@ -50,16 +55,17 @@
def learn_from_ledger_file(ledger_file)
return unless ledger_file
raise "#{ledger_file} doesn't exist!" unless File.exist?(ledger_file)
- learn_from_ledger(File.read(ledger_file))
+ learn_from_ledger(File.new(ledger_file))
end
+ # Takes an IO-like object
def learn_from_ledger(ledger)
LOGGER.info "learning from #{ledger}"
- LedgerParser.new(ledger).entries.each do |entry|
+ @parser.parse(ledger).each do |entry|
entry[:accounts].each do |account|
str = [entry[:desc], account[:amount]].join(" ")
if account[:name] != options[:bank_account]
LOGGER.info "adding document #{account[:name]} #{str}"
@matcher.add_document(account[:name], str)
@@ -82,18 +88,19 @@
else
at = subtree.map do |k, v|
merged_acct = [account, k].compact.join(':')
extract_account_tokens(v, merged_acct)
end
- at.inject({}) { |memo, e| memo.merge!(e)}
+ at.inject({}) { |memo, e| memo.merge!(e) }
end
end
def add_regexp(account, regex_str)
# https://github.com/tenderlove/psych/blob/master/lib/psych/visitors/to_ruby.rb
match = regex_str.match(/^\/(.*)\/([ix]*)$/m)
fail "failed to parse regexp #{regex_str}" unless match
+
options = 0
(match[2] || '').split('').each do |option|
case option
when 'x' then options |= Regexp::EXTENDED
when 'i' then options |= Regexp::IGNORECASE
@@ -118,30 +125,33 @@
seen_anything_new = true
end
if row[:money] > 0
# out_of_account
- answer = ask_account_question("Which account provided this income? (#{cmd_options})", row)
+ answer = ask_account_question(
+ "Which account provided this income? (#{cmd_options})", row
+ )
line1 = [options[:bank_account], row[:pretty_money]]
line2 = [answer, ""]
else
# into_account
- answer = ask_account_question("To which account did this money go? (#{cmd_options})", row)
-# line1 = [answer, row[:pretty_money_negated]]
+ answer = ask_account_question(
+ "To which account did this money go? (#{cmd_options})", row
+ )
line1 = [answer, ""]
line2 = [options[:bank_account], row[:pretty_money]]
end
finish if %w[quit q].include?(answer)
if %w[skip s].include?(answer)
interactive_output "Skipping"
next
end
- ledger = ledger_format(row, line1, line2)
+ ledger = @parser.format_row(row, line1, line2)
LOGGER.info "ledger line: #{ledger}"
- learn_from_ledger(ledger) unless options[:account_tokens_file]
+ learn_from_ledger(StringIO.new(ledger)) unless options[:account_tokens_file]
output(ledger)
end
end
def each_row_backwards
@@ -201,11 +211,11 @@
default = options[:default_outof_account]
default = options[:default_into_account] if row[:pretty_money][0] == '-'
return possible_answers[0] || default
end
- answer = @@cli.ask(msg) do |q|
+ answer = @cli.ask(msg) do |q|
q.completion = possible_answers
q.readline = true
q.default = possible_answers.first
end
@@ -219,21 +229,21 @@
# give user a chance to set account name or retry description
return ask_account_question(msg, row)
end
def add_description(row)
- desc_answer = @@cli.ask("Enter a new description for this transaction (empty line aborts)\n") do |q|
+ desc_answer = @cli.ask("Enter a new description for this transaction (empty line aborts)\n") do |q|
q.overwrite = true
q.readline = true
q.default = row[:description]
end
row[:description] = desc_answer unless desc_answer.empty?
end
def add_note(row)
- desc_answer = @@cli.ask("Enter a new note for this transaction (empty line aborts)\n") do |q|
+ desc_answer = @cli.ask("Enter a new note for this transaction (empty line aborts)\n") do |q|
q.overwrite = true
q.readline = true
q.default = row[:note]
end
@@ -244,22 +254,15 @@
matches = regexps.map { |regexp, account|
if match = regexp.match(row[:description])
[account, match[0]]
end
}.compact
- matches.sort_by! { |_account, matched_text| matched_text.length }.map(&:first)
+ matches.sort_by { |_account, matched_text| matched_text.length }.map(&:first)
end
def suggest(row)
most_specific_regexp_match(row) +
@matcher.find_similar(row[:description]).map { |n| n[:account] }
- end
-
- def ledger_format(row, line1, line2)
- out = "#{row[:pretty_date]}\t#{row[:description]}#{row[:note] ? "\t; " + row[:note]: ""}\n"
- out += "\t#{line1.first}\t\t\t#{line1.last}\n"
- out += "\t#{line2.first}\t\t\t#{line2.last}\n\n"
- out
end
def output(ledger_line)
options[:output_file].puts ledger_line
options[:output_file].flush