lib/reckon/app.rb in reckon-0.2.3 vs lib/reckon/app.rb in reckon-0.3.0

- old
+ new

@@ -1,9 +1,8 @@ module Reckon class App VERSION = "Reckon 0.1" - attr_accessor :options, :csv_data, :accounts, :tokens, :money_column_indices, :date_column_index, :description_column_indices, :seen def initialize(options = {}) self.options = options self.tokens = {} @@ -153,10 +152,11 @@ out end def money_for(index) value = money_column_indices.inject("") { |m, i| m + columns[i][index] } + value = value.gsub(/\./, '').gsub(/,/, '.') if options[:comma_separates_cents] cleaned_value = value.gsub(/[^\d\.]/, '').to_f cleaned_value *= -1 if value =~ /[\(\-]/ cleaned_value end @@ -182,11 +182,11 @@ def pretty_date_for(index) date_for(index).strftime("%Y/%m/%d") end def description_for(index) - description_column_indices.map { |i| columns[i][index] }.join("; ").squeeze(" ") + description_column_indices.map { |i| columns[i][index] }.join("; ").squeeze(" ").gsub(/(;\s+){2,}/, '').strip end def output_table output = Terminal::Table.new do |t| t.headings = 'Date', 'Amount', 'Description' @@ -200,24 +200,40 @@ def evaluate_columns(cols) results = [] found_likely_money_column = false cols.each_with_index do |column, index| money_score = date_score = possible_neg_money_count = possible_pos_money_count = 0 - column.each do |entry| + last = nil + column.reverse.each_with_index do |entry, row_from_bottom| + row = csv_data[csv_data.length - 1 - row_from_bottom] entry = entry.strip - money_score += 10 if entry[/^[\-\+\(]{0,2}\$/] - money_score += entry.gsub(/[^\d\.\-\+,\(\)]/, '').length - money_score -= 100 if entry.length > 17 + money_score += 20 if entry[/^[\-\+\(]{0,2}\$/] + money_score += 20 if entry[/^\$?\-?\$?\d+[\.,\d]*?[\.,]\d\d$/] + money_score += entry.gsub(/[^\d\.\-\+,\(\)]/, '').length if entry.length < 7 + money_score -= entry.length if entry.length > 8 money_score -= 20 if entry !~ /^[\$\+\.\-,\d\(\)]+$/ possible_neg_money_count += 1 if entry =~ /^\$?[\-\(]\$?\d+/ possible_pos_money_count += 1 if entry =~ /^\+?\$?\+?\d+/ date_score += 10 if entry =~ /\b(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)/i date_score += 5 if entry =~ /^[\-\/\.\d:\[\]]+$/ date_score += entry.gsub(/[^\-\/\.\d:\[\]]/, '').length if entry.gsub(/[^\-\/\.\d:\[\]]/, '').length > 3 date_score -= entry.gsub(/[\-\/\.\d:\[\]]/, '').length date_score += 30 if entry =~ /^\d+[:\/\.]\d+[:\/\.]\d+([ :]\d+[:\/\.]\d+)?$/ date_score += 10 if entry =~ /^\d+\[\d+:GMT\]$/i + + # Try to determine if this is a balance column + entry_as_num = entry.gsub(/[^\-\d\.]/, '').to_f + if last && entry_as_num != 0 && last != 0 + row.each do |row_entry| + row_entry = row_entry.to_s.gsub(/[^\-\d\.]/, '').to_f + if row_entry != 0 && last + row_entry == entry_as_num + money_score -= 10 + break + end + end + end + last = entry_as_num end if possible_neg_money_count > (column.length / 5.0) && possible_pos_money_count > (column.length / 5.0) money_score += 10 * column.length found_likely_money_column = true @@ -233,11 +249,11 @@ output_columns = [] columns.each_with_index do |column, index| if index == a new_column = [] column.each_with_index do |row, row_index| - new_column << row + " " + columns[b][row_index] + new_column << row + " " + (columns[b][row_index] || '') end output_columns << new_column elsif index == b # skip else @@ -245,33 +261,41 @@ end end output_columns end + require 'pp' def detect_columns results, found_likely_money_column = evaluate_columns(columns) + self.money_column_indices = [ results.sort { |a, b| b[:money_score] <=> a[:money_score] }.first[:index] ] - if found_likely_money_column - self.money_column_indices = [ results.sort { |a, b| b[:money_score] <=> a[:money_score] }.first[:index] ] - else + if !found_likely_money_column + found_likely_double_money_columns = false 0.upto(columns.length - 2) do |i| - _, found_likely_money_column = evaluate_columns(merge_columns(i, i+1)) + _, found_likely_double_money_columns = evaluate_columns(merge_columns(i, i+1)) - if found_likely_money_column + if found_likely_double_money_columns self.money_column_indices = [ i, i+1 ] + unless settings[:testing] + puts "It looks like this CSV has two seperate columns for money, one of which shows positive" + puts "changes and one of which shows negative changes. If this is true, great. Otherwise," + puts "please report this issue to us so we can take a look!\n" + end break end end + + if !found_likely_double_money_columns && !settings[:testing] + puts "I didn't find a high-likelyhood money column, but I'm taking my best guess with column #{money_column_indices.first + 1}." + end end - if money_column_indices - results.reject! {|i| money_column_indices.include?(i[:index]) } - self.date_column_index = results.sort { |a, b| b[:date_score] <=> a[:date_score] }.first[:index] - results.reject! {|i| i[:index] == date_column_index } + results.reject! {|i| money_column_indices.include?(i[:index]) } + self.date_column_index = results.sort { |a, b| b[:date_score] <=> a[:date_score] }.first[:index] + results.reject! {|i| i[:index] == date_column_index } - self.description_column_indices = results.map { |i| i[:index] } - end + self.description_column_indices = results.map { |i| i[:index] } end def each_row_backwards rows = [] (0...columns.first.length).to_a.each do |index| @@ -286,11 +310,11 @@ def columns @columns ||= begin last_row_length = nil csv_data.inject([]) do |memo, row| - fail "Input CSV must have consistent row lengths." if last_row_length && row.length != last_row_length + # fail "Input CSV must have consistent row lengths." if last_row_length && row.length != last_row_length unless row.all? { |i| i.nil? || i.length == 0 } row.each_with_index do |entry, index| memo[index] ||= [] memo[index] << (entry || '').strip end @@ -301,11 +325,11 @@ end end def parse data = options[:string] || File.read(options[:file]) - self.csv_data = FasterCSV.parse(data.strip) + self.csv_data = FasterCSV.parse(data.strip, :col_sep => options[:csv_separator] || ',') end def self.parse_opts(args = ARGV) options = { :output_file => STDOUT } parser = OptionParser.new do |opts| @@ -334,10 +358,18 @@ opts.on("", "--ignore-columns 1,2,5", "Columns to ignore in the CSV file - the first column is column 1") do |ignore| options[:ignore_columns] = ignore.split(",").map { |i| i.to_i } end + opts.on("", "--csv-separator ';'", "Separator for parsing the CSV - default is comma.") do |csv_separator| + options[:csv_separator] = csv_separator + end + + opts.on("", "--comma-separates-cents", "Use comma instead of period to deliminate dollars from cents when parsing ($100,50 instead of $100.50)") do |c| + options[:comma_separates_cents] = c + end + opts.on_tail("-h", "--help", "Show this message") do puts opts exit end @@ -365,7 +397,18 @@ end end options end + + @settings = { :testing => false } + + def self.settings + @settings + end + + def settings + self.class.settings + end end end + \ No newline at end of file