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