class Reckon::LedgerParser

Attributes

entries[RW]

Public Class Methods

new(ledger, options = {}) click to toggle source
# File lib/reckon/ledger_parser.rb, line 115
def initialize(ledger, options = {})
  @options = options
  @date_format = options[:ledger_date_format] || options[:date_format] || '%Y-%m-%d'
  parse(ledger)
end

Public Instance Methods

parse(ledger) click to toggle source
# File lib/reckon/ledger_parser.rb, line 121
def parse(ledger)
  @entries = []
  new_entry = {}
  in_comment = false
  comment_chars = ';#%*|'
  ledger.strip.split("\n").each do |entry|
    # strip comment lines
    in_comment = true if entry == 'comment'
    in_comment = false if entry == 'end comment'
    next if in_comment
    next if entry =~ /^\s*[#{comment_chars}]/

    # (date, type, code, description), type and code are optional
    if (m = entry.match(%r{^(\d+[\d/-]+)\s+([*!])?\s*(\([^)]+\))?\s*(.*)$}))
      add_entry(new_entry)
      new_entry = {
        date: try_parse_date(m[1]),
        type: m[2] || "",
        code: m[3] && m[3].tr('()', '') || "",
        desc: m[4].strip,
        accounts: []
      }
    elsif entry =~ /^\s*$/ && new_entry[:date]
      add_entry(new_entry)
      new_entry = {}
    elsif new_entry[:date] && entry =~ /^\s+/
      LOGGER.info("Adding new account #{entry}")
      new_entry[:accounts] << parse_account_line(entry)
    else
      LOGGER.info("Unknown entry type: #{entry}")
      add_entry(new_entry)
      new_entry = {}
    end
  end
  add_entry(new_entry)
end
to_csv() click to toggle source

roughly matches ledger csv format

# File lib/reckon/ledger_parser.rb, line 159
def to_csv
  return @entries.flat_map do |n|
    n[:accounts].map do |a|
      row = [
        n[:date].strftime(@date_format),
        n[:code],
        n[:desc],
        a[:name],
        "", # currency (not implemented)
        a[:amount],
        n[:type],
        "", # account comment (not implemented)
      ]
      CSV.generate_line(row).strip
    end
  end
end

Private Instance Methods

add_entry(entry) click to toggle source
# File lib/reckon/ledger_parser.rb, line 179
def add_entry(entry)
  return unless entry[:date] && entry[:accounts].length > 1

  entry[:accounts] = balance(entry[:accounts])
  @entries << entry
end
balance(accounts) click to toggle source
# File lib/reckon/ledger_parser.rb, line 210
def balance(accounts)
  return accounts unless accounts.any? { |i| i[:amount].nil? }

  sum = accounts.reduce(0) { |m, n| m + (n[:amount] || 0) }
  count = 0
  accounts.each do |account|
    next unless account[:amount].nil?

    count += 1
    account[:amount] = -sum
  end
  if count > 1
    puts "Warning: unparsable entry due to more than one missing money value."
    p accounts
    puts
  end

  accounts
end
clean_money(money) click to toggle source
# File lib/reckon/ledger_parser.rb, line 230
def clean_money(money)
  return nil if money.nil? || money.empty?

  money.gsub(/[^0-9.-]/, '').to_f
end
parse_account_line(entry) click to toggle source
# File lib/reckon/ledger_parser.rb, line 195
def parse_account_line(entry)
  (account_name, rest) = entry.strip.split(/\s{2,}|\t+/, 2)

  return {
    name: account_name,
    amount: clean_money("")
  } if rest.nil? || rest.empty?

  (value, _comment) = rest.split(/;/)
  return {
    name: account_name,
    amount: clean_money(value || "")
  }
end
try_parse_date(date_str) click to toggle source
# File lib/reckon/ledger_parser.rb, line 186
def try_parse_date(date_str)
  date = Date.parse(date_str)
  return nil if date.year > 9999 || date.year < 1000

  date
rescue ArgumentError
  nil
end