class Processor::Example
An example of how to implement a custom processor Processes CSV files with this format: <<~ROWS “Date”,“Payee”,“Memo”,“Outflow”,“Inflow” “23/12/2019”,“coaxial”,“”,“1000000.00”,“” “30/12/2019”,“Santa”,“”,“50000.00”,“” “02/02/2020”,“Someone Else”,“”,“45.00”,“” ROWS The file name for the processor should be the institution name in camel case. It's ok to skip “Bank” or “Credit Union” when naming the file if it's redundant. For instance, this parser is for “Example Bank” but it's named “example.rb”, its corresponding spec is “spec/example_processor_spec.rb” and its fixture would be “spec/fixtures/example.csv”
Public Class Methods
@option options [String] :file Path to the CSV file to process
Processor::Base::new
# File lib/ynab_convert/processor/example.rb, line 20 def initialize(options) # Custom converters can be added so that the CSV data is parsed when # loading the original file register_custom_converters # These are the options for the CSV module (see # https://ruby-doc.org/stdlib-2.6/libdoc/csv/rdoc/CSV.html#method-c-new) # They should match the format for the CSV file that the financial # institution generates. @loader_options = { col_sep: ';', # Use your converters, if any converters: %i[transaction_date my_converter], headers: true } # This is the financial institution's full name as it calls itself. This # usually matches the institution's letterhead and/or commercial name. # It can happen that the same institution needs different parsers because # its credit card CSV files are in one format, and its chequing accounts # in another. In that case, more details can be added in parens. # For instance: # 'Example Bank (credit cards)' and 'Example Bank (chequing)' @institution_name = 'Example Bank' # This is mandatory. super(options) end
Protected Instance Methods
Converts the institution's CSV rows into YNAB4 rows. The YNAB4 columns are: “Date', ”Payee“, ”Memo“, ”Outflow“, ”Inflow“ which match Example
Bank's ”transaction_date“ (after parsing), ”beneficiary“, nothing, ”debit“, and ”credit“ respectively. Note that Example
Bank doesn't include any relevant column for YNAB4's ”Memo“ column so it's skipped and gets '' as its value.
# File lib/ynab_convert/processor/example.rb, line 89 def transformers(row) # Convert the original transaction_date to DD/MM/YYYY as YNAB4 expects # it. unless row[headers[:transaction_date]].nil? transaction_date = row[headers[:transaction_date]].strftime('%d/%m/%Y') end payee = row[headers[:payee]] debit = row[headers[:debit]] credit = row[headers[:credit]] # CSV files can have funny data in them, including invalid or empty rows. # These rows can be skipped from the converted YNAB4 file by calling # skip_row when detected. In this particular case, if there is no # transaction date, it means the row is empty or invalid and we discard # it. skip_row(row) if transaction_date.nil? converted_row = [transaction_date, payee, nil, debit, credit] logger.debug "Converted row: #{converted_row}" converted_row end
Private Instance Methods
Institutions love translating the column names, apparently. Rather than hardcoding the column name as a string, use the headers array at the right index. These lookups aren't particularly expensive but they're done on each row so why not memoize them with ||=
# File lib/ynab_convert/processor/example.rb, line 117 def extract_header_names(row) headers[:transaction_date] ||= row.headers[0] headers[:payee] ||= row.headers[2] headers[:debit] ||= row.headers[3] headers[:credit] ||= row.headers[4] end
# File lib/ynab_convert/processor/example.rb, line 51 def register_custom_converters CSV::Converters[:transaction_date] = lambda { |s| # Only match strings that have two digits, a dot, two digits, a dot, # two digits, i.e. the dates in this institution's CSV files. date_regex = /^\d{2}\.\d{2}\.\d{2}$/ if !s.nil? && s.match(date_regex) parsed_date = Date.strptime(s, '%d.%m.%y') logger.debug "Converted `#{s.inspect}' into date "\ "`#{parsed_date}'" return parsed_date end s } CSV::Converters[:my_converter] = lambda { |s| # A contrived example, just to illustrate multiple converters if s.respond_to?(:downcase) converted_s = s.downcase logger.debug "Converted `#{s.inspect}' into downcased string "\ "`#{converted_s}'" return converted_s end s } end