Last active
February 29, 2016 06:53
-
-
Save shkm/69d0e5275716bac3f9d1 to your computer and use it in GitHub Desktop.
Quick 'n dirty Rabobank CSV export parser, parsing into a format that can easily be read by GnuCash.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# Parses Rabobank's CSV export into a CSV format more readable by GnuCash. | |
# | |
# To import, use the following settings: | |
# | |
# Start import on row: 2 | |
# Separators: Semicolon | |
# Date format: y-m-d | |
# Currency format: Period | |
# | |
# Matchers should be defined in order to auto-sort | |
# to categories. | |
require 'csv' | |
require 'date' | |
module RaboParser | |
OUTPUT_SEPARATOR = ';' | |
COLUMN_ORDER = %i(date deposit withdrawal description category) | |
class Matcher | |
attr_reader :category | |
def initialize(category, comparison: :all, party: [], description: []) | |
@category = category | |
@comparison = comparison | |
@party = [*party] | |
@description = [*description] | |
unless %i(any all).include? @comparison | |
raise "Comparison must be :any or :all" | |
end | |
if @party.none? && @description.none? | |
raise "At least one pattern must be defined" | |
end | |
end | |
def match?(row) | |
self.send("#{@comparison}?", row) | |
end | |
private | |
def any?(row) | |
matches(row) | |
.any? { |m| m == :match } | |
end | |
def all?(row) | |
matches = matches(row) | |
.reject { |m| m == :no_patterns } | |
.all? { |m| m == :match } | |
return matches | |
end | |
def matches(row) | |
[match_attr?(row, :party), match_attr?(row, :description)] | |
end | |
def match_attr?(row, attr) | |
patterns = instance_variable_get "@#{attr}" | |
attr = row.send(attr) | |
return :no_patterns if patterns.none? | |
patterns.each do |p| | |
match = (p.is_a?(Regexp) ? !!p.match(attr) : p == attr) | |
return :match if match | |
end | |
:no_match | |
end | |
end | |
class CSV | |
def initialize(input_path, output_path) | |
@input_path = input_path | |
@output_path = output_path | |
end | |
def parse | |
parsed_rows = [header] | |
::CSV.foreach(@input_path) do |raw_row| | |
row = Row.new(raw_row) | |
parsed_rows << row.to_s | |
end | |
File.open(@output_path, 'w') do |file| | |
file.write(parsed_rows.join("\n")) | |
end | |
end | |
def header | |
COLUMN_ORDER | |
.map { |c| c.to_s.capitalize } | |
.join(OUTPUT_SEPARATOR) | |
end | |
end | |
class Row | |
def initialize(row) | |
@row = row | |
end | |
def date | |
Date.parse(@row[2]).to_s | |
end | |
def category | |
MATCHERS.each { |m| return m.category if m.match?(self) } | |
'' | |
end | |
def deposit | |
deposit? ? amount : '' | |
end | |
def withdrawal | |
withdrawal? ? amount : '' | |
end | |
def deposit? | |
@row[3] == 'C' | |
end | |
def withdrawal? | |
@row[3] == 'D' | |
end | |
def amount | |
@row[4].strip | |
end | |
def description | |
# other party name + all descriptive fields | |
str = ([party] + descriptions) | |
.map(&:strip) | |
.reject(&:empty?) | |
.join(' ') | |
str | |
end | |
def descriptions | |
@row[10..15].map(&:strip).reject(&:empty?) | |
end | |
def party | |
@row[6].strip | |
end | |
def to_s | |
COLUMN_ORDER.map { |c| send(c) }.join(OUTPUT_SEPARATOR) | |
end | |
end | |
MATCHERS = [ | |
# Define your matchers here. Examples below. | |
Matcher.new('Expenses:Bank Service Charge', description: [ | |
/\AKosten \(Extra\) RaboCard/, | |
/\AKosten Rabo DirectPakket/, | |
/\AKosten \(Extra\) WereldPas/ | |
]), | |
Matcher.new('Expenses:Groceries', description: [ | |
/\ANettorama/, | |
/\AJumbo/, | |
/\APlus/, | |
]), | |
Matcher.new('Expenses:Utilities:Internet', | |
party: 'XS4ALL Internet B.V.') | |
] | |
end | |
def main | |
unless ARGV[0] && ARGV[1] | |
puts 'Usage: raboparser input_filename output_filename' | |
return | |
end | |
RaboParser::CSV.new(ARGV[0], ARGV[1]).parse | |
end | |
main |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment