├── .document ├── .github └── workflows │ └── ruby.yml ├── .gitignore ├── .rubocop.yml ├── .ruby-gemset ├── .ruby-version ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── bin ├── build-new-version.sh └── reckon ├── lib ├── reckon.rb └── reckon │ ├── app.rb │ ├── beancount_parser.rb │ ├── cosine_similarity.rb │ ├── csv_parser.rb │ ├── date_column.rb │ ├── ledger_parser.rb │ ├── logger.rb │ ├── money.rb │ ├── options.rb │ └── version.rb ├── reckon.gemspec └── spec ├── cosine_training_and_test.rb ├── data_fixtures ├── 51-sample.csv ├── 51-tokens.yml ├── 73-sample.csv ├── 73-tokens.yml ├── 73-transactions.ledger ├── 85-date-example.csv ├── austrian_example.csv ├── bom_utf8_file.csv ├── broker_canada_example.csv ├── chase.csv ├── danish_kroner_nordea_example.csv ├── english_date_example.csv ├── extratofake.csv ├── french_example.csv ├── german_date_example.csv ├── harder_date_example.csv ├── ing.csv ├── intuit_mint_example.csv ├── invalid_header_example.csv ├── inversed_credit_card.csv ├── multi-line-field.csv ├── nationwide.csv ├── simple.csv ├── some_other.csv ├── spanish_date_example.csv ├── suntrust.csv ├── test_money_column.csv ├── tokens.yaml ├── two_money_columns.csv └── yyyymmdd_date_example.csv ├── integration ├── another_bank_example │ ├── input.csv │ ├── output.ledger │ └── test_args ├── ask_for_account │ ├── cli_input.txt │ ├── expected_output │ ├── input.csv │ └── test_args ├── austrian_example │ ├── input.csv │ ├── output.ledger │ └── test_args ├── bom_utf8_file │ ├── input.csv │ ├── output.ledger │ └── test_args ├── broker_canada_example │ ├── input.csv │ ├── output.ledger │ └── test_args ├── chase │ ├── account_tokens_and_regex │ │ ├── output.ledger │ │ ├── test_args │ │ └── tokens.yml │ ├── default_account_names │ │ ├── output.ledger │ │ └── test_args │ ├── input.csv │ ├── learn_from_existing │ │ ├── learn.ledger │ │ ├── output.ledger │ │ └── test_args │ └── simple │ │ ├── output.ledger │ │ └── test_args ├── danish_kroner_nordea_example │ ├── input.csv │ ├── output.ledger │ └── test_args ├── english_date_example │ ├── input.csv │ ├── output.ledger │ └── test_args ├── extratofake │ ├── input.csv │ ├── output.ledger │ └── test_args ├── french_example │ ├── input.csv │ ├── output.ledger │ └── test_args ├── german_date_example │ ├── input.csv │ ├── output.ledger │ └── test_args ├── harder_date_example │ ├── input.csv │ ├── output.ledger │ └── test_args ├── ing │ ├── input.csv │ ├── output.ledger │ └── test_args ├── intuit_mint_example │ ├── input.csv │ ├── output.ledger │ └── test_args ├── invalid_header_example │ ├── input.csv │ ├── output.ledger │ └── test_args ├── inversed_credit_card │ ├── input.csv │ ├── output.ledger │ └── test_args ├── ledger_date_format │ ├── compare_cmds │ ├── input.csv │ ├── output.ledger │ └── test_args ├── nationwide │ ├── input.csv │ ├── output.ledger │ └── test_args ├── regression │ ├── issue_51_account_tokens │ │ ├── input.csv │ │ ├── output.ledger │ │ ├── test_args │ │ └── tokens.yml │ ├── issue_64_date_column │ │ ├── input.csv │ │ ├── output.ledger │ │ └── test_args │ ├── issue_73_account_token_matching │ │ ├── input.csv │ │ ├── output.ledger │ │ ├── test_args │ │ └── tokens.yml │ └── issue_85_date_example │ │ ├── input.csv │ │ ├── output.ledger │ │ └── test_args ├── spanish_date_example │ ├── input.csv │ ├── output.ledger │ └── test_args ├── suntrust │ ├── input.csv │ ├── output.ledger │ └── test_args ├── tab_delimited_file │ ├── input.csv │ ├── output.ledger │ └── test_args ├── test.sh ├── test_money_column │ ├── input.csv │ ├── output.ledger │ └── test_args ├── two_money_columns │ ├── input.csv │ ├── output.ledger │ └── test_args ├── two_money_columns_manual │ ├── input.csv │ ├── output.ledger │ └── test_args ├── unattended_config │ ├── input.csv │ ├── output.ledger │ ├── test_args │ └── tokens.yml └── yyyymmdd_date_example │ ├── input.csv │ ├── output.ledger │ └── test_args ├── reckon ├── app_spec.rb ├── csv_parser_spec.rb ├── date_column_spec.rb ├── ledger_parser_spec.rb ├── money_column_spec.rb ├── money_spec.rb └── options_spec.rb ├── spec.opts └── spec_helper.rb /.document: -------------------------------------------------------------------------------- 1 | README.rdoc 2 | lib/**/*.rb 3 | bin/* 4 | features/**/*.feature 5 | LICENSE 6 | -------------------------------------------------------------------------------- /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake 6 | # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby 7 | 8 | name: Build Status 9 | 10 | on: 11 | push: 12 | pull_request: 13 | 14 | jobs: 15 | test: 16 | runs-on: ubuntu-latest 17 | strategy: 18 | matrix: 19 | ruby-version: 20 | # Current ruby stable version 21 | - 3.1.2 22 | # Ubuntu 22.04 23 | - 3.0 24 | # Ubuntu 20.04 25 | - 2.7 26 | # For date_column errors 27 | - 2.6 28 | steps: 29 | - uses: actions/checkout@v3 30 | - name: Update package 31 | run: sudo apt-get update 32 | - name: Install packages 33 | run: sudo apt-get -y install ledger hledger 34 | - name: Set up Ruby ${{ matrix.ruby-version }} 35 | # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby, 36 | # use ruby/setup-ruby@v1 (see https://github.com/ruby/setup-ruby#versioning): 37 | uses: ruby/setup-ruby@v1 38 | with: 39 | ruby-version: ${{ matrix.ruby-version }} 40 | bundler-cache: true # runs 'bundle install' and caches installed gems 41 | - name: Run tests 42 | run: bundle exec rake test_all 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## MAC OS 2 | .DS_Store 3 | 4 | ## TEXTMATE 5 | *.tmproj 6 | tmtags 7 | 8 | ## SUBLIME 9 | *.sublime-project 10 | 11 | ## EMACS 12 | *~ 13 | \#* 14 | .\#* 15 | 16 | ## VIM 17 | *.swp 18 | 19 | ## PROJECT::GENERAL 20 | coverage 21 | rdoc 22 | pkg 23 | 24 | ## PROJECT::SPECIFIC 25 | .idea 26 | reckon_local 27 | private_tests 28 | 29 | ## Bundler 30 | vendor 31 | .bundle 32 | # binstubs 33 | exec/ 34 | 35 | # Don't commit gem files 36 | *.gem 37 | 38 | test.log 39 | test_log.txt 40 | 41 | .byebug_history 42 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | Layout/LineLength: 2 | Max: 88 3 | 4 | Style/StringLiterals: 5 | Enabled: false 6 | 7 | Style/RedundantReturn: 8 | Enabled: false 9 | 10 | Metrics/ClassLength: 11 | Enabled: False 12 | 13 | Metrics/MethodLength: 14 | Enabled: False 15 | 16 | Metrics/AbcSize: 17 | Enabled: False 18 | 19 | Style/NumericPredicate: 20 | Enabled: False 21 | 22 | Metrics/PerceivedComplexity: 23 | Enabled: False 24 | 25 | Metrics/CyclomaticComplexity: 26 | Enabled: False 27 | 28 | Style/FormatString: 29 | Enabled: False 30 | 31 | Naming/MethodParameterName: 32 | Enabled: False 33 | -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | reckon 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.0.0 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | gemspec 3 | 4 | gem 'rake' 5 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | reckon (0.11.1) 5 | abbrev (> 0.1) 6 | chronic (>= 0.3.0) 7 | csv (> 0.1) 8 | highline (~> 2.0) 9 | matrix (>= 0.4.2) 10 | rchardet (= 1.8.0) 11 | 12 | GEM 13 | remote: http://rubygems.org/ 14 | specs: 15 | abbrev (0.1.2) 16 | chronic (0.10.2) 17 | coderay (1.1.3) 18 | csv (3.3.2) 19 | diff-lcs (1.6.0) 20 | highline (2.1.0) 21 | matrix (0.4.2) 22 | method_source (1.1.0) 23 | pry (0.15.2) 24 | coderay (~> 1.1) 25 | method_source (~> 1.0) 26 | rake (13.2.1) 27 | rantly (1.2.0) 28 | rchardet (1.8.0) 29 | rspec (3.13.0) 30 | rspec-core (~> 3.13.0) 31 | rspec-expectations (~> 3.13.0) 32 | rspec-mocks (~> 3.13.0) 33 | rspec-core (3.13.3) 34 | rspec-support (~> 3.13.0) 35 | rspec-expectations (3.13.3) 36 | diff-lcs (>= 1.2.0, < 2.0) 37 | rspec-support (~> 3.13.0) 38 | rspec-mocks (3.13.2) 39 | diff-lcs (>= 1.2.0, < 2.0) 40 | rspec-support (~> 3.13.0) 41 | rspec-support (3.13.2) 42 | 43 | PLATFORMS 44 | ruby 45 | 46 | DEPENDENCIES 47 | pry (>= 0.12.2) 48 | rake 49 | rantly (= 1.2.0) 50 | reckon! 51 | rspec (>= 1.2.9) 52 | 53 | BUNDLED WITH 54 | 2.3.5 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Andrew Cantino 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reckon 2 | 3 | ![Build Status](https://github.com/cantino/reckon/workflows/Build%20Status/badge.svg) 4 | 5 | Reckon automagically converts CSV files for use with the command-line accounting tool [Ledger](http://www.ledger-cli.org/). It also helps you to select the correct accounts associated with the CSV data using Bayesian machine learning. 6 | 7 | ## Installation 8 | 9 | Assuming you have Ruby and [Rubygems](http://rubygems.org/pages/download) installed on your system, simply run 10 | 11 | gem install --user reckon 12 | 13 | ## Example Usage 14 | 15 | First, login to your bank and export your transaction data as a CSV file. 16 | 17 | To see how the CSV parses: 18 | 19 | reckon -f bank.csv -p 20 | 21 | If your CSV file has a header on the first line, include `--contains-header`. 22 | 23 | To convert to ledger format and label everything, do: 24 | 25 | reckon -f bank.csv -o output.dat 26 | 27 | To have reckon learn from an existing ledger file, provide it with -l: 28 | 29 | reckon -f bank.csv -l 2010.dat -o output.dat 30 | 31 | Learn more: 32 | 33 | > reckon -h 34 | 35 | Usage: Reckon.rb [options] 36 | 37 | -f, --file FILE The CSV file to parse 38 | -a, --account NAME The Ledger Account this file is for 39 | -v, --[no-]verbose Run verbosely 40 | -i, --inverse Use the negative of each amount 41 | -p, --print-table Print out the parsed CSV in table form 42 | -o, --output-file FILE The ledger file to append to 43 | -l, --learn-from FILE An existing ledger file to learn accounts from 44 | --ignore-columns 1,2,5 45 | Columns to ignore, starts from 1 46 | --money-column 2 47 | Column number of the money column, starts from 1 48 | --money-columns 2,3 49 | Column number of the money columns, starts from 1 (1 or 2 columns) 50 | --raw-money 51 | Don't format money column (for stocks) 52 | --sort DATE|DESC|AMT 53 | Sort file by date, description, or amount 54 | --date-column 3 55 | Column number of the date column, starts from 1 56 | --contains-header [N] 57 | Skip N header rows - default 1 58 | --csv-separator ',' 59 | CSV separator (default ',') 60 | --comma-separates-cents 61 | Use comma to separate cents ($100,50 vs. $100.50) 62 | --encoding 'UTF-8' 63 | Specify an encoding for the CSV file 64 | -c, --currency '$' Currency symbol to use - default $ (ex £, EUR) 65 | --date-format FORMAT 66 | CSV file date format (see `date` for format) 67 | --ledger-date-format FORMAT 68 | Ledger date format (see `date` for format) 69 | -u, --unattended Don't ask questions and guess all the accounts automatically. Use with --learn-from or --account-tokens options. 70 | -t, --account-tokens FILE YAML file with manually-assigned tokens for each account (see README) 71 | --table-output-file FILE 72 | --default-into-account NAME 73 | Default into account 74 | --default-outof-account NAME 75 | Default 'out of' account 76 | --fail-on-unknown-account 77 | Fail on unmatched transactions. 78 | --suffixed 79 | Append currency symbol as a suffix. 80 | -h, --help Show this message 81 | --version Show version 82 | 83 | If you find CSV files that it can't parse, send me examples or pull requests! 84 | 85 | ## Unattended mode 86 | 87 | You can run reckon in a non-interactive mode. 88 | To guess the accounts reckon can use an existing ledger file or a token file with keywords. 89 | 90 | `reckon --unattended -a Checking -l 2010.dat -f bank.csv -o ledger.dat` 91 | 92 | `reckon --unattended -a Checking --account-tokens tokens.yaml -f bank.csv -o ledger.dat` 93 | 94 | In unattended mode, you can use STDIN to read your csv data, by specifying `-` as the argument to `-f`. 95 | 96 | `csv_file_generator | reckon --unattended -a Checking -l 2010.dat -o ledger.dat -f -` 97 | 98 | ### Account Tokens 99 | 100 | The account tokens file provides a way to teach reckon about what tokens are associated with an account. As an example, this `tokens.yaml` file: 101 | 102 | Expenses: 103 | Bank: 104 | - 'ING Direct Deposit' 105 | 106 | Would tokenize to 'ING', 'Direct' and 'Deposit'. The matcher would then suggest matches to transactions that included those tokens. (ex 'Chase Direct Deposit') 107 | 108 | Here's an example of `tokens.yaml`: 109 | 110 | ``` 111 | config: 112 | similarity_threshold: 2 # range 0-10 113 | 114 | Income: 115 | Salary: 116 | - 'LÖN' 117 | - 'Salary' 118 | Expenses: 119 | Bank: 120 | - 'Comission' 121 | - 'MasterCard' 122 | Rent: 123 | - '0011223344' # Landlord bank number 124 | Hosting: 125 | - /hosting/i # This regexp will catch descriptions such as WebHosting or filehosting 126 | '[Internal:Transfer]': # Virtual account 127 | - '4433221100' # Your own account number 128 | ``` 129 | 130 | Reckon will use `Income:Unknown` or `Expenses:Unknown` if it can't match a transaction to an account. 131 | 132 | The config key is a special key used to set configuration when running in unattended mode. The only config variable is similarity_threshold (currently). 133 | 134 | You can override these names with the `--default_outof_account` and `--default_into_account` options. 135 | 136 | ### Substring Match 137 | 138 | If, in the above example, you'd prefer to match any transaction that contains the string 'ING Direct Deposit' you have to use a regex: 139 | 140 | Expenses: 141 | Bank: 142 | - /ING Direct Deposit/ 143 | 144 | ## Contributing 145 | 146 | We encourage you to contribute to Reckon! Here is some information to help you. 147 | 148 | ### Patches/Pull Requests Process 149 | 150 | 1. Fork the project. 151 | 2. Make your feature addition or bug fix. 152 | 3. Add tests for it. This is important so I don't break it in a future version unintentionally. 153 | 4. Commit, do not mess with rakefile, version, or history. 154 | - (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull) 155 | 5. Send me a pull request. Bonus points for topic branches. 156 | 157 | ### Integration Tests 158 | 159 | Reckon has integration test located in `spec/integration`. These are integration and regression tests for reckon. 160 | 161 | Run all the tests: 162 | 163 | ./spec/integration/test.sh 164 | 165 | Run a single test 166 | 167 | ./spec/integration/test.sh chase/account_tokens_and_regex 168 | 169 | #### Add a new integration test 170 | 171 | Each test has it's own directory, which you can add any files you want, but the following files are required: 172 | 173 | - `test_args` - arguments to add to the reckon command to test against, can specify `--unattended`, `-f input.csv`, etc 174 | - `output.ledger` - the expected ledger file output 175 | 176 | If the result of running reckon with `test_args` does not match `output.ledger`, then the test fails. 177 | 178 | Most tests will specify `--unattended`, otherwise reckon prompts for keyboard input. 179 | 180 | The convention is to use `input.csv` as the input file, and `tokens.yml` as the tokens file, but it is not required. 181 | 182 | 183 | ## Copyright 184 | 185 | Copyright (c) 2013 Andrew Cantino (@cantino). See LICENSE for details. 186 | 187 | Thanks to @BlackEdder for many contributions! 188 | 189 | Currently maintained by @benprew. Thank you! 190 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require 'rspec/core/rake_task' 5 | require 'English' 6 | 7 | RSpec::Core::RakeTask.new(:spec) 8 | 9 | task default: :spec 10 | 11 | desc "Run specs and integration tests" 12 | task :test_all do 13 | puts "#{`ledger --version |head -n1`}" 14 | puts "Running unit tests" 15 | Rake::Task["spec"].invoke 16 | puts "Running integration tests" 17 | Rake::Task["test_integration"].invoke 18 | end 19 | 20 | desc "Run integration tests" 21 | task :test_integration do 22 | cmd = 'prove -v ./spec/integration/test.sh' 23 | raise 'Integration tests failed' unless system(cmd) 24 | end 25 | -------------------------------------------------------------------------------- /bin/build-new-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | VERSION=$1 6 | 7 | if [ -z "$TOKEN" ]; then 8 | echo "\$TOKEN var must be your github api token" 9 | exit 1 10 | fi 11 | 12 | echo "Install github_changelog_generator" 13 | gem install --user github_changelog_generator 14 | 15 | echo "Update 'lib/reckon/version.rb'" 16 | echo -e "module Reckon\n VERSION = \"$VERSION\"\nend" > lib/reckon/version.rb 17 | echo "Run `bundle install` to build updated Gemfile.lock" 18 | bundle install 19 | echo "Run changelog generator (requires $TOKEN to be your github token)" 20 | github_changelog_generator -u cantino -p reckon -t "$TOKEN" --future-release "v$VERSION" 21 | echo "Commit changes" 22 | git add CHANGELOG.md lib/reckon/version.rb Gemfile.lock 23 | git commit -m "Release $VERSION" 24 | echo "Tag release" 25 | git tag "v$VERSION" 26 | echo "Build new gem" 27 | gem build reckon.gemspec 28 | echo "Push changes and tags" 29 | echo "git push && git push --tags" 30 | echo "Push new gem" 31 | echo "gem push reckon-$VERSION.gem" 32 | gh release create "v$VERSION" "reckon-$VERSION.gem" --draft --generate-notes 33 | -------------------------------------------------------------------------------- /bin/reckon: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rubygems' 4 | require 'reckon' 5 | 6 | begin 7 | options = Reckon::Options.parse_command_line_options 8 | rescue RuntimeError => e 9 | puts("ERROR: #{e}") 10 | exit(1) 11 | end 12 | reckon = Reckon::App.new(options) 13 | 14 | if options[:print_table] 15 | reckon.output_table 16 | if options[:table_output_file] 17 | File.open(options[:table_output_file], 'w') { |fh| reckon.output_table fh } 18 | end 19 | exit 20 | end 21 | 22 | if !reckon.csv_parser.money_column_indices 23 | puts "I was unable to determine either a single or a pair of combined columns to use as the money column." 24 | exit 25 | end 26 | 27 | reckon.walk_backwards 28 | -------------------------------------------------------------------------------- /lib/reckon.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rubygems' 4 | require 'rchardet' 5 | require 'chronic' 6 | require 'csv' 7 | require 'highline' 8 | require 'optparse' 9 | require 'time' 10 | require 'logger' 11 | 12 | require_relative 'reckon/version' 13 | require_relative 'reckon/logger' 14 | require_relative 'reckon/cosine_similarity' 15 | require_relative 'reckon/date_column' 16 | require_relative 'reckon/money' 17 | require_relative 'reckon/ledger_parser' 18 | require_relative 'reckon/beancount_parser' 19 | require_relative 'reckon/csv_parser' 20 | require_relative 'reckon/options' 21 | require_relative 'reckon/app' 22 | -------------------------------------------------------------------------------- /lib/reckon/app.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'yaml' 4 | require 'stringio' 5 | 6 | UnattendedConfig = Struct.new(:similarity_threshold) 7 | 8 | module Reckon 9 | # The main app 10 | class App 11 | attr_accessor :options, :seen, :csv_parser, :regexps, :matcher 12 | 13 | def initialize(opts = {}) 14 | self.options = opts 15 | LOGGER.level = options[:verbose] || Logger::WARN 16 | 17 | self.regexps = {} 18 | self.seen = Set.new 19 | options[:sort] ||= :date 20 | @cli = HighLine.new 21 | @csv_parser = CSVParser.new(options) 22 | @matcher = CosineSimilarity.new(options) 23 | @parser = options[:format] =~ /beancount/i ? BeancountParser.new : LedgerParser.new 24 | learn! 25 | end 26 | 27 | def interactive_output(str, fh = $stdout) 28 | return if options[:unattended] 29 | 30 | fh.puts str 31 | end 32 | 33 | # Learn from previous transactions. Used to recommend accounts for a transaction. 34 | def learn! 35 | learn_from_account_tokens(options[:account_tokens_file]) 36 | learn_from_ledger_file(options[:existing_ledger_file]) 37 | # TODO: make this work 38 | # this doesn't work because output_file is an IO object 39 | # learn_from_ledger_file(options[:output_file]) if File.exist?(options[:output_file]) 40 | end 41 | 42 | def learn_from_account_tokens(filename) 43 | return unless filename 44 | 45 | raise "#{filename} doesn't exist!" unless File.exist?(filename) 46 | 47 | tokens = YAML.load_file(filename) 48 | cfg = build_unattended_config(tokens.delete('config')) 49 | @options[:similarity_threshold] = cfg.similarity_threshold if cfg 50 | 51 | extract_account_tokens(tokens).each do |account, tokens| 52 | tokens.each do |t| 53 | if t.start_with?('/') 54 | add_regexp(account, t) 55 | else 56 | @matcher.add_document(account, t) 57 | end 58 | end 59 | end 60 | end 61 | 62 | def build_unattended_config(cfg) 63 | return unless cfg 64 | invalid = cfg.keys - UnattendedConfig.members.map(&:to_s) 65 | raise "Invalid keys in config: #{invalid}" if invalid.any? 66 | return UnattendedConfig.new(*cfg.values_at(*UnattendedConfig.members.map(&:to_s))) 67 | end 68 | 69 | def learn_from_ledger_file(ledger_file) 70 | return unless ledger_file 71 | 72 | raise "#{ledger_file} doesn't exist!" unless File.exist?(ledger_file) 73 | 74 | learn_from_ledger(File.new(ledger_file)) 75 | end 76 | 77 | # Takes an IO-like object 78 | def learn_from_ledger(ledger) 79 | LOGGER.info "learning from #{ledger}" 80 | @parser.parse(ledger).each do |entry| 81 | entry[:accounts].each do |account| 82 | str = [entry[:desc], account[:amount]].join(" ") 83 | if account[:name] != options[:bank_account] 84 | LOGGER.info "adding document #{account[:name]} #{str}" 85 | @matcher.add_document(account[:name], str) 86 | end 87 | pretty_date = entry[:date].iso8601 88 | if account[:name] == options[:bank_account] 89 | seen << seen_key(pretty_date, @csv_parser.pretty_money(account[:amount])) 90 | end 91 | end 92 | end 93 | end 94 | 95 | # Add tokens from account_tokens_file to accounts 96 | def extract_account_tokens(subtree, account = nil) 97 | if subtree.nil? || !subtree 98 | puts "Warning: empty #{account} tree" 99 | {} 100 | elsif subtree.is_a?(Array) 101 | { account => subtree } 102 | else 103 | at = subtree.map do |k, v| 104 | merged_acct = [account, k].compact.join(':') 105 | extract_account_tokens(v, merged_acct) 106 | end 107 | at.inject({}) { |memo, e| memo.merge!(e) } 108 | end 109 | end 110 | 111 | def add_regexp(account, regex_str) 112 | # https://github.com/tenderlove/psych/blob/master/lib/psych/visitors/to_ruby.rb 113 | match = regex_str.match(/^\/(.*)\/([ix]*)$/m) 114 | fail "failed to parse regexp #{regex_str}" unless match 115 | 116 | options = 0 117 | (match[2] || '').split('').each do |option| 118 | case option 119 | when 'x' then options |= Regexp::EXTENDED 120 | when 'i' then options |= Regexp::IGNORECASE 121 | end 122 | end 123 | regexps[Regexp.new(match[1], options)] = account 124 | end 125 | 126 | def walk_backwards 127 | cmd_options = "[account]/[q]uit/[s]kip/[n]ote/[d]escription" 128 | seen_anything_new = false 129 | each_row_backwards do |row| 130 | print_transaction([row]) 131 | 132 | if already_seen?(row) 133 | interactive_output "NOTE: This row is very similar to a previous one!" 134 | if !seen_anything_new 135 | interactive_output "Skipping..." 136 | next 137 | end 138 | else 139 | seen_anything_new = true 140 | end 141 | 142 | if row[:money] > 0 143 | # out_of_account 144 | answer = ask_account_question( 145 | "Which account provided this income? (#{cmd_options})", row 146 | ) 147 | line1 = [options[:bank_account], row[:pretty_money]] 148 | line2 = [answer, ""] 149 | else 150 | # into_account 151 | answer = ask_account_question( 152 | "To which account did this money go? (#{cmd_options})", row 153 | ) 154 | line1 = [answer, ""] 155 | line2 = [options[:bank_account], row[:pretty_money]] 156 | end 157 | 158 | if answer == '~~SKIP~~' 159 | LOGGER.info "skipping transaction: #{row}" 160 | next 161 | end 162 | 163 | finish if %w[quit q].include?(answer) 164 | if %w[skip s].include?(answer) 165 | interactive_output "Skipping" 166 | next 167 | end 168 | 169 | ledger = @parser.format_row(row, line1, line2) 170 | LOGGER.info "ledger line: #{ledger}" 171 | learn_from_ledger(StringIO.new(ledger)) unless options[:account_tokens_file] 172 | output(ledger) 173 | end 174 | end 175 | 176 | def each_row_backwards 177 | rows = [] 178 | (0...@csv_parser.columns.first.length).to_a.each do |index| 179 | if @csv_parser.date_for(index).nil? 180 | LOGGER.warn("Skipping row: '#{@csv_parser.row(index)}' that doesn't have a valid date") 181 | next 182 | end 183 | rows << { :date => @csv_parser.date_for(index), 184 | :pretty_date => @csv_parser.pretty_date_for(index), 185 | :pretty_money => @csv_parser.pretty_money_for(index), 186 | :pretty_money_negated => @csv_parser.pretty_money_for(index, :negate), 187 | :money => @csv_parser.money_for(index), 188 | :description => @csv_parser.description_for(index) } 189 | end 190 | rows.sort_by do |n| 191 | [n[options[:sort]], -n[:money], n[:description]] 192 | end.each do |row| 193 | yield row 194 | end 195 | end 196 | 197 | def print_transaction(rows, fh = $stdout) 198 | str = "\n" 199 | header = %w[Date Amount Description] 200 | header += ["Note"] if rows.map { |r| r[:note] }.any? 201 | maxes = header.map(&:length) 202 | rows = rows.map do |r| 203 | [r[:pretty_date], r[:pretty_money], r[:description], r[:note]].compact 204 | end 205 | 206 | rows.each do |r| 207 | r.length.times do |i| 208 | l = 0 209 | l = r[i].length if r[i] 210 | maxes[i] ||= 0 211 | maxes[i] = l if maxes[i] < l 212 | end 213 | end 214 | 215 | header.each_with_index do |n, i| 216 | str += " #{n.center(maxes[i])} |" 217 | end 218 | str += "\n" 219 | 220 | rows.each do |row| 221 | row.each_with_index do |_, i| 222 | just = maxes[i] 223 | str += sprintf(" %#{just}s |", row[i]) 224 | end 225 | str += "\n" 226 | end 227 | 228 | interactive_output str, fh 229 | end 230 | 231 | def ask_account_question(msg, row) 232 | # return account token if it matches 233 | token_answer = most_specific_regexp_match(row) 234 | if token_answer.any? 235 | row[:note] = "Matched account token" 236 | puts "NOTE: Matched account token" 237 | puts token_answer[0] 238 | return token_answer[0] 239 | end 240 | 241 | possible_answers = suggest(row) 242 | LOGGER.info "possible_answers===> #{possible_answers.inspect}" 243 | 244 | if options[:unattended] 245 | if options[:fail_on_unknown_account] && possible_answers.empty? 246 | raise %(Couldn't find any matches for '#{row[:description]}' 247 | Try adding an account token with --account-tokens) 248 | end 249 | 250 | default = options[:default_outof_account] 251 | default = options[:default_into_account] if row[:pretty_money][0] == '-' 252 | return possible_answers[0] || default 253 | end 254 | 255 | answer = @cli.ask(msg) do |q| 256 | q.completion = possible_answers 257 | q.readline = true 258 | q.default = possible_answers.first 259 | end 260 | 261 | # if answer isn't n/note/d/description, must be an account name, or skip, or quit 262 | return answer unless %w[n note d description].include?(answer) 263 | 264 | add_description(row) if %w[d description].include?(answer) 265 | add_note(row) if %w[n note].include?(answer) 266 | 267 | print_transaction([row]) 268 | # give user a chance to set account name or retry description 269 | return ask_account_question(msg, row) 270 | end 271 | 272 | def add_description(row) 273 | desc_answer = @cli.ask("Enter a new description for this transaction (empty line aborts)\n") do |q| 274 | q.overwrite = true 275 | q.readline = true 276 | q.default = row[:description] 277 | end 278 | 279 | row[:description] = desc_answer unless desc_answer.empty? 280 | end 281 | 282 | def add_note(row) 283 | desc_answer = @cli.ask("Enter a new note for this transaction (empty line aborts)\n") do |q| 284 | q.overwrite = true 285 | q.readline = true 286 | q.default = row[:note] 287 | end 288 | 289 | row[:note] = desc_answer unless desc_answer.empty? 290 | end 291 | 292 | def most_specific_regexp_match(row) 293 | matches = regexps.map { |regexp, account| 294 | if match = regexp.match(row[:description]) 295 | [account, match[0]] 296 | end 297 | }.compact 298 | matches.sort_by { |_account, matched_text| matched_text.length }.map(&:first) 299 | end 300 | 301 | def suggest(row) 302 | most_specific_regexp_match(row) + 303 | @matcher.find_similar(row[:description]).filter do |n| 304 | !@options[:similarity_threshold] || 305 | n.fetch(:simliarity, 0) * 10 >= @options[:similarity_threshold] 306 | end.map { |n| n[:account] } 307 | end 308 | 309 | def output(ledger_line) 310 | options[:output_file].puts ledger_line 311 | options[:output_file].flush 312 | end 313 | 314 | def seen_key(date, amount) 315 | return [date, amount].join("|") 316 | end 317 | 318 | def already_seen?(row) 319 | seen.include?(seen_key(row[:pretty_date], row[:pretty_money])) 320 | end 321 | 322 | def finish 323 | options[:output_file].close unless options[:output_file] == STDOUT 324 | interactive_output "Exiting." 325 | exit 326 | end 327 | 328 | def output_table(fh = $stdout) 329 | rows = [] 330 | each_row_backwards do |row| 331 | rows << row 332 | end 333 | print_transaction(rows, fh) 334 | end 335 | end 336 | end 337 | -------------------------------------------------------------------------------- /lib/reckon/beancount_parser.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'date' 3 | 4 | module Reckon 5 | class BeancountParser 6 | 7 | attr_accessor :entries 8 | 9 | def initialize(options = {}) 10 | @options = options 11 | @date_format = options[:ledger_date_format] || options[:date_format] || '%Y-%m-%d' 12 | end 13 | 14 | # 2015-01-01 * "Opening Balance for checking account" 15 | # Assets:US:BofA:Checking 3490.52 USD 16 | # Equity:Opening-Balances -3490.52 USD 17 | 18 | # input is an object that response to #each_line, 19 | # (i.e. a StringIO or an IO object) 20 | def parse(input) 21 | entries = [] 22 | comment_chars = ';#%*|' 23 | new_entry = {} 24 | 25 | input.each_line do |entry| 26 | 27 | next if entry =~ /^\s*[#{comment_chars}]/ 28 | 29 | m = entry.match(%r{ 30 | ^ 31 | (\d+[\d/-]+) # date 32 | \s+ 33 | ([*!])? # type 34 | \s* 35 | ("[^"]*")? # description (optional) 36 | \s* 37 | ("[^"]*")? # notes (optional) 38 | # tags (not implemented) 39 | }x) 40 | 41 | # (date, type, code, description), type and code are optional 42 | if (m) 43 | add_entry(entries, new_entry) 44 | new_entry = { 45 | date: try_parse_date(m[1]), 46 | type: m[2] || "", 47 | desc: trim_quote(m[3]), 48 | notes: trim_quote(m[4]), 49 | accounts: [] 50 | } 51 | elsif entry =~ /^\s*$/ && new_entry[:date] 52 | add_entry(entries, new_entry) 53 | new_entry = {} 54 | elsif new_entry[:date] && entry =~ /^\s+/ 55 | LOGGER.info("Adding new account #{entry}") 56 | new_entry[:accounts] << parse_account_line(entry) 57 | else 58 | LOGGER.info("Unknown entry type: #{entry}") 59 | add_entry(entries, new_entry) 60 | new_entry = {} 61 | end 62 | 63 | end 64 | entries 65 | end 66 | 67 | def format_row(row, line1, line2) 68 | out = %Q{#{row[:pretty_date]} * "#{row[:description]}" "#{row[:note]}"\n} 69 | out += "\t#{line1.first}\t\t\t#{line1.last}\n" 70 | out += "\t#{line2.first}\t\t\t#{line2.last}\n\n" 71 | out 72 | end 73 | 74 | private 75 | 76 | # remove leading and trailing quote character (") 77 | def trim_quote(str) 78 | return str if !str 79 | str.gsub(/^"([^"]*)"$/, '\1') 80 | end 81 | 82 | def add_entry(entries, entry) 83 | return unless entry[:date] && entry[:accounts].length > 1 84 | 85 | entry[:accounts] = balance(entry[:accounts]) 86 | entries << entry 87 | end 88 | 89 | def try_parse_date(date_str) 90 | date = Date.parse(date_str) 91 | return nil if date.year > 9999 || date.year < 1000 92 | 93 | date 94 | rescue ArgumentError 95 | nil 96 | end 97 | 98 | def parse_account_line(entry) 99 | # TODO handle buying stocks 100 | # Assets:US:ETrade:VHT 19 VHT {132.32 USD, 2017-08-27} 101 | (account_name, rest) = entry.strip.split(/\s{2,}|\t+/, 2) 102 | 103 | if rest.nil? || rest.empty? 104 | return { 105 | name: account_name, 106 | amount: clean_money("") 107 | } 108 | end 109 | 110 | value = if rest =~ /{/ 111 | (qty, dollar_value, date) = rest.split(/[{,]/) 112 | (qty.to_f * dollar_value.to_f).to_s 113 | else 114 | rest 115 | end 116 | 117 | return { 118 | name: account_name, 119 | amount: clean_money(value || "") 120 | } 121 | end 122 | 123 | def balance(accounts) 124 | return accounts unless accounts.any? { |i| i[:amount].nil? } 125 | 126 | sum = accounts.reduce(0) { |m, n| m + (n[:amount] || 0) } 127 | count = 0 128 | accounts.each do |account| 129 | next unless account[:amount].nil? 130 | 131 | count += 1 132 | account[:amount] = -sum 133 | end 134 | if count > 1 135 | puts "Warning: unparsable entry due to more than one missing money value." 136 | p accounts 137 | puts 138 | end 139 | 140 | accounts 141 | end 142 | 143 | def clean_money(money) 144 | return nil if money.nil? || money.empty? 145 | 146 | money.gsub(/[^0-9.-]/, '').to_f 147 | end 148 | end 149 | end 150 | 151 | -------------------------------------------------------------------------------- /lib/reckon/cosine_similarity.rb: -------------------------------------------------------------------------------- 1 | require 'matrix' 2 | require 'set' 3 | 4 | # Implementation of cosine similarity using TF-IDF for vectorization. 5 | # 6 | # In information retrieval, tf–idf, short for term frequency–inverse document frequency, 7 | # is a numerical statistic that is intended to reflect how important a word is to a 8 | # document in a collection or corpus 9 | # 10 | # Cosine Similarity a measurement to determine how similar 2 documents are to each other. 11 | # 12 | # These weights and measures are used to suggest which account a transaction should be 13 | # assigned to. 14 | module Reckon 15 | # Calculates cosine similarity for tf/idf 16 | class CosineSimilarity 17 | DocumentInfo = Struct.new(:tokens, :accounts) 18 | 19 | def initialize(options) 20 | @docs = DocumentInfo.new({}, {}) 21 | end 22 | 23 | def add_document(account, doc) 24 | tokens = tokenize(doc) 25 | LOGGER.info "doc tokens: #{tokens}" 26 | tokens.each do |n| 27 | (token, count) = n 28 | 29 | @docs.tokens[token] ||= Hash.new(0) 30 | @docs.tokens[token][account] += count 31 | @docs.accounts[account] ||= Hash.new(0) 32 | @docs.accounts[account][token] += count 33 | end 34 | end 35 | 36 | # find most similar documents to query 37 | def find_similar(query) 38 | LOGGER.info "find_similar #{query}" 39 | 40 | accounts = docs_to_check(query).map do |a| 41 | [a, tfidf(@docs.accounts[a])] 42 | end 43 | 44 | q = tfidf(tokenize(query)) 45 | 46 | suggestions = accounts.map do |a, d| 47 | { 48 | similarity: calc_similarity(q, d), 49 | account: a 50 | } 51 | end.select { |n| n[:similarity] > 0 }.sort_by { |n| -n[:similarity] } 52 | 53 | LOGGER.info "most similar accounts: #{suggestions}" 54 | 55 | return suggestions 56 | end 57 | 58 | private 59 | 60 | def docs_to_check(query) 61 | return tokenize(query).reduce(Set.new) do |corpus, t| 62 | corpus.union(Set.new(@docs.tokens[t[0]]&.keys)) 63 | end 64 | end 65 | 66 | def tfidf(tokens) 67 | scores = {} 68 | 69 | tokens.each do |t, n| 70 | scores[t] = calc_tf_idf( 71 | n, 72 | tokens.length, 73 | @docs.tokens[t]&.length&.to_f || 0, 74 | @docs.accounts.length 75 | ) 76 | end 77 | 78 | return scores 79 | end 80 | 81 | # Cosine similarity is used to compare how similar 2 documents are. Returns a float 82 | # between 1 and -1, where 1 is exactly the same and -1 is exactly opposite. 83 | # 84 | # see https://en.wikipedia.org/wiki/Cosine_similarity 85 | # cos(theta) = (A . B) / (||A|| ||B||) 86 | # where A . B is the "dot product" and ||A|| is the magnitude of A 87 | # 88 | # The variables A and B are the set of unique terms in q and d. 89 | # 90 | # For example, when q = "big red balloon" and d ="small green balloon" then the 91 | # variables are (big,red,balloon,small,green) and a = (1,1,1,0,0) and b = 92 | # (0,0,1,1,1). 93 | # 94 | # query and doc are hashes of token => tf/idf score 95 | def calc_similarity(query, doc) 96 | tokens = Set.new(query.keys + doc.keys) 97 | 98 | a = Vector.elements(tokens.map { |n| query[n] || 0 }, false) 99 | b = Vector.elements(tokens.map { |n| doc[n] || 0 }, false) 100 | 101 | return a.inner_product(b) / (a.magnitude * b.magnitude) 102 | end 103 | 104 | def calc_tf_idf(token_count, num_words_in_doc, df, num_docs) 105 | # tf(t,d) = count of t in d / number of words in d 106 | tf = token_count / num_words_in_doc.to_f 107 | 108 | # smooth idf weight 109 | # see https://en.wikipedia.org/wiki/Tf%E2%80%93idf#Inverse_document_frequency_2 110 | # df(t) = num of documents with term t in them 111 | # idf(t) = log(N/(1 + df )) + 1 112 | idf = Math.log(num_docs.to_f / (1 + df)) + 1 113 | 114 | tf * idf 115 | end 116 | 117 | def tokenize(str) 118 | mk_tokens(str).each_with_object(Hash.new(0)) do |n, memo| 119 | memo[n] += 1 120 | end.to_a 121 | end 122 | 123 | def mk_tokens(str) 124 | str.downcase.tr(';', ' ').tr("'", '').split(/[^a-z0-9.]+/).reject(&:empty?) 125 | end 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /lib/reckon/csv_parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'stringio' 4 | 5 | module Reckon 6 | # Parses CSV files 7 | class CSVParser 8 | attr_accessor :options, :csv_data, :money_column_indices, :date_column_index, 9 | :description_column_indices, :money_column, :date_column 10 | 11 | def initialize(options = {}) 12 | self.options = options 13 | 14 | self.options[:csv_separator] = "\t" if options[:csv_separator] == '\t' 15 | self.options[:currency] ||= '$' 16 | 17 | # we convert to a string so we can do character encoding cleanup 18 | @csv_data = parse(options[:string] || File.read(options[:file]), options[:file]) 19 | filter_csv 20 | detect_columns 21 | end 22 | 23 | # transpose csv_data (array of rows) to an array of columns 24 | def columns 25 | @columns ||= @csv_data[0].zip(*@csv_data[1..]) 26 | end 27 | 28 | def date_for(index) 29 | @date_column.for(index) 30 | end 31 | 32 | def pretty_date_for(index) 33 | @date_column.pretty_for(index) 34 | end 35 | 36 | def money_for(index) 37 | @money_column[index] 38 | end 39 | 40 | def pretty_money(amount, negate = false) 41 | Money.new(amount, @options).pretty(negate) 42 | end 43 | 44 | def pretty_money_for(index, negate = false) 45 | money = money_for(index) 46 | return 0 if money.nil? 47 | 48 | money.pretty(negate) 49 | end 50 | 51 | def description_for(index) 52 | description_column_indices.map { |i| columns[i][index].to_s.strip } 53 | .reject(&:empty?) 54 | .join("; ") 55 | .squeeze(" ") 56 | .gsub(/(;\s+){2,}/, '') 57 | .strip 58 | end 59 | 60 | def row(index) 61 | csv_data[index].join(", ") 62 | end 63 | 64 | private 65 | 66 | def filter_csv 67 | return unless options[:ignore_columns] 68 | 69 | new_columns = [] 70 | columns.each_with_index do |column, index| 71 | new_columns << (options[:ignore_columns].include?(index + 1) ? [''] * column.length : column) 72 | end 73 | @columns = new_columns 74 | end 75 | 76 | def evaluate_columns(cols) 77 | results = [] 78 | found_likely_money_column = false 79 | cols.each_with_index do |column, index| 80 | money_score = date_score = possible_neg_money_count = possible_pos_money_count = 0 81 | last = nil 82 | column.reverse.each_with_index do |entry, row_from_bottom| 83 | entry ||= "" # entries can be nil 84 | row = csv_data[csv_data.length - 1 - row_from_bottom] 85 | entry = entry.strip 86 | money_score += Money::likelihood(entry) 87 | possible_neg_money_count += 1 if entry =~ /^\$?[\-\(]\$?\d+/ 88 | possible_pos_money_count += 1 if entry =~ /^\+?\$?\+?\d+/ 89 | date_score += DateColumn.likelihood(entry) 90 | 91 | # Try to determine if this is a balance column 92 | entry_as_num = entry.gsub(/[^\-\d\.]/, '').to_f 93 | if last && entry_as_num != 0 && last != 0 94 | row.each do |row_entry| 95 | row_entry = row_entry.to_s.gsub(/[^\-\d\.]/, '').to_f 96 | if row_entry != 0 && last + row_entry == entry_as_num 97 | money_score -= 10 98 | break 99 | end 100 | end 101 | end 102 | last = entry_as_num 103 | end 104 | 105 | if possible_neg_money_count > (column.length / 5.0) && possible_pos_money_count > (column.length / 5.0) 106 | money_score += 10 * column.length 107 | found_likely_money_column = true 108 | end 109 | 110 | results << { :index => index, :money_score => money_score, 111 | :date_score => date_score } 112 | end 113 | 114 | results.sort_by! { |n| -n[:money_score] } 115 | 116 | # check if it looks like a 2-column file with a balance field 117 | if results.length >= 3 && results[1][:money_score] + results[2][:money_score] >= results[0][:money_score] 118 | results[1][:is_money_column] = true 119 | results[2][:is_money_column] = true 120 | else 121 | results[0][:is_money_column] = true 122 | end 123 | 124 | return results.sort_by { |n| n[:index] } 125 | end 126 | 127 | # Some csv files negative/positive amounts are indicated in separate account 128 | def detect_sign_column 129 | return if columns[0].length <= 2 # This test needs requires more than two rows otherwise will lead to false positives 130 | 131 | signs = [] 132 | if @money_column_indices[0] > 0 133 | column = columns[@money_column_indices[0] - 1] 134 | signs = column.uniq 135 | end 136 | if (signs.length != 2 && 137 | (@money_column_indices[0] + 1 < columns.length)) 138 | column = columns[@money_column_indices[0] + 1] 139 | signs = column.uniq 140 | end 141 | if signs.length == 2 142 | negative_first = true 143 | negative_first = false if signs[0] == "Bij" || signs[0].downcase =~ /^cr/ # look for known debit indicators 144 | @money_column.each_with_index do |money, i| 145 | if negative_first && column[i] == signs[0] 146 | @money_column[i] = -money 147 | elsif !negative_first && column[i] == signs[1] 148 | @money_column[i] = -money 149 | end 150 | end 151 | end 152 | end 153 | 154 | def detect_columns 155 | results = evaluate_columns(columns) 156 | 157 | # We keep money_column options for backwards compatibility reasons, while 158 | # adding option to specify multiple money_columns 159 | if options[:money_column] 160 | self.money_column_indices = [options[:money_column] - 1] 161 | 162 | # One or two columns can be specified as money_columns 163 | elsif options[:money_columns] 164 | if options[:money_columns].length == 1 165 | self.money_column_indices = [options[:money_column] - 1] 166 | elsif options[:money_columns].length == 2 167 | in_col, out_col = options[:money_columns] 168 | self.money_column_indices = [in_col - 1, out_col - 1] 169 | else 170 | puts "Unable to determine money columns, use --money-columns to specify the 1 or 2 column(s) reckon should use." 171 | end 172 | 173 | # If no money_column(s) argument is supplied, try to automatically infer money_column(s) 174 | else 175 | self.money_column_indices = results.select { |n| 176 | n[:is_money_column] 177 | }.map { |n| n[:index] } 178 | if self.money_column_indices.length == 1 179 | # TODO: print the unfiltered column number, not the filtered 180 | # ie if money column is 7, but we ignore columns 4 and 5, this prints "Using column 5 as the money column" 181 | puts "Using column #{money_column_indices.first + 1} as the money column. Use --money-colum to specify a different one." 182 | elsif self.money_column_indices.length == 2 183 | puts "Using columns #{money_column_indices[0] + 1} and #{money_column_indices[1] + 1} as money column. Use --money-columns to specify different ones." 184 | self.money_column_indices = self.money_column_indices[0..1] 185 | else 186 | puts "Unable to determine a money column, use --money-column to specify the column reckon should use." 187 | end 188 | end 189 | 190 | results.reject! { |i| money_column_indices.include?(i[:index]) } 191 | if options[:date_column] 192 | @date_column_index = options[:date_column] - 1 193 | else 194 | # sort by highest score followed by lowest index 195 | @date_column_index = results.max_by { |n| [n[:date_score], -n[:index]] }[:index] 196 | end 197 | results.reject! { |i| i[:index] == date_column_index } 198 | @date_column = DateColumn.new(columns[date_column_index], @options) 199 | 200 | @money_column = MoneyColumn.new(columns[money_column_indices[0]], @options) 201 | if money_column_indices.length == 1 202 | detect_sign_column if @money_column.positive? 203 | else 204 | @money_column.merge! MoneyColumn.new(columns[money_column_indices[1]], @options) 205 | end 206 | 207 | self.description_column_indices = results.map { |i| i[:index] } 208 | end 209 | 210 | def parse(data, filename = nil) 211 | # Use force_encoding to convert the string to utf-8 with as few invalid characters 212 | # as possible. 213 | data.force_encoding(try_encoding(data, filename)) 214 | data = data.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?') 215 | data.sub!("\xEF\xBB\xBF", '') # strip byte order marker, if it exists 216 | 217 | separator = options[:csv_separator] || guess_column_separator(data) 218 | header_lines_to_skip = options[:contains_header] || 0 219 | # -1 is skip 0 footer rows 220 | footer_lines_to_skip = (options[:contains_footer] || 0) + 1 221 | 222 | # convert to a stringio object to handle multi-line fields 223 | parser_opts = { 224 | col_sep: separator, 225 | skip_blanks: true, 226 | row_sep: :auto 227 | } 228 | begin 229 | rows = CSV.parse(StringIO.new(data), **parser_opts) 230 | rows[header_lines_to_skip..-footer_lines_to_skip] 231 | rescue CSV::MalformedCSVError 232 | # try removing N header lines before parsing 233 | index = 0 234 | count = 0 235 | while count < header_lines_to_skip 236 | index = data.index("\n", index) + 1 # skip over newline character 237 | count += 1 238 | end 239 | rows = CSV.parse(StringIO.new(data[index..]), **parser_opts) 240 | rows[0..-footer_lines_to_skip] 241 | end 242 | end 243 | 244 | def guess_column_separator(data) 245 | delimiters = [',', "\t", ';', ':', '|'] 246 | 247 | counts = [0] * delimiters.length 248 | 249 | data.each_line do |line| 250 | delimiters.each_with_index do |delim, i| 251 | counts[i] += line.count(delim) 252 | end 253 | end 254 | 255 | LOGGER.info("guessing #{delimiters[counts.index(counts.max)]} as csv separator") 256 | 257 | delimiters[counts.index(counts.max)] 258 | end 259 | 260 | def try_encoding(data, filename = nil) 261 | encoding = try_encoding_from_file(filename) 262 | 263 | cd = CharDet.detect(data) 264 | encoding ||= cd['encoding'] 265 | 266 | encoding ||= 'BINARY' 267 | 268 | LOGGER.info("suggested file encoding: #{encoding}") 269 | 270 | options[:encoding] || encoding 271 | end 272 | 273 | def try_encoding_from_file(filename = nil) 274 | return unless filename 275 | 276 | m = nil 277 | os = Gem::Platform.local.os 278 | if os == 'linux' 279 | m = `file -i #{filename}`.match(/charset=(\S+)/) 280 | elsif os == 'darwin' 281 | m = `file -I #{filename}`.match(/charset=(\S+)/) 282 | end 283 | m && m[1] 284 | end 285 | end 286 | end 287 | -------------------------------------------------------------------------------- /lib/reckon/date_column.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'date' 4 | 5 | module Reckon 6 | # Handle date columns in csv 7 | class DateColumn < Array 8 | attr_accessor :endian_precedence 9 | 10 | def initialize(arr = [], options = {}) 11 | # output date format 12 | @ledger_date_format = options[:ledger_date_format] 13 | 14 | # input date format 15 | date_format = options[:date_format] 16 | arr.each do |value| 17 | if date_format 18 | begin 19 | value = Date.strptime(value, date_format) 20 | # ruby 2.6.0 doesn't have Date::Error, but Date::Error is a subclass of 21 | # ArgumentError 22 | rescue ArgumentError 23 | puts "I'm having trouble parsing '#{value}' with the desired format: #{date_format}" 24 | exit 1 25 | end 26 | else 27 | value = [$1, $2, $3].join("/") if value =~ /^(\d{4})(\d{2})(\d{2})\d+\[\d+\:GMT\]$/ # chase format 28 | value = [$3, $2, $1].join("/") if value =~ /^(\d{2})\.(\d{2})\.(\d{4})$/ # german format 29 | value = [$3, $2, $1].join("/") if value =~ /^(\d{2})\-(\d{2})\-(\d{4})$/ # nordea format 30 | value = [$1, $2, $3].join("/") if value =~ /^(\d{4})\-(\d{2})\-(\d{2})$/ # yyyy-mm-dd format 31 | value = [$1, $2, $3].join("/") if value =~ /^(\d{4})(\d{2})(\d{2})/ # yyyymmdd format 32 | 33 | unless @endian_precedence # Try to detect endian_precedence 34 | reg_match = value.match(%r{^(\d\d)/(\d\d)/\d\d\d?\d?}) 35 | # If first one is not \d\d/\d\d/\d\d\d?\d set it to default 36 | if !reg_match 37 | @endian_precedence = %i[middle little] 38 | elsif reg_match[1].to_i > 12 39 | @endian_precedence = [:little] 40 | elsif reg_match[2].to_i > 12 41 | @endian_precedence = [:middle] 42 | end 43 | end 44 | end 45 | push(value) 46 | end 47 | 48 | # if endian_precedence still nil, raise error 49 | return if @endian_precedence || date_format 50 | 51 | raise("Unable to determine date format. Please specify using --date-format") 52 | end 53 | 54 | def for(index) 55 | value = at(index) 56 | guess = Chronic.parse(value, contex: :past, 57 | endian_precedence: @endian_precedence) 58 | if guess.to_i < 953_236_800 && value =~ %r{/} 59 | guess = Chronic.parse((value.split("/")[0...-1] + [(2000 + value.split("/").last.to_i).to_s]).join("/"), context: :past, 60 | endian_precedence: @endian_precedence) 61 | end 62 | guess&.to_date 63 | end 64 | 65 | def pretty_for(index) 66 | date = self.for(index) 67 | return "" if date.nil? 68 | 69 | date.strftime(@ledger_date_format || '%Y-%m-%d') 70 | end 71 | 72 | def self.likelihood(entry) 73 | date_score = 0 74 | date_score += 10 if entry =~ /\b(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)/i 75 | date_score += 5 if entry =~ /^[\-\/\.\d:\[\]]+$/ 76 | # add points for columns that start with date-like characters -/.\d:[] 77 | date_score += entry.gsub(/[^\-\/\.\d:\[\]]/, '').length if entry.gsub( 78 | /[^\-\/\.\d:\[\]]/, '' 79 | ).length > 3 80 | date_score -= entry.gsub(/[\-\/\.\d:\[\]]/, '').length 81 | date_score += 30 if entry =~ /^\d+[:\/\.-]\d+[:\/\.-]\d+([ :]\d+[:\/\.]\d+)?$/ 82 | date_score += 10 if entry =~ /^\d+\[\d+:GMT\]$/i 83 | 84 | # ruby 2.6.0 doesn't have Date::Error, but Date::Error is a subclass of 85 | # ArgumentError 86 | # 87 | # Sometimes DateTime.parse can throw a RangeError 88 | # See https://github.com/cantino/reckon/issues/126 89 | begin 90 | DateTime.parse(entry) 91 | date_score += 20 92 | rescue StandardError 93 | # we don't need do anything here since the column didn't parse as a date 94 | nil 95 | end 96 | 97 | date_score 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /lib/reckon/ledger_parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # From: https://www.ledger-cli.org/3.0/doc/ledger3.html#Transactions-and-Comments 4 | # 5 | # The ledger file format is quite simple, but also very flexible. It supports many 6 | # options, though typically the user can ignore most of them. They are summarized below. 7 | # 8 | # The initial character of each line determines what the line means, and how it should 9 | # be interpreted. Allowable initial characters are: 10 | # 11 | # NUMBER 12 | # A line beginning with a number denotes an entry. It may be followed by any 13 | # number of lines, each beginning with whitespace, to denote the entry's account 14 | # transactions. The format of the first line is: 15 | # 16 | # DATE[=EDATE] [*|!] [(CODE)] DESC 17 | # 18 | # If '*' appears after the date (with optional effective date), it indicates the 19 | # entry is "cleared", which can mean whatever the user wants it to mean. If '!' 20 | # appears after the date, it indicates d the entry is "pending"; i.e., tentatively 21 | # cleared from the user's point of view, but not yet actually cleared. If a 'CODE' 22 | # appears in parentheses, it may be used to indicate a check number, or the type of 23 | # the transaction. Following these is the payee, or a description of the 24 | # transaction. 25 | # 26 | # The format of each following transaction is: 27 | # 28 | # ACCOUNT AMOUNT [; NOTE] 29 | # 30 | # The 'ACCOUNT' may be surrounded by parentheses if it is a virtual transactions, or 31 | # square brackets if it is a virtual transactions that must balance. The 'AMOUNT' 32 | # can be followed by a per-unit transaction cost, by specifying '@ AMOUNT', or a 33 | # complete transaction cost with '@@ AMOUNT'. Lastly, the 'NOTE' may specify an 34 | # actual and/or effective date for the transaction by using the syntax 35 | # '[ACTUAL_DATE]' or '[=EFFECTIVE_DATE]' or '[ACTUAL_DATE=EFFECtIVE_DATE]'. 36 | # = 37 | # An automated entry. A value expression must appear after the equal sign. 38 | # 39 | # After this initial line there should be a set of one or more transactions, just as 40 | # if it were normal entry. If the amounts of the transactions have no commodity, 41 | # they will be applied as modifiers to whichever real transaction is matched by the 42 | # value expression. 43 | # ~ 44 | # A period entry. A period expression must appear after the tilde. 45 | # 46 | # After this initial line there should be a set of one or more transactions, just as 47 | # if it were normal entry. 48 | # ! 49 | # A line beginning with an exclamation mark denotes a command directive. It must be 50 | # immediately followed by the command word. The supported commands are: 51 | # 52 | # '!include' 53 | # Include the stated ledger file. 54 | # 55 | # '!account' 56 | # The account name is given is taken to be the parent of all transactions that 57 | # follow, until '!end' is seen. 58 | # 59 | # '!end' 60 | # Ends an account block. 61 | # 62 | # ; 63 | # A line beginning with a colon indicates a comment, and is ignored. 64 | # Y 65 | # If a line begins with a capital Y, it denotes the year used for all subsequent 66 | # entries that give a date without a year. The year should appear immediately after 67 | # the Y, for example: 'Y2004'. This is useful at the beginning of a file, to specify 68 | # the year for that file. If all entries specify a year, however, this command has 69 | # no effect. 70 | # 71 | # P 72 | # Specifies a historical price for a commodity. These are usually found in a pricing 73 | # history file (see the -Q option). The syntax is: 74 | # 75 | # P DATE SYMBOL PRICE 76 | # 77 | # N SYMBOL 78 | # Indicates that pricing information is to be ignored for a given symbol, nor will 79 | # quotes ever be downloaded for that symbol. Useful with a home currency, such as 80 | # the dollar ($). It is recommended that these pricing options be set in the price 81 | # database file, which defaults to ~/.pricedb. The syntax for this command is: 82 | # 83 | # N SYMBOL 84 | # 85 | # D AMOUNT 86 | # Specifies the default commodity to use, by specifying an amount in the expected 87 | # format. The entry command will use this commodity as the default when none other 88 | # can be determined. This command may be used multiple times, to set the default 89 | # flags for different commodities; whichever is seen last is used as the default 90 | # commodity. For example, to set US dollars as the default commodity, while also 91 | # setting the thousands flag and decimal flag for that commodity, use: 92 | # 93 | # D $1,000.00 94 | # 95 | # C AMOUNT1 = AMOUNT2 96 | # Specifies a commodity conversion, where the first amount is given to be equivalent 97 | # to the second amount. The first amount should use the decimal precision desired 98 | # during reporting: 99 | # 100 | # C 1.00 Kb = 1024 bytes 101 | # 102 | # i, o, b, h 103 | # These four relate to timeclock support, which permits ledger to read timelog 104 | # files. See the timeclock's documentation for more info on the syntax of its 105 | # timelog files. 106 | 107 | require 'rubygems' 108 | 109 | module Reckon 110 | # Parses ledger files 111 | class LedgerParser 112 | # ledger is an object that response to #each_line, 113 | # (i.e. a StringIO or an IO object) 114 | def initialize(options = {}) 115 | @options = options 116 | @date_format = options[:ledger_date_format] || options[:date_format] || '%Y-%m-%d' 117 | end 118 | 119 | def parse(ledger) 120 | entries = [] 121 | new_entry = {} 122 | in_comment = false 123 | comment_chars = ';#%*|' 124 | ledger.each_line do |entry| 125 | entry.rstrip! 126 | # strip comment lines 127 | in_comment = true if entry == 'comment' 128 | in_comment = false if entry == 'end comment' 129 | next if in_comment 130 | next if entry =~ /^\s*[#{comment_chars}]/ 131 | 132 | # (date, type, code, description), type and code are optional 133 | if (m = entry.match(%r{^(\d+[^\s]+)\s+([*!])?\s*(\([^)]+\))?\s*(.*)$})) 134 | add_entry(entries, new_entry) 135 | new_entry = { 136 | date: try_parse_date(m[1]), 137 | type: m[2] || "", 138 | code: m[3] && m[3].tr('()', '') || "", 139 | desc: m[4].strip, 140 | accounts: [] 141 | } 142 | elsif entry =~ /^\s*$/ && new_entry[:date] 143 | add_entry(entries, new_entry) 144 | new_entry = {} 145 | elsif new_entry[:date] && entry =~ /^\s+/ 146 | LOGGER.info("Adding new account #{entry}") 147 | new_entry[:accounts] << parse_account_line(entry) 148 | else 149 | LOGGER.info("Unknown entry type: #{entry}") 150 | add_entry(entries, new_entry) 151 | new_entry = {} 152 | end 153 | end 154 | add_entry(entries, new_entry) 155 | entries 156 | end 157 | 158 | # roughly matches ledger csv format 159 | def to_csv(ledger) 160 | return parse(ledger).flat_map do |n| 161 | n[:accounts].map do |a| 162 | row = [ 163 | n[:date].strftime(@date_format), 164 | n[:code], 165 | n[:desc], 166 | a[:name], 167 | "", # currency (not implemented) 168 | a[:amount], 169 | n[:type], 170 | "", # account comment (not implemented) 171 | ] 172 | CSV.generate_line(row).strip 173 | end 174 | end 175 | end 176 | 177 | def format_row(row, line1, line2) 178 | note = row[:note] ? "\t; #{row[:note]}" : "" 179 | out = "#{row[:pretty_date]}\t#{row[:description]}#{note}\n" 180 | out += "\t#{line1.first}\t\t\t#{line1.last}\n" 181 | out += "\t#{line2.first}\t\t\t#{line2.last}\n\n" 182 | out 183 | end 184 | 185 | private 186 | 187 | def add_entry(entries, entry) 188 | return unless entry[:date] && entry[:accounts].length > 1 189 | 190 | entry[:accounts] = balance(entry[:accounts]) 191 | entries << entry 192 | end 193 | 194 | def try_parse_date(date_str) 195 | date = Date.parse(date_str) 196 | return nil if date.year > 9999 || date.year < 1000 197 | 198 | date 199 | rescue ArgumentError 200 | nil 201 | end 202 | 203 | def parse_account_line(entry) 204 | (account_name, rest) = entry.strip.split(/\s{2,}|\t+/, 2) 205 | 206 | return { 207 | name: account_name, 208 | amount: clean_money("") 209 | } if rest.nil? || rest.empty? 210 | 211 | (value, _comment) = rest.split(/;/) 212 | return { 213 | name: account_name, 214 | amount: clean_money(value || "") 215 | } 216 | end 217 | 218 | def balance(accounts) 219 | return accounts unless accounts.any? { |i| i[:amount].nil? } 220 | 221 | sum = accounts.reduce(0) { |m, n| m + (n[:amount] || 0) } 222 | count = 0 223 | accounts.each do |account| 224 | next unless account[:amount].nil? 225 | 226 | count += 1 227 | account[:amount] = -sum 228 | end 229 | if count > 1 230 | puts "Warning: unparsable entry due to more than one missing money value." 231 | p accounts 232 | puts 233 | end 234 | 235 | accounts 236 | end 237 | 238 | def clean_money(money) 239 | return nil if money.nil? || money.empty? 240 | 241 | money.gsub(/[^0-9.-]/, '').to_f 242 | end 243 | end 244 | end 245 | -------------------------------------------------------------------------------- /lib/reckon/logger.rb: -------------------------------------------------------------------------------- 1 | module Reckon 2 | LOGGER = Logger.new(STDERR) 3 | LOGGER.level = Logger::WARN 4 | 5 | def log(tag, msg) 6 | LOGGER.add(Logger::WARN, msg, tag) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/reckon/money.rb: -------------------------------------------------------------------------------- 1 | #coding: utf-8 2 | require 'pp' 3 | 4 | module Reckon 5 | class Money 6 | include Comparable 7 | attr_accessor :amount, :currency, :suffixed 8 | def initialize(amount, options = {}) 9 | @amount_raw = amount 10 | @raw = options[:raw] 11 | 12 | @amount = parse(amount, options[:comma_separates_cents]) 13 | @amount = -@amount if options[:inverse] 14 | @currency = options[:currency] || "$" 15 | @suffixed = options[:suffixed] 16 | end 17 | 18 | def to_f 19 | return @amount 20 | end 21 | 22 | def to_s 23 | return @raw ? "#{@amount_raw} | #{@amount}" : @amount 24 | end 25 | 26 | # unary minus 27 | # ex 28 | # m = Money.new 29 | # -m 30 | def -@ 31 | Money.new(-@amount, :currency => @currency, :suffixed => @suffixed) 32 | end 33 | 34 | def <=>(mon) 35 | other_amount = mon.to_f 36 | if @amount < other_amount 37 | -1 38 | elsif @amount > other_amount 39 | 1 40 | else 41 | 0 42 | end 43 | end 44 | 45 | def pretty(negate = false) 46 | if @raw 47 | return @amount_raw unless negate 48 | 49 | return @amount_raw[0] == '-' ? @amount_raw[1..-1] : "-#{@amount_raw}" 50 | end 51 | 52 | amt = pretty_amount(@amount * (negate ? -1 : 1)) 53 | amt = if @suffixed 54 | "#{amt} #{@currency}" 55 | else 56 | amt.gsub(/^((-)|)(?=\d)/, "\\1#{@currency}") 57 | end 58 | 59 | return (@amount >= 0 ? " " : "") + amt 60 | end 61 | 62 | def self.likelihood(entry) 63 | money_score = 0 64 | # digits separated by , or . with no more than 2 trailing digits 65 | money_score += 40 if entry.match(/\d+[,.]\d{2}[^\d]*$/) 66 | money_score += 10 if entry[/^\$?\-?\$?\d+[\.,\d]*?[\.,]\d\d$/] 67 | money_score += 10 if entry[/\d+[\.,\d]*?[\.,]\d\d$/] 68 | money_score += entry.gsub(/[^\d\.\-\+,\(\)]/, '').length if entry.length < 7 69 | money_score -= entry.length if entry.length > 12 70 | money_score -= 20 if (entry !~ /^[\$\+\.\-,\d\(\)]+$/) && entry.length > 0 71 | money_score 72 | end 73 | 74 | private 75 | 76 | def pretty_amount(amount) 77 | sprintf("%0.2f", amount).reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse 78 | end 79 | 80 | def parse(value, comma_separates_cents) 81 | value = value.to_s 82 | # Empty string is treated as money with value 0 83 | return value.to_f if value.to_s.empty? 84 | 85 | invert = value.match(/^\(.*\)$/) 86 | value = value.gsub(/[^0-9,.-]/, '') 87 | value = value.tr('.', '').tr(',', '.') if comma_separates_cents 88 | value = value.tr(',', '') 89 | value = value.to_f 90 | return invert ? -value : value 91 | end 92 | 93 | end 94 | 95 | class MoneyColumn < Array 96 | def initialize(arr = [], options = {}) 97 | arr.each { |str| push(Money.new(str, options)) } 98 | end 99 | 100 | def positive? 101 | each do |money| 102 | return false if money && money < 0 103 | end 104 | true 105 | end 106 | 107 | def merge!(other_column) 108 | invert = false 109 | invert = true if positive? && other_column.positive? 110 | each_with_index do |mon, i| 111 | other = other_column[i] 112 | return nil if !mon || !other 113 | 114 | if mon != 0.0 && other == 0.0 115 | self[i] = -mon if invert 116 | elsif mon == 0.0 && other != 0.0 117 | self[i] = other 118 | else 119 | self[i] = Money.new(0) 120 | end 121 | end 122 | self 123 | end 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /lib/reckon/options.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Reckon 4 | # Singleton class for parsing command line flags 5 | class Options 6 | def self.parse_command_line_options(args = ARGV, stdin = $stdin) 7 | options = { output_file: $stdout } 8 | OptionParser.new do |opts| 9 | opts.banner = "Usage: Reckon.rb [options]" 10 | opts.separator "" 11 | 12 | opts.on("-f", "--file FILE", "The CSV file to parse") do |file| 13 | options[:file] = file 14 | end 15 | 16 | opts.on("-a", "--account NAME", "The Ledger Account this file is for") do |a| 17 | options[:bank_account] = a 18 | end 19 | 20 | options[:verbose] = Logger::WARN 21 | opts.on("-v", "--v", "Run verbosely (show info log messages)") do 22 | options[:verbose] = Logger::INFO 23 | end 24 | 25 | opts.on("", "--verbose", "Run verbosely (show info log messages)") do 26 | options[:verbose] = Logger::INFO 27 | end 28 | 29 | opts.on("", "--vv", "Run very verbosely (show debug log messages)") do 30 | options[:verbose] = Logger::DEBUG 31 | end 32 | 33 | opts.on("-i", "--inverse", "Use the negative of each amount") do |v| 34 | options[:inverse] = v 35 | end 36 | 37 | opts.on("-p", "--print-table", "Print out the parsed CSV in table form") do |p| 38 | options[:print_table] = p 39 | end 40 | 41 | opts.on("-o", "--output-file FILE", "The ledger file to append to") do |o| 42 | options[:output_file] = File.open(o, 'a') 43 | end 44 | 45 | opts.on("-l", "--learn-from FILE", 46 | "An existing ledger file to learn accounts from") do |l| 47 | options[:existing_ledger_file] = l 48 | end 49 | 50 | opts.on("", "--ignore-columns 1,2,5", 51 | "Columns to ignore, starts from 1") do |ignore| 52 | options[:ignore_columns] = ignore.split(",").map(&:to_i) 53 | end 54 | 55 | opts.on("", "--money-column 2", Integer, 56 | "Column number of the money column, starts from 1") do |col| 57 | options[:money_column] = col 58 | end 59 | 60 | opts.on("", "--money-columns 2,3", 61 | "Column number of the money columns, starts from 1 (1 or 2 columns)") do |ignore| 62 | options[:money_columns] = ignore.split(",").map(&:to_i) 63 | end 64 | 65 | opts.on("", "--raw-money", "Don't format money column (for stocks)") do |n| 66 | options[:raw] = n 67 | end 68 | 69 | options[:sort] = :date 70 | opts.on("", "--sort DATE|DESC|AMT", "Sort file by date, description, or amount") do |s| 71 | if s == 'DESC' 72 | options[:sort] = :description 73 | elsif s == 'AMT' 74 | options[:sort] = :money 75 | elsif s == 'DATE' 76 | options[:sort] = :date 77 | else 78 | raise "'#{s}' is not valid. valid sort options are DATE, DESC, AMT" 79 | end 80 | end 81 | 82 | opts.on("", "--date-column 3", Integer, 83 | "Column number of the date column, starts from 1") do |col| 84 | options[:date_column] = col 85 | end 86 | 87 | opts.on("", "--contains-header [N]", Integer, 88 | "Skip N header rows - default 1") do |hdr| 89 | options[:contains_header] = 1 90 | options[:contains_header] = hdr.to_i 91 | end 92 | 93 | opts.on("", "--contains-footer [N]", Integer, 94 | "Skip N footer rows - default 0") do |hdr| 95 | options[:contains_footer] = hdr.to_i || 0 96 | end 97 | 98 | opts.on("", "--csv-separator ','", "CSV separator (default ',')") do |sep| 99 | options[:csv_separator] = sep 100 | end 101 | 102 | opts.on("", "--comma-separates-cents", 103 | "Use comma to separate cents ($100,50 vs. $100.50)") do |c| 104 | options[:comma_separates_cents] = c 105 | end 106 | 107 | opts.on("", "--encoding 'UTF-8'", "Specify an encoding for the CSV file") do |e| 108 | options[:encoding] = e 109 | end 110 | 111 | opts.on("-c", "--currency '$'", 112 | "Currency symbol to use - default $ (ex £, EUR)") do |e| 113 | options[:currency] = e || '$' 114 | end 115 | 116 | opts.on("", "--date-format FORMAT", 117 | "CSV file date format (see `date` for format)") do |d| 118 | options[:date_format] = d 119 | end 120 | 121 | opts.on("", "--ledger-date-format FORMAT", 122 | "Ledger date format (see `date` for format)") do |d| 123 | options[:ledger_date_format] = d 124 | end 125 | 126 | opts.on("-u", "--unattended", 127 | "Don't ask questions and guess all the accounts automatically. Use with --learn-from or --account-tokens options.") do |n| 128 | options[:unattended] = n 129 | end 130 | 131 | opts.on("-t", "--account-tokens FILE", 132 | "YAML file with manually-assigned tokens for each account (see README)") do |a| 133 | options[:account_tokens_file] = a 134 | end 135 | 136 | opts.on("", "--table-output-file FILE") do |n| 137 | options[:table_output_file] = n 138 | end 139 | 140 | options[:default_into_account] = 'Expenses:Unknown' 141 | opts.on("", "--default-into-account NAME", "Default into account") do |a| 142 | options[:default_into_account] = a 143 | end 144 | 145 | options[:default_outof_account] = 'Income:Unknown' 146 | opts.on("", "--default-outof-account NAME", "Default 'out of' account") do |a| 147 | options[:default_outof_account] = a 148 | end 149 | 150 | opts.on("", "--fail-on-unknown-account", 151 | "Fail on unmatched transactions.") do |n| 152 | options[:fail_on_unknown_account] = n 153 | end 154 | 155 | opts.on("", "--suffixed", "Append currency symbol as a suffix.") do |e| 156 | options[:suffixed] = e 157 | end 158 | 159 | opts.on("", "--ledger-format FORMAT", 160 | "Output/Learn format: BEANCOUNT or LEDGER. Default: LEDGER") do |n| 161 | options[:format] = n 162 | end 163 | 164 | opts.on_tail("-h", "--help", "Show this message") do 165 | puts opts 166 | exit 167 | end 168 | 169 | opts.on_tail("--version", "Show version") do 170 | puts VERSION 171 | exit 172 | end 173 | 174 | opts.parse!(args) 175 | end 176 | 177 | if options[:file] == '-' 178 | unless options[:unattended] 179 | raise "--unattended is required to use STDIN as CSV source." 180 | end 181 | 182 | options[:string] = stdin.read 183 | end 184 | 185 | validate_options(options) 186 | 187 | return options 188 | end 189 | 190 | def self.validate_options(options) 191 | cli = HighLine.new 192 | unless options[:file] 193 | options[:file] = cli.ask("What CSV file should I parse? ") 194 | if options[:file].empty? 195 | puts "\nERROR: You must provide a CSV file to parse.\n" 196 | exit 197 | end 198 | end 199 | 200 | unless options[:bank_account] 201 | if options[:unattended] 202 | puts "ERROR: Must specify --account in unattended mode" 203 | exit 204 | end 205 | 206 | options[:bank_account] = cli.ask("What is the Ledger account name?\n") do |q| 207 | q.readline = true 208 | q.validate = /^.{2,}$/ 209 | q.default = "Assets:Bank:Checking" 210 | end 211 | end 212 | 213 | return true 214 | end 215 | end 216 | end 217 | -------------------------------------------------------------------------------- /lib/reckon/version.rb: -------------------------------------------------------------------------------- 1 | module Reckon 2 | VERSION = "0.11.1" 3 | end 4 | -------------------------------------------------------------------------------- /reckon.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | require_relative 'lib/reckon/version' 3 | 4 | Gem::Specification.new do |s| 5 | s.name = %q{reckon} 6 | s.version = Reckon::VERSION 7 | s.authors = ["Andrew Cantino", "BlackEdder", "Ben Prew"] 8 | s.email = %q{andrew@iterationlabs.com} 9 | s.homepage = %q{https://github.com/cantino/reckon} 10 | s.description = %q{Reckon automagically converts CSV files for use with the command-line accounting tool Ledger. It also helps you to select the correct accounts associated with the CSV data using Bayesian machine learning.} 11 | s.summary = %q{Utility for interactively converting and labeling CSV files for the Ledger accounting tool.} 12 | s.licenses = ['MIT'] 13 | s.required_ruby_version = ">= 2.6" 14 | 15 | s.files = `git ls-files`.split("\n") 16 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 17 | s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) } 18 | s.require_paths = ["lib"] 19 | 20 | s.add_development_dependency "rspec", ">= 1.2.9" 21 | s.add_development_dependency "pry", ">= 0.12.2" 22 | s.add_development_dependency "rantly", "= 1.2.0" 23 | s.add_runtime_dependency "chronic", ">= 0.3.0" 24 | s.add_runtime_dependency "highline", "~> 2.0" # 3.0 replaces readline with reline and breaks reckon 25 | s.add_runtime_dependency "rchardet", "= 1.8.0" 26 | s.add_runtime_dependency "matrix", ">= 0.4.2" 27 | s.add_runtime_dependency "csv", "> 0.1" 28 | s.add_runtime_dependency "abbrev", "> 0.1" 29 | end 30 | -------------------------------------------------------------------------------- /spec/cosine_training_and_test.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'pp' 4 | 5 | require 'reckon' 6 | 7 | ledger_file = ARGV[0] 8 | account = ARGV[1] 9 | seed = ARGV[2] ? ARGV[2].to_i : Random.new_seed 10 | 11 | ledger = Reckon::LedgerParser.new.parse(File.new(ledger_file)) 12 | matcher = Reckon::CosineSimilarity.new({}) 13 | 14 | train = [] 15 | test = [] 16 | 17 | def has_account(account, entry) 18 | entry[:accounts].map { |a| a[:name] }.include?(account) 19 | end 20 | 21 | entries = ledger.entries.select { |e| has_account(account, e) } 22 | 23 | r = Random.new(seed) 24 | entries.length.times do |i| 25 | r.rand < 0.9 ? train << i : test << i 26 | end 27 | 28 | train.each do |i| 29 | entry = entries[i] 30 | entry[:accounts].each do |a| 31 | matcher.add_document( 32 | a[:name], 33 | [entry[:desc], a[:amount]].join(" ") 34 | ) 35 | end 36 | end 37 | 38 | result = [nil] * test.length 39 | test.each do |i| 40 | entry = entries[i] 41 | matches = matcher.find_similar( 42 | entry[:desc] + " " + entry[:accounts][0][:amount].to_s 43 | ) 44 | 45 | if !matches[0] || !has_account(matches[0][:account], entry) 46 | result[i] = [entry, matches] 47 | end 48 | end 49 | 50 | # pp result.compact 51 | puts "using #{seed} as random seed" 52 | puts "true: #{result.count(nil)} false: #{result.count { |v| !v.nil? }}" 53 | puts(result.filter { |v| !v.nil? }) 54 | -------------------------------------------------------------------------------- /spec/data_fixtures/51-sample.csv: -------------------------------------------------------------------------------- 1 | 01/09/2015,05354 SUBWAY,8.19,,1000.00 2 | 02/18/2015,WENDY'S #6338,8.55,,1000.00 3 | 02/25/2015,WENDY'S #6338,8.55,,1000.00 4 | 02/25/2015,WENDY'S #6338,9.14,,1000.00 5 | 02/27/2015,WENDY'S #6338,5.85,,1000.00 6 | 03/09/2015,WENDY'S #6338,17.70,,1000.00 7 | 03/16/2015,WENDY'S #6338,11.15,,1000.00 8 | 03/23/2015,WENDY'S,10.12,,1000.00 9 | -------------------------------------------------------------------------------- /spec/data_fixtures/51-tokens.yml: -------------------------------------------------------------------------------- 1 | Expenses: 2 | Dining: 3 | Coffee: 4 | - 'STARBUCKS' 5 | - 'TIM HORTON' 6 | Resturant: 7 | - 'WENDY''S' 8 | - 'SUBWAY' 9 | - 'BARAKAT' 10 | -------------------------------------------------------------------------------- /spec/data_fixtures/73-sample.csv: -------------------------------------------------------------------------------- 1 | Transaction Date,Description,Amount,Category 2 | 07/06/2017,TRIPLE T CAR WASH CHAMPAIGN IL,$27.00,Automotive 3 | -------------------------------------------------------------------------------- /spec/data_fixtures/73-tokens.yml: -------------------------------------------------------------------------------- 1 | Expenses: 2 | Automotive: 3 | Car Wash: 4 | - 'TRIPLE T CAR WASH CHAMPAIGN IL' 5 | - "BIG T's CAR WASH" 6 | Maintenance: 7 | - 'Autozone' 8 | - "O'Reillys auto parts" 9 | -------------------------------------------------------------------------------- /spec/data_fixtures/73-transactions.ledger: -------------------------------------------------------------------------------- 1 | 2004/05/14 TRIPLE T CAR WASH CHAMPAIGN IL 2 | Expenses:Automotive:Car Wash -$10.00 3 | Assets:Bank:Checking 4 | 5 | 2004/05/27 AUTOZONE #2228 6 | Expenses:Automotive:Maintenance $86.56 7 | Liabilities:CreditCard -$86.56 8 | -------------------------------------------------------------------------------- /spec/data_fixtures/85-date-example.csv: -------------------------------------------------------------------------------- 1 | Visa, 4514010000000000, 2020-02-20, , GOJEK SINGAPORE, 8.10 SGD @ .976500000000, -7.91, D 2 | Visa, 4514010000000000, 2020-02-20, , GOJEK SINGAPORE, 6.00 SGD @ .976600000000, -5.86, D 3 | -------------------------------------------------------------------------------- /spec/data_fixtures/austrian_example.csv: -------------------------------------------------------------------------------- 1 | 00075757575;Abbuchung Onlinebanking 654321098765 BG/000002462 BICBICBI AT654000000065432109 Thematische Universität Stadt ;22.01.2014;22.01.2014;-18,00;EUR 2 | 00075757575;333222111333222 222111333222 OG/000002461 BICBICBIXXX AT333000000003332221 Telekom Land AG RECHNUNG 11/13 333222111333222 ;17.01.2014;17.01.2014;-9,05;EUR 3 | 00075757575;Helm BG/000002460 10000 00007878787 Muster Dr.Beispiel-Vorname ;15.01.2014;15.01.2014;+120,00;EUR 4 | 00075757575;Gutschrift Dauerauftrag BG/000002459 BICBICBI AT787000000007878787 Muster Dr.Beispiel-Vorname ;15.01.2014;15.01.2014;+22,00;EUR 5 | 00075757575;Bezahlung Bankomat MC/000002458 0001 K1 06.01.UM 18.11 Bahn 8020 FSA\\Ort\10 10 2002200EUR ;07.01.2014;06.01.2014;-37,60;EUR 6 | 00075757575;Bezahlung Bankomat 10.33 MC/000002457 0001 K1 02.01.UM 10.33 Abcdef Electronic\\Wie n\1150 0400444 ;03.01.2014;02.01.2014;-46,42;EUR 7 | 00075757575;050055556666000 OG/000002456 BKAUATWWXXX AT555500000555566665 JKL Telekommm Stadt GmbH JKL Rechnung 555666555 ;03.01.2014;03.01.2014;-17,15;EUR 8 | 00075757575;Abbuchung Einzugsermächtigung OG/000002455 INTERNATIONALER AUTOMOBIL-, 10000 00006655665 ;02.01.2014;02.01.2014;-17,40;EUR 9 | 00075757575;POLIZZE 1/01/0101010 Fondsge010101010101nsverOG/000002454 BICBICBIXXX AT101000000101010101 VERSICHERUNG NAMEDERV AG POLIZZE 1/01/0101010 Fondsgebundene Lebensversicherung - fällig 01.01. 2014 Folg eprämie ;02.01.2014;02.01.2014;-31,71;EUR 10 | 00075757575;POLIZZE 1/01/0101012 Rentenv010101010102- fälOG/000002453 BICBICBIXXX AT101000000101010102 VERSICHERUNG NAMEDERV AG POLIZZE 1/01/0101012 Rentenversicherung - fällig 01.01.20 14 Folgeprämi e ;02.01.2014;02.01.2014;-32,45;EUR 11 | 00075757575;Anlass VD/000002452 BKAUATWWBRN AT808800080880880880 Dipl.Ing.Dr. Berta Beispiel ;02.01.2014;02.01.2014;+61,90;EUR 12 | 00075757575;Abbuchung Onlinebanking 000009999999 BG/000002451 BICBICBI AT099000000009999999 Asdfjklöasdf Asdfjklöasdfjklöasdf ;02.01.2014;02.01.2014;-104,69;EUR 13 | 00075757575;Abbuchung Onlinebanking FE/000002450 AT556600055665566556 CD Stadt Efghij Club Dipl.Ing. Max Muster M005566 - Mitgliedsbeitrag 2014 ;02.01.2014;02.01.2014;-39,00;EUR 14 | -------------------------------------------------------------------------------- /spec/data_fixtures/bom_utf8_file.csv: -------------------------------------------------------------------------------- 1 | "Date","Time","TimeZone","Name","Type","Status","Currency","Gross","Fee","Net","From Email Address","To Email Address","Transaction ID","Shipping Address","Address Status","Item Title","Item ID","Shipping and Handling Amount","Insurance Amount","Sales Tax","Option 1 Name","Option 1 Value","Option 2 Name","Option 2 Value","Reference Txn ID","Invoice Number","Custom Number","Quantity","Receipt ID","Balance","Address Line 1","Address Line 2/District/Neighborhood","Town/City","State/Province/Region/County/Territory/Prefecture/Republic","Zip/Postal Code","Country","Contact Phone Number","Subject","Note","Country Code","Balance Impact" 2 | -------------------------------------------------------------------------------- /spec/data_fixtures/broker_canada_example.csv: -------------------------------------------------------------------------------- 1 | 2014-02-10,2014-02-10,Interest,ISHARES S&P/TSX CAPPED REIT IN,XRE,179,,,12.55,CAD 2 | 2014-01-16,2014-01-16,Reinvestment,ISHARES GLOBAL AGRICULTURE IND,COW,3,,,-81.57,CAD 3 | 2014-01-16,2014-01-16,Contribution,CONTRIBUTION,,,,,600.00,CAD 4 | 2014-01-16,2014-01-16,Interest,ISHARES GLOBAL AGRICULTURE IND,COW,200,,,87.05,CAD 5 | 2014-01-14,2014-01-14,Reinvestment,BMO NASDAQ 100 EQTY HEDGED TO,ZQQ,2,,,-54.72,CAD 6 | 2014-01-07,2014-01-10,Sell,BMO NASDAQ 100 EQTY HEDGED TO,ZQQ,-300,27.44,CDN,8222.05,CAD 7 | 2014-01-07,2014-01-07,Interest,BMO S&P/TSX EQUAL WEIGHT BKS I,ZEB,250,,,14.00,CAD 8 | 2013-07-02,2013-07-02,Dividend,SELECT SECTOR SPDR FD SHS BEN,XLB,130,,,38.70,USD 9 | 2013-06-27,2013-06-27,Dividend,ICICI BK SPONSORED ADR,IBN,100,,,66.70,USD 10 | 2013-06-19,2013-06-24,Buy,ISHARES S&P/TSX CAPPED REIT IN,XRE,300,15.90,CDN,-4779.95,CAD 11 | 2013-06-17,2013-06-17,Contribution,CONTRIBUTION,,,,,600.00,CAD 12 | 2013-05-22,2013-05-22,Dividend,NATBK,NA,70,,,58.10,CAD 13 | -------------------------------------------------------------------------------- /spec/data_fixtures/chase.csv: -------------------------------------------------------------------------------- 1 | DEBIT,20091224120000[0:GMT],"HOST 037196321563 MO 12/22SLICEHOST",-85.00 2 | CHECK,20091224120000[0:GMT],"CHECK 2656",-20.00 3 | DEBIT,20091224120000[0:GMT],"GITHUB 041287430274 CA 12/22GITHUB 04",-7.00 4 | CREDIT,20091223120000[0:GMT],"Some Company vendorpymt PPD ID: 59728JSL20",3520.00 5 | CREDIT,20091223120000[0:GMT],"Blarg BLARG REVENUE PPD ID: 00jah78563",1558.52 6 | DEBIT,20091221120000[0:GMT],"WEBSITE-BALANCE-17DEC09 12 12/17WEBSITE-BAL",-12.23 7 | DEBIT,20091214120000[0:GMT],"WEBSITE-BALANCE-10DEC09 12 12/10WEBSITE-BAL",-20.96 8 | CREDIT,20091211120000[0:GMT],"PAYPAL TRANSFER PPD ID: PAYPALSDSL",-116.22 9 | CREDIT,20091210120000[0:GMT],"Some Company vendorpymt PPD ID: 5KL3832735",2105.00 10 | -------------------------------------------------------------------------------- /spec/data_fixtures/danish_kroner_nordea_example.csv: -------------------------------------------------------------------------------- 1 | 16-11-2012;Dankort-nota DSB Kobenhavn 15149;16-11-2012;-48,00;26550,33 2 | 26-10-2012;Dankort-nota Ziggy Cafe 19471;26-10-2012;-79,00;26054,54 3 | 22-10-2012;Dankort-nota H&M Hennes & M 10681;23-10-2012;497,90;25433,54 4 | 12-10-2012;Visa kob DKK 995,00 WWW.ASOS.COM 00000 ;12-10-2012;-995,00;27939,54 5 | 12-09-2012;Dankort-nota B.J. TRADING E 14660;12-09-2012;-3452,90;26164,80 6 | 27-08-2012;Dankort-nota MATAS - 20319 18230;27-08-2012;-655,00;21127,45 7 | -------------------------------------------------------------------------------- /spec/data_fixtures/english_date_example.csv: -------------------------------------------------------------------------------- 1 | 24/12/2009,Check - 0000000122,122,-$76.00,"","$1,750.06" 2 | 24/12/2009,BLARG R SH 456930,"","",+$327.49,"$1,826.06" 3 | 24/12/2009,Check - 0000000112,112,-$800.00,"","$1,498.57" 4 | -------------------------------------------------------------------------------- /spec/data_fixtures/extratofake.csv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantino/reckon/bff8cce159be0d79da63209bd478d4b04fa7a1e4/spec/data_fixtures/extratofake.csv -------------------------------------------------------------------------------- /spec/data_fixtures/french_example.csv: -------------------------------------------------------------------------------- 1 | 01234567890;22/01/2014;CHEQUE 012345678901234578ABC000 0000 4381974748378178473744441;0000037;-10,00; 2 | 01234567890;22/01/2014;CHEQUE 012345678901937845500TS1 0000 7439816947047874387438445;0000038;-5,76; 3 | 01234567890;22/01/2014;CARTE 012345 CB:*0123456 XX XXXXXX XXX 33BORDEAUX;00X0X0X;-105,90; 4 | 01234567890;22/01/2014;CARTE 012345 CB:*0123456 XXXXXXXXXXX 33SAINT ANDRE D;00X0X0X;-39,99; 5 | 01234567890;22/01/2014;CARTE 012345 CB:*0123456 XXXXXXX XXXXX 33BORDEAUX;10X9X6X;-36,00; 6 | 01234567890;22/01/2014;PRLV XXXXXXXX ABONNEMENT XXXXXXXXXXXXXX.NET N.EMETTEUR: 324411;0XX0XXX;-40,00; 7 | 01234567890;21/01/2014;CARTE 012345 CB:*0123456 XXXXX XX33433ST ANDRE DE C;0POBUES;-47,12; 8 | 01234567890;21/01/2014;CARTE 012345 CB:*0123456 XXXXXXXXXXXX33433ST ANDRE DE C;0POBUER;-27,02; 9 | 01234567890;21/01/2014;CARTE 012345 CB:*0123456 XXXXXX XXXXXXXX33ST ANDRE 935/;0POBUEQ;-25,65; 10 | -------------------------------------------------------------------------------- /spec/data_fixtures/german_date_example.csv: -------------------------------------------------------------------------------- 1 | 24.12.2009,Check - 0000000122,122,-$76.00,"","$1,750.06" 2 | 24.12.2009,BLARG R SH 456930,"","",+$327.49,"$1,826.06" 3 | 24.12.2009,Check - 0000000112,112,-$800.00,"","$1,498.57" 4 | -------------------------------------------------------------------------------- /spec/data_fixtures/harder_date_example.csv: -------------------------------------------------------------------------------- 1 | 10-Nov-9,-123.12,,,TRANSFER DEBIT INTERNET TRANSFER,INTERNET TRANSFER MORTGAGE,0.00, 2 | 09-Nov-10,123.12,,,SALARY SALARY,NGHSKS46383BGDJKD FOO BAR,432.12, 3 | 04-Nov-11,-1234.00,,,TRANSFER DEBIT INTERNET TRANSFER,INTERNET TRANSFER SAV TO MECU,0.00, 4 | 04-Nov-9,1234.00,,,TRANSFER CREDIT INTERNET TRANSFER,INTERNET TRANSFER,1234.00, 5 | 28-Oct-10,-123.12,,,TRANSFER DEBIT INTERNET TRANSFER,INTERNET TRANSFER SAV TO MORTGAGE,0.00, 6 | -------------------------------------------------------------------------------- /spec/data_fixtures/ing.csv: -------------------------------------------------------------------------------- 1 | 20121115,From1,Acc,T1,IC,Af,"136,13",Incasso,SEPA Incasso, Opm1 2 | 20121112,Names,NL28 INGB 1200 3244 16,21817,GT,Bij,"375,00", Opm2 3 | 20091117,Names,NL28 INGB 1200 3244 16,21817,GT,Af,"257,50", Opm3 4 | -------------------------------------------------------------------------------- /spec/data_fixtures/intuit_mint_example.csv: -------------------------------------------------------------------------------- 1 | "12/10/2014","Dn Ing Inv","[DN]ING INV/PLA","0.01","credit","Investments","Chequing","","" 2 | "2/03/2014","Ds Lms Msp Condo","[DS]LMS598 MSP/DIV","331.63","debit","Condo Fees","Chequing","","" 3 | "2/10/2014","Ib Granville","[IB] 2601 GRANVILLE","100.00","debit","Uncategorized","Chequing","","" 4 | "2/06/2014","So Pa","[SO]PA 0005191230116379851","140.72","debit","Mortgage & Rent","Chequing","","" 5 | "2/03/2014","Dn Sun Life","[DN]SUN LIFE MSP/DIV","943.34","credit","Income","Chequing","","" 6 | "1/30/2014","Transfer to CBT (Savings)","[CW] TF 0004#3409-797","500.00","debit","Transfer","Chequing","","" 7 | "1/30/2014","Costco","[PR]COSTCO WHOLESAL","559.96","debit","Business Services","Chequing","","" 8 | -------------------------------------------------------------------------------- /spec/data_fixtures/invalid_header_example.csv: -------------------------------------------------------------------------------- 1 | - 2 | ="0234500012345678";21/11/2015;19/02/2016;36;19/02/2016;1234,37 EUR 3 | 4 | Date de l'opération;Libellé;Détail de l'écriture;Montant de l'opération;Devise 5 | 19/02/2016;VIR RECU 508160;VIR RECU 1234567834S DE: Francois REF: 123457891234567894561231 PROVENANCE: DE Allemagne ;50,00;EUR 6 | 18/02/2016;COTISATION JAZZ;COTISATION JAZZ ;-8,10;EUR 7 | -------------------------------------------------------------------------------- /spec/data_fixtures/inversed_credit_card.csv: -------------------------------------------------------------------------------- 1 | 2013/01/17,2013/01/16,2013011702,DEBIT,2226,"VODAFONE PREPAY VISA M AUCKLAND NZL",30.00 2 | 2013/01/18,2013/01/17,2013011801,DEBIT,2226,"WILSON PARKING AUCKLAND NZL",4.60 3 | 2013/01/18,2013/01/17,2013011802,DEBIT,2226,"AUCKLAND TRANSPORT HENDERSON NZL",2.00 4 | 2013/01/19,2013/01/19,2013011901,CREDIT,2226,"INTERNET PAYMENT RECEIVED ",-500.00 5 | 2013/01/26,2013/01/23,2013012601,DEBIT,2226,"ITUNES NZ CORK IRL",64.99 6 | 2013/01/26,2013/01/25,2013012602,DEBIT,2226,"VODAFONE FXFLNE BBND R NEWTON NZL",90.26 7 | 2013/01/29,2013/01/29,2013012901,CREDIT,2101,"PAYMENT RECEIVED THANK YOU ",-27.75 8 | 2013/01/30,2013/01/29,2013013001,DEBIT,2226,"AUCKLAND TRANSPORT HENDERSON NZL",3.50 9 | 2013/02/05,2013/02/03,2013020501,DEBIT,2226,"Z BEACH RD AUCKLAND NZL",129.89 10 | 2013/02/05,2013/02/03,2013020502,DEBIT,2226,"TOURNAMENT KHYBER PASS AUCKLAND NZL",8.00 11 | 2013/02/05,2013/02/04,2013020503,DEBIT,2226,"VODAFONE PREPAY VISA M AUCKLAND NZL",30.00 12 | 2013/02/08,2013/02/07,2013020801,DEBIT,2226,"AKLD TRANSPORT PARKING AUCKLAND NZL",2.50 13 | 2013/02/08,2013/02/07,2013020802,DEBIT,2226,"AUCKLAND TRANSPORT HENDERSON NZL",3.50 14 | 2013/02/12,2013/02/11,2013021201,DEBIT,2226,"AKLD TRANSPORT PARKING AUCKLAND NZL",1.50 15 | 2013/02/17,2013/02/17,2013021701,CREDIT,2226,"INTERNET PAYMENT RECEIVED ",-12.00 16 | 2013/02/17,2013/02/17,2013021702,CREDIT,2226,"INTERNET PAYMENT RECEIVED ",-18.00 17 | -------------------------------------------------------------------------------- /spec/data_fixtures/multi-line-field.csv: -------------------------------------------------------------------------------- 1 | ,311053760,2002-09-10T23:00:04,Merchant Transaction,Complete,,,"Lyft, Inc",- $21.59,,,,,,Venmo balance,,,,,Venmo,, 2 | ,,,,,,,,,,,,,,,,,$23.40,$0.00,,$0.00,"In case of errors or questions about your 3 | electronic transfers: 4 | This is a multi-line string 5 | " 6 | -------------------------------------------------------------------------------- /spec/data_fixtures/nationwide.csv: -------------------------------------------------------------------------------- 1 | 07 Nov 2013,Bank credit,Bank credit,,£500.00,£500.00 2 | 09 Oct 2013,ATM Withdrawal,Withdrawal,£20.00,,£480.00 3 | 09 Dec 2013,Visa,Supermarket,£19.77,,£460.23 4 | 10 Dec 2013,ATM Withdrawal 2,ATM Withdrawal 4,£100.00,,£360.23 5 | -------------------------------------------------------------------------------- /spec/data_fixtures/simple.csv: -------------------------------------------------------------------------------- 1 | entry1,entry2,entry3 2 | entry4,entry5,entry6 3 | -------------------------------------------------------------------------------- /spec/data_fixtures/some_other.csv: -------------------------------------------------------------------------------- 1 | DEBIT,2011/12/24,"HOST 037196321563 MO 12/22SLICEHOST",($85.00) 2 | CHECK,2010/12/24,"CHECK 2656",($20.00) 3 | DEBIT,2009/12/24,"GITHUB 041287430274 CA 12/22GITHUB 04",($7.00) 4 | CREDIT,2008/12/24,"Some Company vendorpymt PPD ID: 59728JSL20",$3520.00 5 | CREDIT,2007/12/24,"Blarg BLARG REVENUE PPD ID: 00jah78563",$1558.52 6 | DEBIT,2006/12/24,"WEBSITE-BALANCE-17DEC09 12 12/17WEBSITE-BAL",$.23 7 | DEBIT,2005/12/24,"WEBSITE-BALANCE-10DEC09 12 12/10WEBSITE-BAL",($0.96) 8 | CREDIT,2004/12/24,"PAYPAL TRANSFER PPD ID: PAYPALSDSL",($116.22) 9 | CREDIT,2003/12/24,"Some Company vendorpymt PPD ID: 5KL3832735",$2105.00 10 | -------------------------------------------------------------------------------- /spec/data_fixtures/spanish_date_example.csv: -------------------------------------------------------------------------------- 1 | 02/12/2009,Check - 0000000122,122,-$76.00,"","$1,750.06" 2 | 02/12/2009,BLARG R SH 456930,"","",+$327.49,"$1,826.06" 3 | 02/12/2009,Check - 0000000112,112,-$800.00,"","$1,498.57" 4 | -------------------------------------------------------------------------------- /spec/data_fixtures/suntrust.csv: -------------------------------------------------------------------------------- 1 | 11/01/2014,0, Deposit,0,500.00,500.00 2 | 11/02/2014,101,Check,100.00,0,400.00 3 | 11/03/2014,102,Check,100.00,0,300.00 4 | 11/04/2014,103,Check,100.00,0,200.00 5 | 11/05/2014,104,Check,100.00,0,100.00 6 | 11/06/2014,105,Check,100.00,0,0.00 7 | 11/17/2014,0, Deposit,0,700.00,700.00 8 | -------------------------------------------------------------------------------- /spec/data_fixtures/test_money_column.csv: -------------------------------------------------------------------------------- 1 | "Date","Note","Amount" 2 | "2012/3/22","DEPOSIT","50.00" 3 | "2012/3/23","TRANSFER TO SAVINGS","-10.00" 4 | -------------------------------------------------------------------------------- /spec/data_fixtures/tokens.yaml: -------------------------------------------------------------------------------- 1 | Income: 2 | Salary: 3 | - 'LÖN' 4 | - 'Salary' 5 | Expenses: 6 | Bank: 7 | - 'Comission' 8 | - /mastercard/i 9 | Rent: 10 | - '0011223344' # Landlord bank number 11 | Websites: 12 | - /web/i 13 | Books: 14 | - 'Book' 15 | '[Internal:Transfer]': # Virtual account 16 | - '4433221100' # Your own account number 17 | -------------------------------------------------------------------------------- /spec/data_fixtures/two_money_columns.csv: -------------------------------------------------------------------------------- 1 | 4/1/2008,Check - 0000000122,122,-$76.00,"","$1,750.06" 2 | 3/28/2008,BLARG R SH 456930,"","",+$327.49,"$1,826.06" 3 | 3/27/2008,Check - 0000000112,112,-$800.00,"","$1,498.57" 4 | 3/26/2008,Check - 0000000251,251,-$88.55,"","$1,298.57" 5 | 3/26/2008,Check - 0000000251,251,"","+$88.55","$1,298.57" 6 | -------------------------------------------------------------------------------- /spec/data_fixtures/yyyymmdd_date_example.csv: -------------------------------------------------------------------------------- 1 | DEBIT,20121231,"ODESK***BAL-27DEC12 650-12345 CA 12/28",-123.45 2 | -------------------------------------------------------------------------------- /spec/integration/another_bank_example/input.csv: -------------------------------------------------------------------------------- 1 | DEBIT,2011/12/24,"HOST 037196321563 MO 12/22SLICEHOST",($85.00) 2 | CHECK,2010/12/24,"CHECK 2656",($20.00) 3 | DEBIT,2009/12/24,"GITHUB 041287430274 CA 12/22GITHUB 04",($7.00) 4 | CREDIT,2008/12/24,"Some Company vendorpymt PPD ID: 59728JSL20",$3520.00 5 | CREDIT,2007/12/24,"Blarg BLARG REVENUE PPD ID: 00jah78563",$1558.52 6 | DEBIT,2006/12/24,"WEBSITE-BALANCE-17DEC09 12 12/17WEBSITE-BAL",$.23 7 | DEBIT,2005/12/24,"WEBSITE-BALANCE-10DEC09 12 12/10WEBSITE-BAL",($0.96) 8 | CREDIT,2004/12/24,"PAYPAL TRANSFER PPD ID: PAYPALSDSL",($116.22) 9 | CREDIT,2003/12/24,"Some Company vendorpymt PPD ID: 5KL3832735",$2105.00 10 | -------------------------------------------------------------------------------- /spec/integration/another_bank_example/output.ledger: -------------------------------------------------------------------------------- 1 | 2003-12-24 CREDIT; Some Company vendorpymt PPD ID: 5KL3832735 2 | Assets:Bank:Checking $2,105.00 3 | Income:Unknown 4 | 5 | 2004-12-24 CREDIT; PAYPAL TRANSFER PPD ID: PAYPALSDSL 6 | Income:Unknown 7 | Assets:Bank:Checking -$116.22 8 | 9 | 2005-12-24 DEBIT; WEBSITE-BALANCE-10DEC09 12 12/10WEBSITE-BAL 10 | Expenses:Unknown 11 | Assets:Bank:Checking -$0.96 12 | 13 | 2006-12-24 DEBIT; WEBSITE-BALANCE-17DEC09 12 12/17WEBSITE-BAL 14 | Assets:Bank:Checking $0.23 15 | Expenses:Unknown 16 | 17 | 2007-12-24 CREDIT; Blarg BLARG REVENUE PPD ID: 00jah78563 18 | Assets:Bank:Checking $1,558.52 19 | Income:Unknown 20 | 21 | 2008-12-24 CREDIT; Some Company vendorpymt PPD ID: 59728JSL20 22 | Assets:Bank:Checking $3,520.00 23 | Income:Unknown 24 | 25 | 2009-12-24 DEBIT; GITHUB 041287430274 CA 12/22GITHUB 04 26 | Expenses:Unknown 27 | Assets:Bank:Checking -$7.00 28 | 29 | 2010-12-24 CHECK; CHECK 2656 30 | Expenses:Unknown 31 | Assets:Bank:Checking -$20.00 32 | 33 | 2011-12-24 DEBIT; HOST 037196321563 MO 12/22SLICEHOST 34 | Expenses:Unknown 35 | Assets:Bank:Checking -$85.00 36 | 37 | -------------------------------------------------------------------------------- /spec/integration/another_bank_example/test_args: -------------------------------------------------------------------------------- 1 | -f input.csv --unattended --account Assets:Bank:Checking 2 | -------------------------------------------------------------------------------- /spec/integration/ask_for_account/cli_input.txt: -------------------------------------------------------------------------------- 1 | Test::Bank 2 | -------------------------------------------------------------------------------- /spec/integration/ask_for_account/expected_output: -------------------------------------------------------------------------------- 1 | 2 | Date | Amount | Description | 3 | 2003-12-24 | $2,105.00 | CREDIT; Some Company vendorpymt PPD ID: 5KL3832735 | 4 | 2004-12-24 | -$116.22 | CREDIT; PAYPAL TRANSFER PPD ID: PAYPALSDSL | 5 | 2005-12-24 | -$0.96 | DEBIT; WEBSITE-BALANCE-10DEC09 12 12/10WEBSITE-BAL | 6 | 2006-12-24 | $0.23 | DEBIT; WEBSITE-BALANCE-17DEC09 12 12/17WEBSITE-BAL | 7 | 2007-12-24 | $1,558.52 | CREDIT; Blarg BLARG REVENUE PPD ID: 00jah78563 | 8 | 2008-12-24 | $3,520.00 | CREDIT; Some Company vendorpymt PPD ID: 59728JSL20 | 9 | 2009-12-24 | -$7.00 | DEBIT; GITHUB 041287430274 CA 12/22GITHUB 04 | 10 | 2010-12-24 | -$20.00 | CHECK; CHECK 2656 | 11 | 2011-12-24 | -$85.00 | DEBIT; HOST 037196321563 MO 12/22SLICEHOST | 12 | -------------------------------------------------------------------------------- /spec/integration/ask_for_account/input.csv: -------------------------------------------------------------------------------- 1 | DEBIT,2011/12/24,"HOST 037196321563 MO 12/22SLICEHOST",($85.00) 2 | CHECK,2010/12/24,"CHECK 2656",($20.00) 3 | DEBIT,2009/12/24,"GITHUB 041287430274 CA 12/22GITHUB 04",($7.00) 4 | CREDIT,2008/12/24,"Some Company vendorpymt PPD ID: 59728JSL20",$3520.00 5 | CREDIT,2007/12/24,"Blarg BLARG REVENUE PPD ID: 00jah78563",$1558.52 6 | DEBIT,2006/12/24,"WEBSITE-BALANCE-17DEC09 12 12/17WEBSITE-BAL",$.23 7 | DEBIT,2005/12/24,"WEBSITE-BALANCE-10DEC09 12 12/10WEBSITE-BAL",($0.96) 8 | CREDIT,2004/12/24,"PAYPAL TRANSFER PPD ID: PAYPALSDSL",($116.22) 9 | CREDIT,2003/12/24,"Some Company vendorpymt PPD ID: 5KL3832735",$2105.00 10 | -------------------------------------------------------------------------------- /spec/integration/ask_for_account/test_args: -------------------------------------------------------------------------------- 1 | -f input.csv -p 2 | -------------------------------------------------------------------------------- /spec/integration/austrian_example/input.csv: -------------------------------------------------------------------------------- 1 | 00075757575;Abbuchung Onlinebanking 654321098765 BG/000002462 BICBICBI AT654000000065432109 Thematische Universität Stadt ;22.01.2014;22.01.2014;-18,00;EUR 2 | 00075757575;333222111333222 222111333222 OG/000002461 BICBICBIXXX AT333000000003332221 Telekom Land AG RECHNUNG 11/13 333222111333222 ;17.01.2014;17.01.2014;-9,05;EUR 3 | 00075757575;Helm BG/000002460 10000 00007878787 Muster Dr.Beispiel-Vorname ;15.01.2014;15.01.2014;+120,00;EUR 4 | 00075757575;Gutschrift Dauerauftrag BG/000002459 BICBICBI AT787000000007878787 Muster Dr.Beispiel-Vorname ;15.01.2014;15.01.2014;+22,00;EUR 5 | 00075757575;Bezahlung Bankomat MC/000002458 0001 K1 06.01.UM 18.11 Bahn 8020 FSA\\Ort\10 10 2002200EUR ;07.01.2014;06.01.2014;-37,60;EUR 6 | 00075757575;Bezahlung Bankomat 10.33 MC/000002457 0001 K1 02.01.UM 10.33 Abcdef Electronic\\Wie n\1150 0400444 ;03.01.2014;02.01.2014;-46,42;EUR 7 | 00075757575;050055556666000 OG/000002456 BKAUATWWXXX AT555500000555566665 JKL Telekommm Stadt GmbH JKL Rechnung 555666555 ;03.01.2014;03.01.2014;-17,15;EUR 8 | 00075757575;Abbuchung Einzugsermächtigung OG/000002455 INTERNATIONALER AUTOMOBIL-, 10000 00006655665 ;02.01.2014;02.01.2014;-17,40;EUR 9 | 00075757575;POLIZZE 1/01/0101010 Fondsge010101010101nsverOG/000002454 BICBICBIXXX AT101000000101010101 VERSICHERUNG NAMEDERV AG POLIZZE 1/01/0101010 Fondsgebundene Lebensversicherung - fällig 01.01. 2014 Folg eprämie ;02.01.2014;02.01.2014;-31,71;EUR 10 | 00075757575;POLIZZE 1/01/0101012 Rentenv010101010102- fälOG/000002453 BICBICBIXXX AT101000000101010102 VERSICHERUNG NAMEDERV AG POLIZZE 1/01/0101012 Rentenversicherung - fällig 01.01.20 14 Folgeprämi e ;02.01.2014;02.01.2014;-32,45;EUR 11 | 00075757575;Anlass VD/000002452 BKAUATWWBRN AT808800080880880880 Dipl.Ing.Dr. Berta Beispiel ;02.01.2014;02.01.2014;+61,90;EUR 12 | 00075757575;Abbuchung Onlinebanking 000009999999 BG/000002451 BICBICBI AT099000000009999999 Asdfjklöasdf Asdfjklöasdfjklöasdf ;02.01.2014;02.01.2014;-104,69;EUR 13 | 00075757575;Abbuchung Onlinebanking FE/000002450 AT556600055665566556 CD Stadt Efghij Club Dipl.Ing. Max Muster M005566 - Mitgliedsbeitrag 2014 ;02.01.2014;02.01.2014;-39,00;EUR 14 | -------------------------------------------------------------------------------- /spec/integration/austrian_example/output.ledger: -------------------------------------------------------------------------------- 1 | 2014-01-02 00075757575; Anlass VD/000002452 BKAUATWWBRN AT808800080880880880 Dipl.Ing.Dr. Berta Beispiel; 02.01.2014; EUR 2 | Assets:Bank:Checking $61.90 3 | Income:Unknown 4 | 5 | 2014-01-02 00075757575; Abbuchung Einzugsermächtigung OG/000002455 INTERNATIONALER AUTOMOBIL-, 10000 00006655665; 02.01.2014; EUR 6 | Income:Unknown 7 | Assets:Bank:Checking -$17.40 8 | 9 | 2014-01-02 00075757575; POLIZZE 1/01/0101010 Fondsge010101010101nsverOG/000002454 BICBICBIXXX AT101000000101010101 VERSICHERUNG NAMEDERV AG POLIZZE 1/01/0101010 Fondsgebundene Lebensversicherung - fällig 01.01. 2014 Folg eprämie; 02.01.2014; EUR 10 | Income:Unknown 11 | Assets:Bank:Checking -$31.71 12 | 13 | 2014-01-02 00075757575; POLIZZE 1/01/0101012 Rentenv010101010102- fälOG/000002453 BICBICBIXXX AT101000000101010102 VERSICHERUNG NAMEDERV AG POLIZZE 1/01/0101012 Rentenversicherung - fällig 01.01.20 14 Folgeprämi e; 02.01.2014; EUR 14 | Income:Unknown 15 | Assets:Bank:Checking -$32.45 16 | 17 | 2014-01-02 00075757575; Abbuchung Onlinebanking FE/000002450 AT556600055665566556 CD Stadt Efghij Club Dipl.Ing. Max Muster M005566 - Mitgliedsbeitrag 2014; 02.01.2014; EUR 18 | Income:Unknown 19 | Assets:Bank:Checking -$39.00 20 | 21 | 2014-01-02 00075757575; Abbuchung Onlinebanking 000009999999 BG/000002451 BICBICBI AT099000000009999999 Asdfjklöasdf Asdfjklöasdfjklöasdf; 02.01.2014; EUR 22 | Income:Unknown 23 | Assets:Bank:Checking -$104.69 24 | 25 | 2014-01-03 00075757575; 050055556666000 OG/000002456 BKAUATWWXXX AT555500000555566665 JKL Telekommm Stadt GmbH JKL Rechnung 555666555; 03.01.2014; EUR 26 | Income:Unknown 27 | Assets:Bank:Checking -$17.15 28 | 29 | 2014-01-03 00075757575; Bezahlung Bankomat 10.33 MC/000002457 0001 K1 02.01.UM 10.33 Abcdef Electronic\\Wie n\1150 0400444; 02.01.2014; EUR 30 | Income:Unknown 31 | Assets:Bank:Checking -$46.42 32 | 33 | 2014-01-07 00075757575; Bezahlung Bankomat MC/000002458 0001 K1 06.01.UM 18.11 Bahn 8020 FSA\\Ort\10 10 2002200EUR; 06.01.2014; EUR 34 | Income:Unknown 35 | Assets:Bank:Checking -$37.60 36 | 37 | 2014-01-15 00075757575; Helm BG/000002460 10000 00007878787 Muster Dr.Beispiel-Vorname; 15.01.2014; EUR 38 | Assets:Bank:Checking $120.00 39 | Income:Unknown 40 | 41 | 2014-01-15 00075757575; Gutschrift Dauerauftrag BG/000002459 BICBICBI AT787000000007878787 Muster Dr.Beispiel-Vorname; 15.01.2014; EUR 42 | Assets:Bank:Checking $22.00 43 | Income:Unknown 44 | 45 | 2014-01-17 00075757575; 333222111333222 222111333222 OG/000002461 BICBICBIXXX AT333000000003332221 Telekom Land AG RECHNUNG 11/13 333222111333222; 17.01.2014; EUR 46 | Income:Unknown 47 | Assets:Bank:Checking -$9.05 48 | 49 | 2014-01-22 00075757575; Abbuchung Onlinebanking 654321098765 BG/000002462 BICBICBI AT654000000065432109 Thematische Universität Stadt; 22.01.2014; EUR 50 | Income:Unknown 51 | Assets:Bank:Checking -$18.00 52 | 53 | -------------------------------------------------------------------------------- /spec/integration/austrian_example/test_args: -------------------------------------------------------------------------------- 1 | -f input.csv --unattended --account Assets:Bank:Checking \ 2 | --comma-separates-cents --csv-separator ';' 3 | -------------------------------------------------------------------------------- /spec/integration/bom_utf8_file/input.csv: -------------------------------------------------------------------------------- 1 | "Date","Time","TimeZone","Name","Type","Status","Currency","Gross","Fee","Net","From Email Address","To Email Address","Transaction ID","Shipping Address","Address Status","Item Title","Item ID","Shipping and Handling Amount","Insurance Amount","Sales Tax","Option 1 Name","Option 1 Value","Option 2 Name","Option 2 Value","Reference Txn ID","Invoice Number","Custom Number","Quantity","Receipt ID","Balance","Address Line 1","Address Line 2/District/Neighborhood","Town/City","State/Province/Region/County/Territory/Prefecture/Republic","Zip/Postal Code","Country","Contact Phone Number","Subject","Note","Country Code","Balance Impact" 2 | "12/27/2019","19:33:49","PST","Humble Bundle, Inc.","Express Checkout Payment","Completed","USD","-7.49","0.00","-7.49","test@gmail.com","test@humblebundle.com","","","Non-Confirmed","Purchase ","","0.00","","0.00","","","","","","","","1","","705.37","","","","","","","","","","","Debit" 3 | 4 | -------------------------------------------------------------------------------- /spec/integration/bom_utf8_file/output.ledger: -------------------------------------------------------------------------------- 1 | 2019-12-27 Humble Bundle, Inc.; Express Checkout Payment; Purchase 2 | Expenses:Unknown 3 | Assets:Bank:Checking -$7.49 4 | 5 | -------------------------------------------------------------------------------- /spec/integration/bom_utf8_file/test_args: -------------------------------------------------------------------------------- 1 | -f input.csv --unattended --account Assets:Bank:Checking \ 2 | --contains-header 1 \ 3 | --ignore-columns 2,3,6,7,8,9,11,12,13,14,15,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,40,41 4 | -------------------------------------------------------------------------------- /spec/integration/broker_canada_example/input.csv: -------------------------------------------------------------------------------- 1 | 2014-02-10,2014-02-10,Interest,ISHARES S&P/TSX CAPPED REIT IN,XRE,179,,,12.55,CAD 2 | 2014-01-16,2014-01-16,Reinvestment,ISHARES GLOBAL AGRICULTURE IND,COW,3,,,-81.57,CAD 3 | 2014-01-16,2014-01-16,Contribution,CONTRIBUTION,,,,,600.00,CAD 4 | 2014-01-16,2014-01-16,Interest,ISHARES GLOBAL AGRICULTURE IND,COW,200,,,87.05,CAD 5 | 2014-01-14,2014-01-14,Reinvestment,BMO NASDAQ 100 EQTY HEDGED TO,ZQQ,2,,,-54.72,CAD 6 | 2014-01-07,2014-01-10,Sell,BMO NASDAQ 100 EQTY HEDGED TO,ZQQ,-300,27.44,CDN,8222.05,CAD 7 | 2014-01-07,2014-01-07,Interest,BMO S&P/TSX EQUAL WEIGHT BKS I,ZEB,250,,,14.00,CAD 8 | 2013-07-02,2013-07-02,Dividend,SELECT SECTOR SPDR FD SHS BEN,XLB,130,,,38.70,USD 9 | 2013-06-27,2013-06-27,Dividend,ICICI BK SPONSORED ADR,IBN,100,,,66.70,USD 10 | 2013-06-19,2013-06-24,Buy,ISHARES S&P/TSX CAPPED REIT IN,XRE,300,15.90,CDN,-4779.95,CAD 11 | 2013-06-17,2013-06-17,Contribution,CONTRIBUTION,,,,,600.00,CAD 12 | 2013-05-22,2013-05-22,Dividend,NATBK,NA,70,,,58.10,CAD 13 | -------------------------------------------------------------------------------- /spec/integration/broker_canada_example/output.ledger: -------------------------------------------------------------------------------- 1 | 2013-05-22 2013-05-22; Dividend; NATBK; NA; 70; CAD 2 | Assets:Bank:Checking $58.10 3 | Income:Unknown 4 | 5 | 2013-06-17 2013-06-17; Contribution; CONTRIBUTION; CAD 6 | Assets:Bank:Checking $600.00 7 | Income:Unknown 8 | 9 | 2013-06-19 2013-06-24; Buy; ISHARES S&P/TSX CAPPED REIT IN; XRE; 300; 15.90; CDN; CAD 10 | Income:Unknown 11 | Assets:Bank:Checking -$4,779.95 12 | 13 | 2013-06-27 2013-06-27; Dividend; ICICI BK SPONSORED ADR; IBN; 100; USD 14 | Assets:Bank:Checking $66.70 15 | Income:Unknown 16 | 17 | 2013-07-02 2013-07-02; Dividend; SELECT SECTOR SPDR FD SHS BEN; XLB; 130; USD 18 | Assets:Bank:Checking $38.70 19 | Income:Unknown 20 | 21 | 2014-01-07 2014-01-10; Sell; BMO NASDAQ 100 EQTY HEDGED TO; ZQQ; -300; 27.44; CDN; CAD 22 | Assets:Bank:Checking $8,222.05 23 | Income:Unknown 24 | 25 | 2014-01-07 2014-01-07; Interest; BMO S&P/TSX EQUAL WEIGHT BKS I; ZEB; 250; CAD 26 | Assets:Bank:Checking $14.00 27 | Income:Unknown 28 | 29 | 2014-01-14 2014-01-14; Reinvestment; BMO NASDAQ 100 EQTY HEDGED TO; ZQQ; 2; CAD 30 | Income:Unknown 31 | Assets:Bank:Checking -$54.72 32 | 33 | 2014-01-16 2014-01-16; Contribution; CONTRIBUTION; CAD 34 | Assets:Bank:Checking $600.00 35 | Income:Unknown 36 | 37 | 2014-01-16 2014-01-16; Interest; ISHARES GLOBAL AGRICULTURE IND; COW; 200; CAD 38 | Assets:Bank:Checking $87.05 39 | Income:Unknown 40 | 41 | 2014-01-16 2014-01-16; Reinvestment; ISHARES GLOBAL AGRICULTURE IND; COW; 3; CAD 42 | Income:Unknown 43 | Assets:Bank:Checking -$81.57 44 | 45 | 2014-02-10 2014-02-10; Interest; ISHARES S&P/TSX CAPPED REIT IN; XRE; 179; CAD 46 | Assets:Bank:Checking $12.55 47 | Income:Unknown 48 | 49 | -------------------------------------------------------------------------------- /spec/integration/broker_canada_example/test_args: -------------------------------------------------------------------------------- 1 | -f input.csv --unattended --account Assets:Bank:Checking 2 | -------------------------------------------------------------------------------- /spec/integration/chase/account_tokens_and_regex/output.ledger: -------------------------------------------------------------------------------- 1 | 2009-12-10 CREDIT; Some Company vendorpymt PPD ID: 5KL3832735 2 | Assets:Bank:Checking $2,105.00 3 | Income:Unknown 4 | 5 | 2009-12-11 CREDIT; PAYPAL TRANSFER PPD ID: PAYPALSDSL 6 | Expenses:Unknown 7 | Assets:Bank:Checking -$116.22 8 | 9 | 2009-12-14 DEBIT; WEBSITE-BALANCE-10DEC09 12 12/10WEBSITE-BAL 10 | Expenses:Websites 11 | Assets:Bank:Checking -$20.96 12 | 13 | 2009-12-21 DEBIT; WEBSITE-BALANCE-17DEC09 12 12/17WEBSITE-BAL 14 | Expenses:Websites 15 | Assets:Bank:Checking -$12.23 16 | 17 | 2009-12-23 CREDIT; Some Company vendorpymt PPD ID: 59728JSL20 18 | Assets:Bank:Checking $3,520.00 19 | Income:Unknown 20 | 21 | 2009-12-23 CREDIT; Blarg BLARG REVENUE PPD ID: 00jah78563 22 | Assets:Bank:Checking $1,558.52 23 | Income:Unknown 24 | 25 | 2009-12-24 DEBIT; GITHUB 041287430274 CA 12/22GITHUB 04 26 | Expenses:Unknown 27 | Assets:Bank:Checking -$7.00 28 | 29 | 2009-12-24 CHECK; Book Store 30 | Expenses:Books 31 | Assets:Bank:Checking -$20.00 32 | 33 | 2009-12-24 DEBIT; HOST 037196321563 MO 12/22SLICEHOST 34 | Expenses:Unknown 35 | Assets:Bank:Checking -$85.00 36 | 37 | -------------------------------------------------------------------------------- /spec/integration/chase/account_tokens_and_regex/test_args: -------------------------------------------------------------------------------- 1 | -f ../input.csv --unattended --account 'Assets:Bank:Checking' \ 2 | --account-tokens tokens.yml 3 | -------------------------------------------------------------------------------- /spec/integration/chase/account_tokens_and_regex/tokens.yml: -------------------------------------------------------------------------------- 1 | Income: 2 | Salary: 3 | - 'LÖN' 4 | - 'Salary' 5 | Expenses: 6 | Bank: 7 | - 'Comission' 8 | - /mastercard/i 9 | Rent: 10 | - '0011223344' # Landlord bank number 11 | Websites: 12 | - /web/i 13 | Books: 14 | - 'Book' 15 | '[Internal:Transfer]': # Virtual account 16 | - '4433221100' # Your own account number 17 | -------------------------------------------------------------------------------- /spec/integration/chase/default_account_names/output.ledger: -------------------------------------------------------------------------------- 1 | 2009-12-10 CREDIT; Some Company vendorpymt PPD ID: 5KL3832735 2 | Assets:Bank:Checking $2,105.00 3 | Income:Default 4 | 5 | 2009-12-11 CREDIT; PAYPAL TRANSFER PPD ID: PAYPALSDSL 6 | Income:Default 7 | Assets:Bank:Checking -$116.22 8 | 9 | 2009-12-14 DEBIT; WEBSITE-BALANCE-10DEC09 12 12/10WEBSITE-BAL 10 | Expenses:Default 11 | Assets:Bank:Checking -$20.96 12 | 13 | 2009-12-21 DEBIT; WEBSITE-BALANCE-17DEC09 12 12/17WEBSITE-BAL 14 | Expenses:Default 15 | Assets:Bank:Checking -$12.23 16 | 17 | 2009-12-23 CREDIT; Some Company vendorpymt PPD ID: 59728JSL20 18 | Assets:Bank:Checking $3,520.00 19 | Income:Default 20 | 21 | 2009-12-23 CREDIT; Blarg BLARG REVENUE PPD ID: 00jah78563 22 | Assets:Bank:Checking $1,558.52 23 | Income:Default 24 | 25 | 2009-12-24 DEBIT; GITHUB 041287430274 CA 12/22GITHUB 04 26 | Expenses:Default 27 | Assets:Bank:Checking -$7.00 28 | 29 | 2009-12-24 CHECK; Book Store 30 | Expenses:Default 31 | Assets:Bank:Checking -$20.00 32 | 33 | 2009-12-24 DEBIT; HOST 037196321563 MO 12/22SLICEHOST 34 | Expenses:Default 35 | Assets:Bank:Checking -$85.00 36 | 37 | -------------------------------------------------------------------------------- /spec/integration/chase/default_account_names/test_args: -------------------------------------------------------------------------------- 1 | -f ../input.csv --unattended --account Assets:Bank:Checking \ 2 | --default-into-account Expenses:Default \ 3 | --default-outof-account Income:Default 4 | -------------------------------------------------------------------------------- /spec/integration/chase/input.csv: -------------------------------------------------------------------------------- 1 | DEBIT,20091224120000[0:GMT],"HOST 037196321563 MO 12/22SLICEHOST",-85.00 2 | CHECK,20091224120000[0:GMT],"Book Store",-20.00 3 | DEBIT,20091224120000[0:GMT],"GITHUB 041287430274 CA 12/22GITHUB 04",-7.00 4 | CREDIT,20091223120000[0:GMT],"Some Company vendorpymt PPD ID: 59728JSL20",3520.00 5 | CREDIT,20091223120000[0:GMT],"Blarg BLARG REVENUE PPD ID: 00jah78563",1558.52 6 | DEBIT,20091221120000[0:GMT],"WEBSITE-BALANCE-17DEC09 12 12/17WEBSITE-BAL",-12.23 7 | DEBIT,20091214120000[0:GMT],"WEBSITE-BALANCE-10DEC09 12 12/10WEBSITE-BAL",-20.96 8 | CREDIT,20091211120000[0:GMT],"PAYPAL TRANSFER PPD ID: PAYPALSDSL",-116.22 9 | CREDIT,20091210120000[0:GMT],"Some Company vendorpymt PPD ID: 5KL3832735",2105.00 10 | -------------------------------------------------------------------------------- /spec/integration/chase/learn_from_existing/learn.ledger: -------------------------------------------------------------------------------- 1 | 2004/05/14 * Pay day 2 | Assets:Bank:Checking $500.00 3 | Income:Salary 4 | 5 | 2004/05/27 Book Store 6 | Expenses:Books $20.00 7 | Liabilities:MasterCard 8 | -------------------------------------------------------------------------------- /spec/integration/chase/learn_from_existing/output.ledger: -------------------------------------------------------------------------------- 1 | 2009-12-10 CREDIT; Some Company vendorpymt PPD ID: 5KL3832735 2 | Assets:Bank:Checking $2,105.00 3 | Income:Unknown 4 | 5 | 2009-12-11 CREDIT; PAYPAL TRANSFER PPD ID: PAYPALSDSL 6 | Income:Unknown 7 | Assets:Bank:Checking -$116.22 8 | 9 | 2009-12-14 DEBIT; WEBSITE-BALANCE-10DEC09 12 12/10WEBSITE-BAL 10 | Expenses:Unknown 11 | Assets:Bank:Checking -$20.96 12 | 13 | 2009-12-21 DEBIT; WEBSITE-BALANCE-17DEC09 12 12/17WEBSITE-BAL 14 | Expenses:Unknown 15 | Assets:Bank:Checking -$12.23 16 | 17 | 2009-12-23 CREDIT; Some Company vendorpymt PPD ID: 59728JSL20 18 | Assets:Bank:Checking $3,520.00 19 | Income:Unknown 20 | 21 | 2009-12-23 CREDIT; Blarg BLARG REVENUE PPD ID: 00jah78563 22 | Assets:Bank:Checking $1,558.52 23 | Income:Unknown 24 | 25 | 2009-12-24 DEBIT; GITHUB 041287430274 CA 12/22GITHUB 04 26 | Expenses:Unknown 27 | Assets:Bank:Checking -$7.00 28 | 29 | 2009-12-24 CHECK; Book Store 30 | Expenses:Books 31 | Assets:Bank:Checking -$20.00 32 | 33 | 2009-12-24 DEBIT; HOST 037196321563 MO 12/22SLICEHOST 34 | Expenses:Unknown 35 | Assets:Bank:Checking -$85.00 36 | 37 | -------------------------------------------------------------------------------- /spec/integration/chase/learn_from_existing/test_args: -------------------------------------------------------------------------------- 1 | -f ../input.csv --unattended --account Assets:Bank:Checking -l learn.ledger 2 | -------------------------------------------------------------------------------- /spec/integration/chase/simple/output.ledger: -------------------------------------------------------------------------------- 1 | 2009-12-10 CREDIT; Some Company vendorpymt PPD ID: 5KL3832735 2 | Assets:Bank:Checking $2,105.00 3 | Income:Unknown 4 | 5 | 2009-12-11 CREDIT; PAYPAL TRANSFER PPD ID: PAYPALSDSL 6 | Income:Unknown 7 | Assets:Bank:Checking -$116.22 8 | 9 | 2009-12-14 DEBIT; WEBSITE-BALANCE-10DEC09 12 12/10WEBSITE-BAL 10 | Expenses:Unknown 11 | Assets:Bank:Checking -$20.96 12 | 13 | 2009-12-21 DEBIT; WEBSITE-BALANCE-17DEC09 12 12/17WEBSITE-BAL 14 | Expenses:Unknown 15 | Assets:Bank:Checking -$12.23 16 | 17 | 2009-12-23 CREDIT; Some Company vendorpymt PPD ID: 59728JSL20 18 | Assets:Bank:Checking $3,520.00 19 | Income:Unknown 20 | 21 | 2009-12-23 CREDIT; Blarg BLARG REVENUE PPD ID: 00jah78563 22 | Assets:Bank:Checking $1,558.52 23 | Income:Unknown 24 | 25 | 2009-12-24 DEBIT; GITHUB 041287430274 CA 12/22GITHUB 04 26 | Expenses:Unknown 27 | Assets:Bank:Checking -$7.00 28 | 29 | 2009-12-24 CHECK; Book Store 30 | Expenses:Unknown 31 | Assets:Bank:Checking -$20.00 32 | 33 | 2009-12-24 DEBIT; HOST 037196321563 MO 12/22SLICEHOST 34 | Expenses:Unknown 35 | Assets:Bank:Checking -$85.00 36 | 37 | -------------------------------------------------------------------------------- /spec/integration/chase/simple/test_args: -------------------------------------------------------------------------------- 1 | -f ../input.csv --unattended --account 'Assets:Bank:Checking' 2 | -------------------------------------------------------------------------------- /spec/integration/danish_kroner_nordea_example/input.csv: -------------------------------------------------------------------------------- 1 | 16-11-2012;Dankort-nota DSB Kobenhavn 15149;16-11-2012;-48,00;26550,33 2 | 26-10-2012;Dankort-nota Ziggy Cafe 19471;26-10-2012;-79,00;26054,54 3 | 22-10-2012;Dankort-nota H&M Hennes & M 10681;23-10-2012;497,90;25433,54 4 | 12-10-2012;Visa kob DKK 995,00 WWW.ASOS.COM 00000 ;12-10-2012;-995,00;27939,54 5 | 12-09-2012;Dankort-nota B.J. TRADING E 14660;12-09-2012;-3452,90;26164,80 6 | 27-08-2012;Dankort-nota MATAS - 20319 18230;27-08-2012;-655,00;21127,45 7 | -------------------------------------------------------------------------------- /spec/integration/danish_kroner_nordea_example/output.ledger: -------------------------------------------------------------------------------- 1 | 2012-08-27 Dankort-nota MATAS - 20319 18230; 27-08-2012; 21127,45 2 | Expenses:Unknown 3 | Assets:Bank:Checking -$655.00 4 | 5 | 2012-09-12 Dankort-nota B.J. TRADING E 14660; 12-09-2012; 26164,80 6 | Expenses:Unknown 7 | Assets:Bank:Checking -$3,452.90 8 | 9 | 2012-10-12 Visa kob DKK 995,00 WWW.ASOS.COM 00000; 12-10-2012; 27939,54 10 | Expenses:Unknown 11 | Assets:Bank:Checking -$995.00 12 | 13 | 2012-10-22 Dankort-nota H&M Hennes & M 10681; 23-10-2012; 25433,54 14 | Assets:Bank:Checking $497.90 15 | Expenses:Unknown 16 | 17 | 2012-10-26 Dankort-nota Ziggy Cafe 19471; 26-10-2012; 26054,54 18 | Expenses:Unknown 19 | Assets:Bank:Checking -$79.00 20 | 21 | 2012-11-16 Dankort-nota DSB Kobenhavn 15149; 16-11-2012; 26550,33 22 | Expenses:Unknown 23 | Assets:Bank:Checking -$48.00 24 | 25 | -------------------------------------------------------------------------------- /spec/integration/danish_kroner_nordea_example/test_args: -------------------------------------------------------------------------------- 1 | -f input.csv --unattended --account Assets:Bank:Checking --csv-separator ';' --comma-separates-cents 2 | -------------------------------------------------------------------------------- /spec/integration/english_date_example/input.csv: -------------------------------------------------------------------------------- 1 | 24/12/2009,Check - 0000000122,122,-$76.00,"","$1,750.06" 2 | 24/12/2009,BLARG R SH 456930,"","",+$327.49,"$1,826.06" 3 | 24/12/2009,Check - 0000000112,112,-$800.00,"","$1,498.57" 4 | -------------------------------------------------------------------------------- /spec/integration/english_date_example/output.ledger: -------------------------------------------------------------------------------- 1 | 2009-12-24 BLARG R SH 456930; $1,826.06 2 | Assets:Bank:Checking $327.49 3 | Income:Unknown 4 | 5 | 2009-12-24 Check - 0000000122; 122; $1,750.06 6 | Income:Unknown 7 | Assets:Bank:Checking -$76.00 8 | 9 | 2009-12-24 Check - 0000000112; 112; $1,498.57 10 | Income:Unknown 11 | Assets:Bank:Checking -$800.00 12 | 13 | -------------------------------------------------------------------------------- /spec/integration/english_date_example/test_args: -------------------------------------------------------------------------------- 1 | -f input.csv --unattended --account Assets:Bank:Checking 2 | -------------------------------------------------------------------------------- /spec/integration/extratofake/input.csv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantino/reckon/bff8cce159be0d79da63209bd478d4b04fa7a1e4/spec/integration/extratofake/input.csv -------------------------------------------------------------------------------- /spec/integration/extratofake/output.ledger: -------------------------------------------------------------------------------- 1 | 2012-10-31 Saldo Anterior; 0 2 | Assets:Bank:Checking $100.00 3 | Income:Unknown 4 | 5 | 2012-11-01 Proventos; 496774 6 | Assets:Bank:Checking $1,000.00 7 | Income:Unknown 8 | 9 | 2012-11-01 0000-9; Transferência on line - 01/11 4885 256620-6 XXXXXXXXXXXXX; 224885000256620 10 | Assets:Bank:Checking $100.00 11 | Income:Unknown 12 | 13 | 2012-11-01 Benefício; 496775 14 | Assets:Bank:Checking $100.00 15 | Income:Unknown 16 | 17 | 2012-11-01 Depósito COMPE - 033 0502 27588602104 XXXXXXXXXXXXXX; 101150 18 | Assets:Bank:Checking $100.00 19 | Income:Unknown 20 | 21 | 2012-11-01 0000-0; Compra com Cartão - 01/11 09:45 XXXXXXXXXXX; 135102 22 | Income:Unknown 23 | Assets:Bank:Checking -$1.00 24 | 25 | 2012-11-01 Cobrança de I.O.F.; 391100701 26 | Expenses:Unknown 27 | Assets:Bank:Checking -$1.00 28 | 29 | 2012-11-01 0000-0; Compra com Cartão - 01/11 09:48 XXXXXXXXXXX; 235338 30 | Income:Unknown 31 | Assets:Bank:Checking -$10.00 32 | 33 | 2012-11-01 0000-0; Compra com Cartão - 01/11 12:35 XXXXXXXX; 345329 34 | Income:Unknown 35 | Assets:Bank:Checking -$10.00 36 | 37 | 2012-11-01 0000-0; Compra com Cartão - 01/11 23:57 XXXXXXXXXXXXXXXX; 686249 38 | Income:Unknown 39 | Assets:Bank:Checking -$10.00 40 | 41 | 2012-11-01 0000-0; Saque com cartão - 01/11 13:17 XXXXXXXXXXXXXXXX; 11317296267021 42 | Income:Unknown 43 | Assets:Bank:Checking -$10.00 44 | 45 | 2012-11-01 Pagto conta telefone - VIVO DF; 110101 46 | Expenses:Unknown 47 | Assets:Bank:Checking -$100.00 48 | 49 | 2012-11-05 0000-0; Compra com Cartão - 02/11 17:18 XXXXXXXXXXXXX; 262318 50 | Income:Unknown 51 | Assets:Bank:Checking -$1.00 52 | 53 | 2012-11-05 0000-0; Compra com Cartão - 02/11 23:19 XXXXXXXXXXX; 683985 54 | Income:Unknown 55 | Assets:Bank:Checking -$1.00 56 | 57 | 2012-11-05 0000-0; Compra com Cartão - 03/11 11:08 XXXXXXXX; 840112 58 | Income:Unknown 59 | Assets:Bank:Checking -$1.00 60 | 61 | 2012-11-05 0000-0; Compra com Cartão - 02/11 16:57 XXXXXXXXXXXX; 161057 62 | Income:Unknown 63 | Assets:Bank:Checking -$10.00 64 | 65 | 2012-11-05 0000-0; Compra com Cartão - 03/11 01:19 XXXXXXXXXXXXXXXX; 704772 66 | Income:Unknown 67 | Assets:Bank:Checking -$10.00 68 | 69 | 2012-11-05 0000-0; Compra com Cartão - 03/11 18:57 XXXXXXXXXXXXXXX; 168279 70 | Income:Unknown 71 | Assets:Bank:Checking -$10.00 72 | 73 | 2012-11-05 0000-0; Compra com Cartão - 05/11 12:32 XXXXXXXXXXXXXXXXX; 245166 74 | Income:Unknown 75 | Assets:Bank:Checking -$10.00 76 | 77 | 2012-11-05 0000-0; Saque com cartão - 05/11 19:24 XXXXXXXXXXXXXXXXX; 51924256267021 78 | Income:Unknown 79 | Assets:Bank:Checking -$10.00 80 | 81 | 2012-11-05 0000-0; Compra com Cartão - 02/11 22:46 XXXXXXXXXXX; 382002 82 | Income:Unknown 83 | Assets:Bank:Checking -$100.00 84 | 85 | 2012-11-05 0000-0; Transferência on line - 05/11 4885 256620-6 XXXXXXXXXXXXX; 224885000256620 86 | Income:Unknown 87 | Assets:Bank:Checking -$100.00 88 | 89 | 2012-11-05 Pagamento de Título - XXXXXXXXXXXXXXXXXXX; 110501 90 | Expenses:Unknown 91 | Assets:Bank:Checking -$100.00 92 | 93 | -------------------------------------------------------------------------------- /spec/integration/extratofake/test_args: -------------------------------------------------------------------------------- 1 | -f input.csv --unattended --account Assets:Bank:Checking 2 | -------------------------------------------------------------------------------- /spec/integration/french_example/input.csv: -------------------------------------------------------------------------------- 1 | 01234567890;22/01/2014;CHEQUE 012345678901234578ABC000 0000 4381974748378178473744441;0000037;-10,00; 2 | 01234567890;22/01/2014;CHEQUE 012345678901937845500TS1 0000 7439816947047874387438445;0000038;-5,76; 3 | 01234567890;22/01/2014;CARTE 012345 CB:*0123456 XX XXXXXX XXX 33BORDEAUX;00X0X0X;-105,90; 4 | 01234567890;22/01/2014;CARTE 012345 CB:*0123456 XXXXXXXXXXX 33SAINT ANDRE D;00X0X0X;-39,99; 5 | 01234567890;22/01/2014;CARTE 012345 CB:*0123456 XXXXXXX XXXXX 33BORDEAUX;10X9X6X;-36,00; 6 | 01234567890;22/01/2014;PRLV XXXXXXXX ABONNEMENT XXXXXXXXXXXXXX.NET N.EMETTEUR: 324411;0XX0XXX;-40,00; 7 | 01234567890;21/01/2014;CARTE 012345 CB:*0123456 XXXXX XX33433ST ANDRE DE C;0POBUES;-47,12; 8 | 01234567890;21/01/2014;CARTE 012345 CB:*0123456 XXXXXXXXXXXX33433ST ANDRE DE C;0POBUER;-27,02; 9 | 01234567890;21/01/2014;CARTE 012345 CB:*0123456 XXXXXX XXXXXXXX33ST ANDRE 935/;0POBUEQ;-25,65; 10 | -------------------------------------------------------------------------------- /spec/integration/french_example/output.ledger: -------------------------------------------------------------------------------- 1 | 2014-01-21 01234567890; CARTE 012345 CB:*0123456 XXXXXX XXXXXXXX33ST ANDRE 935/; 0POBUEQ 2 | Expenses:Unknown 3 | Assets:Bank:Checking -$25.65 4 | 5 | 2014-01-21 01234567890; CARTE 012345 CB:*0123456 XXXXXXXXXXXX33433ST ANDRE DE C; 0POBUER 6 | Expenses:Unknown 7 | Assets:Bank:Checking -$27.02 8 | 9 | 2014-01-21 01234567890; CARTE 012345 CB:*0123456 XXXXX XX33433ST ANDRE DE C; 0POBUES 10 | Expenses:Unknown 11 | Assets:Bank:Checking -$47.12 12 | 13 | 2014-01-22 01234567890; CHEQUE 012345678901937845500TS1 0000 7439816947047874387438445; 0000038 14 | Expenses:Unknown 15 | Assets:Bank:Checking -$5.76 16 | 17 | 2014-01-22 01234567890; CHEQUE 012345678901234578ABC000 0000 4381974748378178473744441; 0000037 18 | Expenses:Unknown 19 | Assets:Bank:Checking -$10.00 20 | 21 | 2014-01-22 01234567890; CARTE 012345 CB:*0123456 XXXXXXX XXXXX 33BORDEAUX; 10X9X6X 22 | Expenses:Unknown 23 | Assets:Bank:Checking -$36.00 24 | 25 | 2014-01-22 01234567890; CARTE 012345 CB:*0123456 XXXXXXXXXXX 33SAINT ANDRE D; 00X0X0X 26 | Expenses:Unknown 27 | Assets:Bank:Checking -$39.99 28 | 29 | 2014-01-22 01234567890; PRLV XXXXXXXX ABONNEMENT XXXXXXXXXXXXXX.NET N.EMETTEUR: 324411; 0XX0XXX 30 | Expenses:Unknown 31 | Assets:Bank:Checking -$40.00 32 | 33 | 2014-01-22 01234567890; CARTE 012345 CB:*0123456 XX XXXXXX XXX 33BORDEAUX; 00X0X0X 34 | Expenses:Unknown 35 | Assets:Bank:Checking -$105.90 36 | 37 | -------------------------------------------------------------------------------- /spec/integration/french_example/test_args: -------------------------------------------------------------------------------- 1 | -f input.csv --unattended --account Assets:Bank:Checking \ 2 | --comma-separates-cents --csv-separator ';' 3 | -------------------------------------------------------------------------------- /spec/integration/german_date_example/input.csv: -------------------------------------------------------------------------------- 1 | 24.12.2009,Check - 0000000122,122,-$76.00,"","$1,750.06" 2 | 24.12.2009,BLARG R SH 456930,"","",+$327.49,"$1,826.06" 3 | 24.12.2009,Check - 0000000112,112,-$800.00,"","$1,498.57" 4 | -------------------------------------------------------------------------------- /spec/integration/german_date_example/output.ledger: -------------------------------------------------------------------------------- 1 | 2009-12-24 BLARG R SH 456930; $1,826.06 2 | Assets:Bank:Checking $327.49 3 | Income:Unknown 4 | 5 | 2009-12-24 Check - 0000000122; 122; $1,750.06 6 | Income:Unknown 7 | Assets:Bank:Checking -$76.00 8 | 9 | 2009-12-24 Check - 0000000112; 112; $1,498.57 10 | Income:Unknown 11 | Assets:Bank:Checking -$800.00 12 | 13 | -------------------------------------------------------------------------------- /spec/integration/german_date_example/test_args: -------------------------------------------------------------------------------- 1 | -f input.csv --unattended --account Assets:Bank:Checking 2 | -------------------------------------------------------------------------------- /spec/integration/harder_date_example/input.csv: -------------------------------------------------------------------------------- 1 | 10-Nov-9,-123.12,,,TRANSFER DEBIT INTERNET TRANSFER,INTERNET TRANSFER MORTGAGE,0.00, 2 | 09-Nov-10,123.12,,,SALARY SALARY,NGHSKS46383BGDJKD FOO BAR,432.12, 3 | 04-Nov-11,-1234.00,,,TRANSFER DEBIT INTERNET TRANSFER,INTERNET TRANSFER SAV TO MECU,0.00, 4 | 04-Nov-9,1234.00,,,TRANSFER CREDIT INTERNET TRANSFER,INTERNET TRANSFER,1234.00, 5 | 28-Oct-10,-123.12,,,TRANSFER DEBIT INTERNET TRANSFER,INTERNET TRANSFER SAV TO MORTGAGE,0.00, 6 | -------------------------------------------------------------------------------- /spec/integration/harder_date_example/output.ledger: -------------------------------------------------------------------------------- 1 | 2009-11-04 TRANSFER CREDIT INTERNET TRANSFER; INTERNET TRANSFER; 1234.00 2 | Assets:Bank:Checking $1,234.00 3 | Income:Unknown 4 | 5 | 2009-11-10 TRANSFER DEBIT INTERNET TRANSFER; INTERNET TRANSFER MORTGAGE; 0.00 6 | Income:Unknown 7 | Assets:Bank:Checking -$123.12 8 | 9 | 2010-10-28 TRANSFER DEBIT INTERNET TRANSFER; INTERNET TRANSFER SAV TO MORTGAGE; 0.00 10 | Income:Unknown 11 | Assets:Bank:Checking -$123.12 12 | 13 | 2010-11-09 SALARY SALARY; NGHSKS46383BGDJKD FOO BAR; 432.12 14 | Assets:Bank:Checking $123.12 15 | Income:Unknown 16 | 17 | 2011-11-04 TRANSFER DEBIT INTERNET TRANSFER; INTERNET TRANSFER SAV TO MECU; 0.00 18 | Income:Unknown 19 | Assets:Bank:Checking -$1,234.00 20 | 21 | -------------------------------------------------------------------------------- /spec/integration/harder_date_example/test_args: -------------------------------------------------------------------------------- 1 | -f input.csv --unattended --account Assets:Bank:Checking 2 | -------------------------------------------------------------------------------- /spec/integration/ing/input.csv: -------------------------------------------------------------------------------- 1 | 20121115,From1,Acc,T1,IC,Af,"136,13",Incasso,SEPA Incasso, Opm1 2 | 20121112,Names,NL28 INGB 1200 3244 16,21817,GT,Bij,"375,00", Opm2 3 | 20091117,Names,NL28 INGB 1200 3244 16,21817,GT,Af,"257,50", Opm3 4 | -------------------------------------------------------------------------------- /spec/integration/ing/output.ledger: -------------------------------------------------------------------------------- 1 | 2009-11-17 Names; NL28 INGB 1200 3244 16; 21817; GT; Af; Opm3 2 | Expenses:Unknown 3 | Assets:Bank:Checking -$257.50 4 | 5 | 2012-11-12 Names; NL28 INGB 1200 3244 16; 21817; GT; Bij; Opm2 6 | Assets:Bank:Checking $375.00 7 | Expenses:Unknown 8 | 9 | 2012-11-15 From1; Acc; T1; IC; Af; Incasso; SEPA Incasso; Opm1 10 | Expenses:Unknown 11 | Assets:Bank:Checking -$136.13 12 | 13 | -------------------------------------------------------------------------------- /spec/integration/ing/test_args: -------------------------------------------------------------------------------- 1 | -f input.csv --unattended --account Assets:Bank:Checking --comma-separates-cents 2 | -------------------------------------------------------------------------------- /spec/integration/intuit_mint_example/input.csv: -------------------------------------------------------------------------------- 1 | "12/10/2014","Dn Ing Inv","[DN]ING INV/PLA","0.01","credit","Investments","Chequing","","" 2 | "2/03/2014","Ds Lms Msp Condo","[DS]LMS598 MSP/DIV","331.63","debit","Condo Fees","Chequing","","" 3 | "2/10/2014","Ib Granville","[IB] 2601 GRANVILLE","100.00","debit","Uncategorized","Chequing","","" 4 | "2/06/2014","So Pa","[SO]PA 0005191230116379851","140.72","debit","Mortgage & Rent","Chequing","","" 5 | "2/03/2014","Dn Sun Life","[DN]SUN LIFE MSP/DIV","943.34","credit","Income","Chequing","","" 6 | "1/30/2014","Transfer to CBT (Savings)","[CW] TF 0004#3409-797","500.00","debit","Transfer","Chequing","","" 7 | "1/30/2014","Costco","[PR]COSTCO WHOLESAL","559.96","debit","Business Services","Chequing","","" 8 | -------------------------------------------------------------------------------- /spec/integration/intuit_mint_example/output.ledger: -------------------------------------------------------------------------------- 1 | 2014-01-30 Transfer to CBT (Savings); [CW] TF 0004#3409-797; debit; Transfer; Chequing 2 | Expenses:Unknown 3 | Assets:Bank:Checking -$500.00 4 | 5 | 2014-01-30 Costco; [PR]COSTCO WHOLESAL; debit; Business Services; Chequing 6 | Expenses:Unknown 7 | Assets:Bank:Checking -$559.96 8 | 9 | 2014-02-03 Dn Sun Life; [DN]SUN LIFE MSP/DIV; credit; Income; Chequing 10 | Assets:Bank:Checking $943.34 11 | Expenses:Unknown 12 | 13 | 2014-02-03 Ds Lms Msp Condo; [DS]LMS598 MSP/DIV; debit; Condo Fees; Chequing 14 | Expenses:Unknown 15 | Assets:Bank:Checking -$331.63 16 | 17 | 2014-02-06 So Pa; [SO]PA 0005191230116379851; debit; Mortgage & Rent; Chequing 18 | Expenses:Unknown 19 | Assets:Bank:Checking -$140.72 20 | 21 | 2014-02-10 Ib Granville; [IB] 2601 GRANVILLE; debit; Uncategorized; Chequing 22 | Expenses:Unknown 23 | Assets:Bank:Checking -$100.00 24 | 25 | 2014-12-10 Dn Ing Inv; [DN]ING INV/PLA; credit; Investments; Chequing 26 | Assets:Bank:Checking $0.01 27 | Expenses:Unknown 28 | 29 | -------------------------------------------------------------------------------- /spec/integration/intuit_mint_example/test_args: -------------------------------------------------------------------------------- 1 | -f input.csv --unattended --account Assets:Bank:Checking 2 | -------------------------------------------------------------------------------- /spec/integration/invalid_header_example/input.csv: -------------------------------------------------------------------------------- 1 | - 2 | ="0234500012345678";21/11/2015;19/02/2016;36;19/02/2016;1234,37 EUR 3 | 4 | Date de l'opération;Libellé;Détail de l'écriture;Montant de l'opération;Devise 5 | 19/02/2016;VIR RECU 508160;VIR RECU 1234567834S DE: Francois REF: 123457891234567894561231 PROVENANCE: DE Allemagne ;50,00;EUR 6 | 18/02/2016;COTISATION JAZZ;COTISATION JAZZ ;-8,10;EUR 7 | -------------------------------------------------------------------------------- /spec/integration/invalid_header_example/output.ledger: -------------------------------------------------------------------------------- 1 | 2016-02-18 COTISATION JAZZ; COTISATION JAZZ; EUR 2 | Expenses:Unknown 3 | Assets:Bank:Checking -$8.10 4 | 5 | 2016-02-19 VIR RECU 508160; VIR RECU 1234567834S DE: Francois REF: 123457891234567894561231 PROVENANCE: DE Allemagne; EUR 6 | Assets:Bank:Checking $50.00 7 | Expenses:Unknown 8 | -------------------------------------------------------------------------------- /spec/integration/invalid_header_example/test_args: -------------------------------------------------------------------------------- 1 | -f input.csv --unattended --account Assets:Bank:Checking --contains-header 4 --comma-separates-cents --verbose 2 | -------------------------------------------------------------------------------- /spec/integration/inversed_credit_card/input.csv: -------------------------------------------------------------------------------- 1 | 2013/01/17,2013/01/16,2013011702,DEBIT,2226,"VODAFONE PREPAY VISA M AUCKLAND NZL",30.00 2 | 2013/01/18,2013/01/17,2013011801,DEBIT,2226,"WILSON PARKING AUCKLAND NZL",4.60 3 | 2013/01/18,2013/01/17,2013011802,DEBIT,2226,"AUCKLAND TRANSPORT HENDERSON NZL",2.00 4 | 2013/01/19,2013/01/19,2013011901,CREDIT,2226,"INTERNET PAYMENT RECEIVED ",-500.00 5 | 2013/01/26,2013/01/23,2013012601,DEBIT,2226,"ITUNES NZ CORK IRL",64.99 6 | 2013/01/26,2013/01/25,2013012602,DEBIT,2226,"VODAFONE FXFLNE BBND R NEWTON NZL",90.26 7 | 2013/01/29,2013/01/29,2013012901,CREDIT,2101,"PAYMENT RECEIVED THANK YOU ",-27.75 8 | 2013/01/30,2013/01/29,2013013001,DEBIT,2226,"AUCKLAND TRANSPORT HENDERSON NZL",3.50 9 | 2013/02/05,2013/02/03,2013020501,DEBIT,2226,"Z BEACH RD AUCKLAND NZL",129.89 10 | 2013/02/05,2013/02/03,2013020502,DEBIT,2226,"TOURNAMENT KHYBER PASS AUCKLAND NZL",8.00 11 | 2013/02/05,2013/02/04,2013020503,DEBIT,2226,"VODAFONE PREPAY VISA M AUCKLAND NZL",30.00 12 | 2013/02/08,2013/02/07,2013020801,DEBIT,2226,"AKLD TRANSPORT PARKING AUCKLAND NZL",2.50 13 | 2013/02/08,2013/02/07,2013020802,DEBIT,2226,"AUCKLAND TRANSPORT HENDERSON NZL",3.50 14 | 2013/02/12,2013/02/11,2013021201,DEBIT,2226,"AKLD TRANSPORT PARKING AUCKLAND NZL",1.50 15 | 2013/02/17,2013/02/17,2013021701,CREDIT,2226,"INTERNET PAYMENT RECEIVED ",-12.00 16 | 2013/02/17,2013/02/17,2013021702,CREDIT,2226,"INTERNET PAYMENT RECEIVED ",-18.00 17 | -------------------------------------------------------------------------------- /spec/integration/inversed_credit_card/output.ledger: -------------------------------------------------------------------------------- 1 | 2013-01-17 2013/01/16; 2013011702; DEBIT; 2226; VODAFONE PREPAY VISA M AUCKLAND NZL 2 | Assets:Bank:Checking $30.00 3 | Income:Unknown 4 | 5 | 2013-01-18 2013/01/17; 2013011801; DEBIT; 2226; WILSON PARKING AUCKLAND NZL 6 | Assets:Bank:Checking $4.60 7 | Income:Unknown 8 | 9 | 2013-01-18 2013/01/17; 2013011802; DEBIT; 2226; AUCKLAND TRANSPORT HENDERSON NZL 10 | Assets:Bank:Checking $2.00 11 | Income:Unknown 12 | 13 | 2013-01-19 2013/01/19; 2013011901; CREDIT; 2226; INTERNET PAYMENT RECEIVED 14 | Income:Unknown 15 | Assets:Bank:Checking -$500.00 16 | 17 | 2013-01-26 2013/01/25; 2013012602; DEBIT; 2226; VODAFONE FXFLNE BBND R NEWTON NZL 18 | Assets:Bank:Checking $90.26 19 | Income:Unknown 20 | 21 | 2013-01-26 2013/01/23; 2013012601; DEBIT; 2226; ITUNES NZ CORK IRL 22 | Assets:Bank:Checking $64.99 23 | Income:Unknown 24 | 25 | 2013-01-29 2013/01/29; 2013012901; CREDIT; 2101; PAYMENT RECEIVED THANK YOU 26 | Income:Unknown 27 | Assets:Bank:Checking -$27.75 28 | 29 | 2013-01-30 2013/01/29; 2013013001; DEBIT; 2226; AUCKLAND TRANSPORT HENDERSON NZL 30 | Assets:Bank:Checking $3.50 31 | Income:Unknown 32 | 33 | 2013-02-05 2013/02/03; 2013020501; DEBIT; 2226; Z BEACH RD AUCKLAND NZL 34 | Assets:Bank:Checking $129.89 35 | Income:Unknown 36 | 37 | 2013-02-05 2013/02/04; 2013020503; DEBIT; 2226; VODAFONE PREPAY VISA M AUCKLAND NZL 38 | Assets:Bank:Checking $30.00 39 | Income:Unknown 40 | 41 | 2013-02-05 2013/02/03; 2013020502; DEBIT; 2226; TOURNAMENT KHYBER PASS AUCKLAND NZL 42 | Assets:Bank:Checking $8.00 43 | Income:Unknown 44 | 45 | 2013-02-08 2013/02/07; 2013020802; DEBIT; 2226; AUCKLAND TRANSPORT HENDERSON NZL 46 | Assets:Bank:Checking $3.50 47 | Income:Unknown 48 | 49 | 2013-02-08 2013/02/07; 2013020801; DEBIT; 2226; AKLD TRANSPORT PARKING AUCKLAND NZL 50 | Assets:Bank:Checking $2.50 51 | Income:Unknown 52 | 53 | 2013-02-12 2013/02/11; 2013021201; DEBIT; 2226; AKLD TRANSPORT PARKING AUCKLAND NZL 54 | Assets:Bank:Checking $1.50 55 | Income:Unknown 56 | 57 | 2013-02-17 2013/02/17; 2013021701; CREDIT; 2226; INTERNET PAYMENT RECEIVED 58 | Income:Unknown 59 | Assets:Bank:Checking -$12.00 60 | 61 | 2013-02-17 2013/02/17; 2013021702; CREDIT; 2226; INTERNET PAYMENT RECEIVED 62 | Income:Unknown 63 | Assets:Bank:Checking -$18.00 64 | 65 | -------------------------------------------------------------------------------- /spec/integration/inversed_credit_card/test_args: -------------------------------------------------------------------------------- 1 | -f input.csv --unattended --account Assets:Bank:Checking 2 | -------------------------------------------------------------------------------- /spec/integration/ledger_date_format/compare_cmds: -------------------------------------------------------------------------------- 1 | ledger --input-date-format '%d/%m/%Y' 2 | -------------------------------------------------------------------------------- /spec/integration/ledger_date_format/input.csv: -------------------------------------------------------------------------------- 1 | 02/12/2009,Check - 0000000122,122,-$76.00,"","$1,750.06" 2 | 02/12/2009,BLARG R SH 456930,"","",+$327.49,"$1,826.06" 3 | 02/12/2009,Check - 0000000112,112,-$800.00,"","$1,498.57" 4 | -------------------------------------------------------------------------------- /spec/integration/ledger_date_format/output.ledger: -------------------------------------------------------------------------------- 1 | 02/12/2009 BLARG R SH 456930; $1,826.06 2 | Assets:Bank:Checking $327.49 3 | Income:Unknown 4 | 5 | 02/12/2009 Check - 0000000122; 122; $1,750.06 6 | Income:Unknown 7 | Assets:Bank:Checking -$76.00 8 | 9 | 02/12/2009 Check - 0000000112; 112; $1,498.57 10 | Income:Unknown 11 | Assets:Bank:Checking -$800.00 12 | 13 | -------------------------------------------------------------------------------- /spec/integration/ledger_date_format/test_args: -------------------------------------------------------------------------------- 1 | -f input.csv --unattended --account Assets:Bank:Checking --date-format '%d/%m/%Y' --ledger-date-format '%d/%m/%Y' 2 | -------------------------------------------------------------------------------- /spec/integration/nationwide/input.csv: -------------------------------------------------------------------------------- 1 | 07 Nov 2013,Bank credit,Bank credit,,£500.00,£500.00 2 | 09 Oct 2013,ATM Withdrawal,Withdrawal,£20.00,,£480.00 3 | 09 Dec 2013,Visa,Supermarket,£19.77,,£460.23 4 | 10 Dec 2013,ATM Withdrawal 2,ATM Withdrawal 4,£100.00,,£360.23 5 | -------------------------------------------------------------------------------- /spec/integration/nationwide/output.ledger: -------------------------------------------------------------------------------- 1 | 2013-10-09 ATM Withdrawal; Withdrawal; £480.00 2 | Expenses:Unknown 3 | Assets:Bank:Checking -20.00 POUND 4 | 5 | 2013-11-07 Bank credit; Bank credit; £500.00 6 | Assets:Bank:Checking 500.00 POUND 7 | Income:Unknown 8 | 9 | 2013-12-09 Visa; Supermarket; £460.23 10 | Expenses:Unknown 11 | Assets:Bank:Checking -19.77 POUND 12 | 13 | 2013-12-10 ATM Withdrawal 2; ATM Withdrawal 4; £360.23 14 | Expenses:Unknown 15 | Assets:Bank:Checking -100.00 POUND 16 | 17 | -------------------------------------------------------------------------------- /spec/integration/nationwide/test_args: -------------------------------------------------------------------------------- 1 | -f input.csv --unattended --account Assets:Bank:Checking --csv-separator , --suffixed --currency POUND 2 | -------------------------------------------------------------------------------- /spec/integration/regression/issue_51_account_tokens/input.csv: -------------------------------------------------------------------------------- 1 | 01/09/2015,05354 SUBWAY,8.19,,1000.00 2 | 02/18/2015,WENDY'S #6338,8.55,,1000.00 3 | 02/25/2015,WENDY'S #6338,8.55,,1000.00 4 | 02/25/2015,WENDY'S #6338,9.14,,1000.00 5 | 02/27/2015,WENDY'S #6338,5.85,,1000.00 6 | 03/09/2015,WENDY'S #6338,17.70,,1000.00 7 | 03/16/2015,WENDY'S #6338,11.15,,1000.00 8 | 03/23/2015,WENDY'S,10.12,,1000.00 9 | -------------------------------------------------------------------------------- /spec/integration/regression/issue_51_account_tokens/output.ledger: -------------------------------------------------------------------------------- 1 | 2015-01-09 05354 SUBWAY 2 | Assets:Chequing $8.19 3 | Expenses:Dining:Resturant 4 | 5 | 2015-02-18 WENDY'S #6338 6 | Assets:Chequing $8.55 7 | Expenses:Dining:Resturant 8 | 9 | 2015-02-25 WENDY'S #6338 10 | Assets:Chequing $9.14 11 | Expenses:Dining:Resturant 12 | 13 | 2015-02-25 WENDY'S #6338 14 | Assets:Chequing $8.55 15 | Expenses:Dining:Resturant 16 | 17 | 2015-02-27 WENDY'S #6338 18 | Assets:Chequing $5.85 19 | Expenses:Dining:Resturant 20 | 21 | 2015-03-09 WENDY'S #6338 22 | Assets:Chequing $17.70 23 | Expenses:Dining:Resturant 24 | 25 | 2015-03-16 WENDY'S #6338 26 | Assets:Chequing $11.15 27 | Expenses:Dining:Resturant 28 | 29 | 2015-03-23 WENDY'S 30 | Assets:Chequing $10.12 31 | Expenses:Dining:Resturant 32 | 33 | -------------------------------------------------------------------------------- /spec/integration/regression/issue_51_account_tokens/test_args: -------------------------------------------------------------------------------- 1 | -f input.csv --unattended --account Assets:Chequing \ 2 | --ignore-columns 5 \ 3 | --account-tokens tokens.yml 4 | 5 | -------------------------------------------------------------------------------- /spec/integration/regression/issue_51_account_tokens/tokens.yml: -------------------------------------------------------------------------------- 1 | Expenses: 2 | Dining: 3 | Coffee: 4 | - 'STARBUCKS' 5 | - 'TIM HORTON' 6 | Resturant: 7 | - 'WENDY''S' 8 | - 'SUBWAY' 9 | - 'BARAKAT' 10 | -------------------------------------------------------------------------------- /spec/integration/regression/issue_64_date_column/input.csv: -------------------------------------------------------------------------------- 1 | "Date","Note","Amount" 2 | "2012/3/22","DEPOSIT","50.00" 3 | "2012/3/23","TRANSFER TO SAVINGS","-10.00" 4 | -------------------------------------------------------------------------------- /spec/integration/regression/issue_64_date_column/output.ledger: -------------------------------------------------------------------------------- 1 | 2012-03-22 DEPOSIT 2 | Assets:Bank:Checking $50.00 3 | Income:Unknown 4 | 5 | 2012-03-23 TRANSFER TO SAVINGS 6 | Expenses:Unknown 7 | Assets:Bank:Checking -$10.00 8 | 9 | -------------------------------------------------------------------------------- /spec/integration/regression/issue_64_date_column/test_args: -------------------------------------------------------------------------------- 1 | -f input.csv --unattended --account Assets:Bank:Checking 2 | -------------------------------------------------------------------------------- /spec/integration/regression/issue_73_account_token_matching/input.csv: -------------------------------------------------------------------------------- 1 | Transaction Date,Description,Amount,Category 2 | 07/06/2017,TRIPLE T CAR WASH CHAMPAIGN IL,$27.00,Automotive 3 | -------------------------------------------------------------------------------- /spec/integration/regression/issue_73_account_token_matching/output.ledger: -------------------------------------------------------------------------------- 1 | 2017-07-06 TRIPLE T CAR WASH CHAMPAIGN IL 2 | Liabilities:Credit Cards:Visa $27.00 3 | Expenses:Automotive:Car Wash 4 | 5 | -------------------------------------------------------------------------------- /spec/integration/regression/issue_73_account_token_matching/test_args: -------------------------------------------------------------------------------- 1 | -f input.csv --unattended --account 'Liabilities:Credit Cards:Visa' \ 2 | --contains-header 1 \ 3 | --ignore-columns 4 \ 4 | --date-format %m/%d/%Y \ 5 | --account-tokens tokens.yml 6 | 7 | -------------------------------------------------------------------------------- /spec/integration/regression/issue_73_account_token_matching/tokens.yml: -------------------------------------------------------------------------------- 1 | Expenses: 2 | Automotive: 3 | Car Wash: 4 | - 'TRIPLE T CAR WASH CHAMPAIGN IL' 5 | - "BIG T's CAR WASH" 6 | Maintenance: 7 | - 'Autozone' 8 | - "O'Reillys auto parts" 9 | -------------------------------------------------------------------------------- /spec/integration/regression/issue_85_date_example/input.csv: -------------------------------------------------------------------------------- 1 | Visa, 4514010000000000, 2020-02-20, , GOJEK SINGAPORE, 8.10 SGD @ .976500000000, -7.91, D 2 | Visa, 4514010000000000, 2020-02-20, , GOJEK SINGAPORE, 6.00 SGD @ .976600000000, -5.86, D 3 | -------------------------------------------------------------------------------- /spec/integration/regression/issue_85_date_example/output.ledger: -------------------------------------------------------------------------------- 1 | 2020-02-20 Visa; 4514010000000000; GOJEK SINGAPORE; 6.00 SGD @ .976600000000; D 2 | Expenses:Unknown 3 | Assets:Bank:Checking -$5.86 4 | 5 | 2020-02-20 Visa; 4514010000000000; GOJEK SINGAPORE; 8.10 SGD @ .976500000000; D 6 | Expenses:Unknown 7 | Assets:Bank:Checking -$7.91 8 | 9 | -------------------------------------------------------------------------------- /spec/integration/regression/issue_85_date_example/test_args: -------------------------------------------------------------------------------- 1 | -f input.csv --unattended --account Assets:Bank:Checking 2 | -------------------------------------------------------------------------------- /spec/integration/spanish_date_example/input.csv: -------------------------------------------------------------------------------- 1 | 02/12/2009,Check - 0000000122,122,-$76.00,"","$1,750.06" 2 | 02/12/2009,BLARG R SH 456930,"","",+$327.49,"$1,826.06" 3 | 02/12/2009,Check - 0000000112,112,-$800.00,"","$1,498.57" 4 | -------------------------------------------------------------------------------- /spec/integration/spanish_date_example/output.ledger: -------------------------------------------------------------------------------- 1 | 2009-12-02 BLARG R SH 456930; $1,826.06 2 | Assets:Bank:Checking $327.49 3 | Income:Unknown 4 | 5 | 2009-12-02 Check - 0000000122; 122; $1,750.06 6 | Income:Unknown 7 | Assets:Bank:Checking -$76.00 8 | 9 | 2009-12-02 Check - 0000000112; 112; $1,498.57 10 | Income:Unknown 11 | Assets:Bank:Checking -$800.00 12 | 13 | -------------------------------------------------------------------------------- /spec/integration/spanish_date_example/test_args: -------------------------------------------------------------------------------- 1 | -f input.csv --unattended --account Assets:Bank:Checking --date-format '%d/%m/%Y' 2 | -------------------------------------------------------------------------------- /spec/integration/suntrust/input.csv: -------------------------------------------------------------------------------- 1 | 11/01/2014,0, Deposit,0,500.00,500.00 2 | 11/02/2014,101,Check,100.00,0,400.00 3 | 11/03/2014,102,Check,100.00,0,300.00 4 | 11/04/2014,103,Check,100.00,0,200.00 5 | 11/05/2014,104,Check,100.00,0,100.00 6 | 11/06/2014,105,Check,100.00,0,0.00 7 | 11/17/2014,0, Deposit,0,700.00,700.00 8 | -------------------------------------------------------------------------------- /spec/integration/suntrust/output.ledger: -------------------------------------------------------------------------------- 1 | 2014-11-01 0; Deposit; 500.00 2 | Assets:Bank:Checking $500.00 3 | Income:Unknown 4 | 5 | 2014-11-02 101; Check; 400.00 6 | Expenses:Unknown 7 | Assets:Bank:Checking -$100.00 8 | 9 | 2014-11-03 102; Check; 300.00 10 | Expenses:Unknown 11 | Assets:Bank:Checking -$100.00 12 | 13 | 2014-11-04 103; Check; 200.00 14 | Expenses:Unknown 15 | Assets:Bank:Checking -$100.00 16 | 17 | 2014-11-05 104; Check; 100.00 18 | Expenses:Unknown 19 | Assets:Bank:Checking -$100.00 20 | 21 | 2014-11-06 105; Check; 0.00 22 | Expenses:Unknown 23 | Assets:Bank:Checking -$100.00 24 | 25 | 2014-11-17 0; Deposit; 700.00 26 | Assets:Bank:Checking $700.00 27 | Income:Unknown 28 | 29 | -------------------------------------------------------------------------------- /spec/integration/suntrust/test_args: -------------------------------------------------------------------------------- 1 | -f input.csv --unattended --account Assets:Bank:Checking 2 | -------------------------------------------------------------------------------- /spec/integration/tab_delimited_file/input.csv: -------------------------------------------------------------------------------- 1 | 123456789 EUR 20160102 15,00 10,00 20160102 -5,00 DESCRIPTION 2 | 123456789 EUR 20160102 10,00 0,00 20160102 -10,00 DESCRIPTION -------------------------------------------------------------------------------- /spec/integration/tab_delimited_file/output.ledger: -------------------------------------------------------------------------------- 1 | 2016-01-02 123456789; EUR; 20160102; DESCRIPTION 2 | Expenses:Unknown 3 | Test::Account -€5.00 4 | 5 | 2016-01-02 123456789; EUR; 20160102; DESCRIPTION 6 | Expenses:Unknown 7 | Test::Account -€10.00 8 | 9 | -------------------------------------------------------------------------------- /spec/integration/tab_delimited_file/test_args: -------------------------------------------------------------------------------- 1 | -f input.csv --unattended -c € --ignore-columns 4,5 --comma-separates-cents -v --date-format '%Y%m%d' --csv-separator '\t' -a Test::Account -------------------------------------------------------------------------------- /spec/integration/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -Euo pipefail 4 | 5 | 6 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 7 | TEST_DIFF="" 8 | OUTPUT="" 9 | RECKON_CMD="reckon -v" 10 | export RUBYLIB=$SCRIPT_DIR/../../lib:${RUBYLIB:-} 11 | export PATH="$SCRIPT_DIR/../../bin:$PATH" 12 | 13 | main () { 14 | trap test_fail EXIT 15 | 16 | if [[ $# -eq 1 ]]; then 17 | TESTS=$1/test_args 18 | else 19 | TESTS=$(find "$SCRIPT_DIR" -name 'test_args') 20 | fi 21 | 22 | echo > test.log 23 | 24 | NUM_TESTS=$(echo "$TESTS" |wc -l |awk '{print $1}') 25 | 26 | echo "1..$NUM_TESTS" 27 | 28 | I=1 29 | 30 | for t in $TESTS; do 31 | TEST_DIR=$(dirname "$t") 32 | TEST_LOG=$(mktemp) 33 | pushd "$TEST_DIR" >/dev/null || exit 1 34 | if [[ -e "cli_input.txt" ]]; then 35 | cli_test >$TEST_LOG 2>&1 36 | else 37 | unattended_test >$TEST_LOG 2>&1 38 | fi 39 | 40 | popd >/dev/null || exit 1 41 | # have to save output after popd 42 | echo -e "\n\n======>$TEST_DIR" >> test.log 43 | echo -e "TEST_CMD: $TEST_CMD" >> test.log 44 | cat $TEST_LOG >> test.log 45 | 46 | if [[ $ERROR -ne 0 ]]; then 47 | echo -e "not ok $I - $TEST_DIR" 48 | tail -n25 test.log 49 | exit 1 50 | else 51 | echo -e "ok $I - $TEST_DIR" 52 | fi 53 | I=$(($I + 1)) 54 | done 55 | } 56 | 57 | cli_test () { 58 | OUTPUT_FILE=$(mktemp) 59 | TEST_CMD="$RECKON_CMD --table-output-file $OUTPUT_FILE $(cat test_args)" 60 | cat cli_input.txt | $TEST_CMD 61 | TEST_DIFF=$(diff -u "$OUTPUT_FILE" expected_output) 62 | 63 | # ${#} is character length, test that there was no output from diff 64 | if [ ${#TEST_DIFF} -eq 0 ]; then 65 | ERROR=0 66 | else 67 | ERROR=1 68 | fi 69 | } 70 | 71 | unattended_test() { 72 | OUTPUT_FILE=$(mktemp) 73 | TEST_CMD="$RECKON_CMD -o $OUTPUT_FILE $(cat test_args)" 74 | eval "$TEST_CMD" 2>&1 75 | ERROR=0 76 | 77 | compare_output "$OUTPUT_FILE" 78 | } 79 | 80 | test_fail () { 81 | STATUS=$? 82 | if [[ $STATUS -ne 0 ]]; then 83 | echo -e "FAILED!!!\n$TEST_DIFF\nTest output saved to $OUTPUT_FILE\n" 84 | exit $STATUS 85 | fi 86 | } 87 | 88 | compare_output () { 89 | OUTPUT_FILE=$1 90 | pwd 91 | if [[ -e compare_cmds ]]; then 92 | COMPARE_CMDS=$(cat compare_cmds) 93 | else 94 | COMPARE_CMDS=$'ledger\nhledger' 95 | fi 96 | 97 | ERROR=1 98 | while IFS= read -r n; do 99 | if compare_output_for "$OUTPUT_FILE" "$n"; then 100 | ERROR=0 101 | else 102 | ERROR=1 103 | break 104 | fi 105 | done <<< "$COMPARE_CMDS" 106 | } 107 | 108 | compare_output_for () { 109 | OUTPUT_FILE=$1 110 | LEDGER=$2 111 | 112 | EXPECTED_FILE=$(mktemp) 113 | ACTUAL_FILE=$(mktemp) 114 | 115 | EXPECTED_CMD="$LEDGER -f output.ledger reg >$EXPECTED_FILE" 116 | echo "$EXPECTED_CMD" 117 | eval "$EXPECTED_CMD" || return 1 118 | 119 | ACTUAL_CMD="$LEDGER -f \"$OUTPUT_FILE\" reg" 120 | echo "running $ACTUAL_CMD" 121 | eval $ACTUAL_CMD >$ACTUAL_FILE || return 1 122 | 123 | TEST_DIFF=$(diff -u "$EXPECTED_FILE" "$ACTUAL_FILE") 124 | 125 | # ${#} is character length, test that there was no output from diff 126 | if [ ${#TEST_DIFF} -eq 0 ]; then 127 | return 0 128 | else 129 | return 1 130 | fi 131 | } 132 | 133 | main "$@" 134 | -------------------------------------------------------------------------------- /spec/integration/test_money_column/input.csv: -------------------------------------------------------------------------------- 1 | "Date","Note","Amount" 2 | "2012/3/22","DEPOSIT","50.00" 3 | "2012/3/23","TRANSFER TO SAVINGS","-10.00" 4 | -------------------------------------------------------------------------------- /spec/integration/test_money_column/output.ledger: -------------------------------------------------------------------------------- 1 | 2012-03-22 DEPOSIT 2 | Assets:Bank:Checking $50.00 3 | Income:Unknown 4 | 5 | 2012-03-23 TRANSFER TO SAVINGS 6 | Expenses:Unknown 7 | Assets:Bank:Checking -$10.00 8 | 9 | -------------------------------------------------------------------------------- /spec/integration/test_money_column/test_args: -------------------------------------------------------------------------------- 1 | -f input.csv --unattended --account Assets:Bank:Checking 2 | -------------------------------------------------------------------------------- /spec/integration/two_money_columns/input.csv: -------------------------------------------------------------------------------- 1 | 4/1/2008,Check - 0000000122,122,-$76.00,"","$1,750.06" 2 | 3/28/2008,BLARG R SH 456930,"","",+$327.49,"$1,826.06" 3 | 3/27/2008,Check - 0000000112,112,-$800.00,"","$1,498.57" 4 | 3/26/2008,Check - 0000000251,251,-$88.55,"","$1,298.57" 5 | 3/26/2008,Check - 0000000251,251,"","+$88.55","$1,298.57" 6 | -------------------------------------------------------------------------------- /spec/integration/two_money_columns/output.ledger: -------------------------------------------------------------------------------- 1 | 2008-03-26 Check - 0000000251; 251; $1,298.57 2 | Assets:Bank:Checking $88.55 3 | Income:Unknown 4 | 5 | 2008-03-26 Check - 0000000251; 251; $1,298.57 6 | Income:Unknown 7 | Assets:Bank:Checking -$88.55 8 | 9 | 2008-03-27 Check - 0000000112; 112; $1,498.57 10 | Income:Unknown 11 | Assets:Bank:Checking -$800.00 12 | 13 | 2008-03-28 BLARG R SH 456930; $1,826.06 14 | Assets:Bank:Checking $327.49 15 | Income:Unknown 16 | 17 | 2008-04-01 Check - 0000000122; 122; $1,750.06 18 | Income:Unknown 19 | Assets:Bank:Checking -$76.00 20 | 21 | -------------------------------------------------------------------------------- /spec/integration/two_money_columns/test_args: -------------------------------------------------------------------------------- 1 | -f input.csv --unattended --account Assets:Bank:Checking 2 | -------------------------------------------------------------------------------- /spec/integration/two_money_columns_manual/input.csv: -------------------------------------------------------------------------------- 1 | Date,Summary,Withdrawal,Deposit,Ending balance,Remarks,In/outward transfer account,Notes 2 | 2021/07/02,Expense,200,,999,,, 3 | 2021/07/02,Expense,100,,999,,, 4 | 2021/07/08,Transfer,,200,999,,,In1 5 | 2021/07/08,Transfer,500,,999,,,Out1 6 | -------------------------------------------------------------------------------- /spec/integration/two_money_columns_manual/output.ledger: -------------------------------------------------------------------------------- 1 | 2021-07-02 Expense; 999 2 | Expenses:Unknown 3 | Assets:Bank:Checking -$100.00 4 | 5 | 2021-07-02 Expense; 999 6 | Expenses:Unknown 7 | Assets:Bank:Checking -$200.00 8 | 9 | 2021-07-08 Transfer; 999; In1 10 | Assets:Bank:Checking $200.00 11 | Expenses:Unknown 12 | 13 | 2021-07-08 Transfer; 999; Out1 14 | Expenses:Unknown 15 | Assets:Bank:Checking -$500.00 16 | 17 | -------------------------------------------------------------------------------- /spec/integration/two_money_columns_manual/test_args: -------------------------------------------------------------------------------- 1 | -f input.csv --unattended --account Assets:Bank:Checking --money-columns 3,4 2 | -------------------------------------------------------------------------------- /spec/integration/unattended_config/input.csv: -------------------------------------------------------------------------------- 1 | DEBIT,20091224120000[0:GMT],"HOST 037196321563 MO 12/22SLICEHOST",-85.00 2 | CHECK,20091224120000[0:GMT],"Book Store",-20.00 3 | DEBIT,20091224120000[0:GMT],"GITHUB 041287430274 CA 12/22GITHUB 04",-7.00 4 | CREDIT,20091223120000[0:GMT],"Some Company vendorpymt PPD ID: 59728JSL20",3520.00 5 | CREDIT,20091223120000[0:GMT],"Blarg BLARG REVENUE PPD ID: 00jah78563",1558.52 6 | DEBIT,20091221120000[0:GMT],"WEBSITE-BALANCE-17DEC09 12 12/17WEBSITE-BAL",-12.23 7 | DEBIT,20091214120000[0:GMT],"WEBSITE-BALANCE-10DEC09 12 12/10WEBSITE-BAL",-20.96 8 | CREDIT,20091211120000[0:GMT],"PAYPAL TRANSFER PPD ID: PAYPALSDSL",-116.22 9 | CREDIT,20091210120000[0:GMT],"Some Company vendorpymt PPD ID: 5KL3832735",2105.00 10 | -------------------------------------------------------------------------------- /spec/integration/unattended_config/output.ledger: -------------------------------------------------------------------------------- 1 | 2009-12-10 CREDIT; Some Company vendorpymt PPD ID: 5KL3832735 2 | Assets:Bank:Checking $2,105.00 3 | Income:Unknown 4 | 5 | 2009-12-11 CREDIT; PAYPAL TRANSFER PPD ID: PAYPALSDSL 6 | Expenses:Unknown 7 | Assets:Bank:Checking -$116.22 8 | 9 | 2009-12-14 DEBIT; WEBSITE-BALANCE-10DEC09 12 12/10WEBSITE-BAL 10 | Expenses:Websites 11 | Assets:Bank:Checking -$20.96 12 | 13 | 2009-12-21 DEBIT; WEBSITE-BALANCE-17DEC09 12 12/17WEBSITE-BAL 14 | Expenses:Websites 15 | Assets:Bank:Checking -$12.23 16 | 17 | 2009-12-23 CREDIT; Some Company vendorpymt PPD ID: 59728JSL20 18 | Assets:Bank:Checking $3,520.00 19 | Income:Unknown 20 | 21 | 2009-12-23 CREDIT; Blarg BLARG REVENUE PPD ID: 00jah78563 22 | Assets:Bank:Checking $1,558.52 23 | Income:Unknown 24 | 25 | 2009-12-24 DEBIT; GITHUB 041287430274 CA 12/22GITHUB 04 26 | Expenses:Unknown 27 | Assets:Bank:Checking -$7.00 28 | 29 | 2009-12-24 CHECK; Book Store 30 | Expenses:Unknown 31 | Assets:Bank:Checking -$20.00 32 | 33 | 2009-12-24 DEBIT; HOST 037196321563 MO 12/22SLICEHOST 34 | Expenses:Unknown 35 | Assets:Bank:Checking -$85.00 36 | 37 | -------------------------------------------------------------------------------- /spec/integration/unattended_config/test_args: -------------------------------------------------------------------------------- 1 | -f input.csv --unattended --account 'Assets:Bank:Checking' \ 2 | --account-tokens tokens.yml 3 | -------------------------------------------------------------------------------- /spec/integration/unattended_config/tokens.yml: -------------------------------------------------------------------------------- 1 | config: 2 | similarity_threshold: 6 3 | 4 | Income: 5 | Salary: 6 | - 'LÖN' 7 | - 'Salary' 8 | Expenses: 9 | Bank: 10 | - 'Comission' 11 | - /mastercard/i 12 | Rent: 13 | - '0011223344' # Landlord bank number 14 | Websites: 15 | - /web/i 16 | Books: 17 | - 'Book' 18 | '[Internal:Transfer]': # Virtual account 19 | - '4433221100' # Your own account number 20 | -------------------------------------------------------------------------------- /spec/integration/yyyymmdd_date_example/input.csv: -------------------------------------------------------------------------------- 1 | DEBIT,20121231,"ODESK***BAL-27DEC12 650-12345 CA 12/28",-123.45 2 | -------------------------------------------------------------------------------- /spec/integration/yyyymmdd_date_example/output.ledger: -------------------------------------------------------------------------------- 1 | 2012-12-31 DEBIT; ODESK***BAL-27DEC12 650-12345 CA 12/28 2 | Expenses:Unknown 3 | Assets:Bank:Checking -$123.45 4 | 5 | -------------------------------------------------------------------------------- /spec/integration/yyyymmdd_date_example/test_args: -------------------------------------------------------------------------------- 1 | -f input.csv --unattended --account Assets:Bank:Checking 2 | -------------------------------------------------------------------------------- /spec/reckon/app_spec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "spec_helper" 4 | require 'rubygems' 5 | require 'reckon' 6 | 7 | describe Reckon::App do 8 | context 'with chase csv input' do 9 | before do 10 | @chase = Reckon::App.new(string: BANK_CSV) 11 | @chase.learn_from_ledger(BANK_LEDGER) 12 | @rows = [] 13 | @chase.each_row_backwards { |row| @rows.push(row) } 14 | end 15 | 16 | describe "each_row_backwards" do 17 | it "should return rows with hashes" do 18 | @rows[0][:pretty_date].should == "2009-12-10" 19 | @rows[0][:pretty_money].should == " $2,105.00" 20 | @rows[0][:description].should == "CREDIT; Some Company vendorpymt PPD ID: 5KL3832735" 21 | @rows[1][:pretty_date].should == "2009-12-11" 22 | @rows[1][:pretty_money].should == " $116.22" 23 | @rows[1][:description].should == "CREDIT; PAYPAL TRANSFER PPD ID: PAYPALSDSL" 24 | end 25 | end 26 | 27 | describe "weighted_account_match" do 28 | it "should guess the correct account" do 29 | row = @rows.find { |n| n[:description] =~ /Book Store/ } 30 | 31 | result = @chase.matcher.find_similar(row[:description]).first 32 | expect(result[:account]).to eq("Expenses:Books") 33 | expect(result[:similarity]).to be > 0.0 34 | end 35 | end 36 | end 37 | 38 | context 'unattended mode with chase csv input' do 39 | let(:output_file) { StringIO.new } 40 | let(:chase) do 41 | Reckon::App.new( 42 | string: BANK_CSV, 43 | unattended: true, 44 | output_file: output_file, 45 | bank_account: 'Assets:Bank:Checking', 46 | default_into_account: 'Expenses:Unknown', 47 | default_outof_account: 'Income:Unknown', 48 | ) 49 | end 50 | 51 | describe 'walk backwards' do 52 | it 'should assign Income:Unknown and Expenses:Unknown by default' do 53 | chase.walk_backwards 54 | expect(output_file.string.scan('Expenses:Unknown').count).to eq(5) 55 | expect(output_file.string.scan('Income:Unknown').count).to eq(4) 56 | end 57 | 58 | it 'should change default account names' do 59 | chase = Reckon::App.new( 60 | string: BANK_CSV, 61 | unattended: true, 62 | output_file: output_file, 63 | default_into_account: 'Expenses:Default', 64 | default_outof_account: 'Income:Default', 65 | bank_account: 'Assets:Bank:Checking', 66 | ) 67 | chase.walk_backwards 68 | expect(output_file.string.scan('Expenses:Default').count).to eq(5) 69 | expect(output_file.string.scan('Income:Default').count).to eq(4) 70 | end 71 | 72 | it 'should learn from a ledger file' do 73 | chase.learn_from_ledger(BANK_LEDGER) 74 | chase.walk_backwards 75 | output_file.string.scan('Expenses:Books').count.should == 1 76 | end 77 | 78 | it 'should learn from an account tokens file and parse regexps' do 79 | chase = Reckon::App.new( 80 | string: BANK_CSV, 81 | unattended: true, 82 | output_file: output_file, 83 | account_tokens_file: fixture_path('tokens.yaml'), 84 | bank_account: 'Assets:Bank:Checking', 85 | ) 86 | chase.walk_backwards 87 | expect(output_file.string.scan('Expenses:Books').count).to eq(1) 88 | expect(output_file.string.scan('Expenses:Websites').count).to eq(2) 89 | end 90 | end 91 | 92 | it 'should fail-on-unknown-account' do 93 | chase = Reckon::App.new( 94 | string: BANK_CSV, 95 | unattended: true, 96 | output_file: output_file, 97 | bank_account: 'Assets:Bank:Checking', 98 | default_into_account: 'Expenses:Unknown', 99 | default_outof_account: 'Income:Unknown', 100 | fail_on_unknown_account: true 101 | ) 102 | 103 | expect { chase.walk_backwards }.to( 104 | raise_error(RuntimeError, /Couldn't find any matches/) 105 | ) 106 | end 107 | end 108 | 109 | context "Issue #73 - regression test" do 110 | it "should categorize transaction correctly" do 111 | output = StringIO.new 112 | app = Reckon::App.new( 113 | file: fixture_path('73-sample.csv'), 114 | unattended: true, 115 | account_tokens_file: fixture_path('73-tokens.yml'), 116 | bank_account: "Liabilities:Credit Cards:Visa", 117 | contains_header: 1, 118 | ignore_column: [4], 119 | date_format: '%m/%d/%Y', 120 | output_file: output 121 | ) 122 | app.walk_backwards 123 | 124 | expect(output.string).to include('Expenses:Automotive:Car Wash') 125 | end 126 | end 127 | 128 | context "Issue #64 - regression test" do 129 | it 'should work for simple file' do 130 | rows = [] 131 | app = Reckon::App.new(file: fixture_path('test_money_column.csv')) 132 | expect { app.each_row_backwards { |n| rows << n } } 133 | .to output(/Skipping row: 'Date, Note, Amount'/).to_stderr_from_any_process 134 | expect(rows.length).to eq(2) 135 | expect(rows[0][:pretty_date]).to eq('2012-03-22') 136 | expect(rows[0][:pretty_money]).to eq(' $50.00') 137 | expect(rows[1][:pretty_date]).to eq('2012-03-23') 138 | expect(rows[1][:pretty_money]).to eq('-$10.00') 139 | end 140 | end 141 | 142 | context 'Issue #51 - regression test' do 143 | it 'should assign correct accounts with tokens' do 144 | output = StringIO.new 145 | Reckon::App.new( 146 | file: fixture_path('51-sample.csv'), 147 | unattended: true, 148 | account_tokens_file: fixture_path('51-tokens.yml'), 149 | ignore_columns: [5], 150 | bank_account: 'Assets:Chequing', 151 | output_file: output 152 | ).walk_backwards 153 | expect(output.string).not_to include('Income:Unknown') 154 | expect(output.string.scan('Expenses:Dining:Resturant').size).to eq(8) 155 | end 156 | end 157 | 158 | #DATA 159 | BANK_CSV = (<<-CSV).strip 160 | DEBIT,20091224120000[0:GMT],"HOST 037196321563 MO 12/22SLICEHOST",-85.00 161 | CHECK,20091224120000[0:GMT],"Book Store",-20.00 162 | DEBIT,20091224120000[0:GMT],"GITHUB 041287430274 CA 12/22GITHUB 04",-7.00 163 | CREDIT,20091223120000[0:GMT],"Some Company vendorpymt PPD ID: 59728JSL20",3520.00 164 | CREDIT,20091223120000[0:GMT],"Blarg BLARG REVENUE PPD ID: 00jah78563",1558.52 165 | DEBIT,20091221120000[0:GMT],"WEBSITE-BALANCE-17DEC09 12 12/17WEBSITE-BAL",-12.23 166 | DEBIT,20091214120000[0:GMT],"WEBSITE-BALANCE-10DEC09 12 12/10WEBSITE-BAL",-20.96 167 | CREDIT,20091211120000[0:GMT],"PAYPAL TRANSFER PPD ID: PAYPALSDSL",116.22 168 | CREDIT,20091210120000[0:GMT],"Some Company vendorpymt PPD ID: 5KL3832735",2105.00 169 | CSV 170 | 171 | BANK_LEDGER = (<<-LEDGER).strip 172 | 2004/05/14 * Pay day 173 | Assets:Bank:Checking $500.00 174 | Income:Salary 175 | 176 | 2004/05/27 Book Store 177 | Expenses:Books $20.00 178 | Liabilities:MasterCard 179 | LEDGER 180 | end 181 | -------------------------------------------------------------------------------- /spec/reckon/csv_parser_spec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require_relative "../spec_helper" 4 | require 'rubygems' 5 | require_relative '../../lib/reckon' 6 | 7 | describe Reckon::CSVParser do 8 | let(:chase) { Reckon::CSVParser.new(file: fixture_path('chase.csv')) } 9 | let(:some_other_bank) { Reckon::CSVParser.new(file: fixture_path('some_other.csv')) } 10 | let(:two_money_columns) { 11 | Reckon::CSVParser.new(file: fixture_path('two_money_columns.csv')) 12 | } 13 | let(:suntrust_csv) { Reckon::CSVParser.new(file: fixture_path('suntrust.csv')) } 14 | let(:simple_csv) { Reckon::CSVParser.new(file: fixture_path('simple.csv')) } 15 | let(:nationwide) { 16 | Reckon::CSVParser.new(file: fixture_path('nationwide.csv'), csv_separator: ',', 17 | suffixed: true, currency: "POUND") 18 | } 19 | let(:german_date) { 20 | Reckon::CSVParser.new(file: fixture_path('german_date_example.csv')) 21 | } 22 | let(:danish_kroner_nordea) { 23 | Reckon::CSVParser.new(file: fixture_path('danish_kroner_nordea_example.csv'), 24 | csv_separator: ';', comma_separates_cents: true) 25 | } 26 | let(:yyyymmdd_date) { 27 | Reckon::CSVParser.new(file: fixture_path('yyyymmdd_date_example.csv')) 28 | } 29 | let(:spanish_date) { 30 | Reckon::CSVParser.new(file: fixture_path('spanish_date_example.csv'), 31 | date_format: '%d/%m/%Y') 32 | } 33 | let(:english_date) { 34 | Reckon::CSVParser.new(file: fixture_path('english_date_example.csv')) 35 | } 36 | let(:ing_csv) { 37 | Reckon::CSVParser.new(file: fixture_path('ing.csv'), comma_separates_cents: true) 38 | } 39 | let(:austrian_csv) { 40 | Reckon::CSVParser.new(file: fixture_path('austrian_example.csv'), 41 | comma_separates_cents: true, csv_separator: ';') 42 | } 43 | let(:french_csv) { 44 | Reckon::CSVParser.new(file: fixture_path('french_example.csv'), csv_separator: ';', 45 | comma_separates_cents: true) 46 | } 47 | let(:broker_canada) { 48 | Reckon::CSVParser.new(file: fixture_path('broker_canada_example.csv')) 49 | } 50 | let(:intuit_mint) { 51 | Reckon::CSVParser.new(file: fixture_path('intuit_mint_example.csv')) 52 | } 53 | 54 | describe "parse" do 55 | it "should use binary encoding if none specified and chardet fails" do 56 | allow(CharDet).to receive(:detect).and_return({ 'encoding' => nil }) 57 | app = Reckon::CSVParser.new(file: fixture_path("extratofake.csv")) 58 | expect(app.send(:try_encoding, "foobarbaz")).to eq("BINARY") 59 | end 60 | 61 | it "should work with foreign character encodings" do 62 | app = Reckon::CSVParser.new(file: fixture_path("extratofake.csv")) 63 | app.columns[0][0..2].should == ["Data", "10/31/2012", "11/01/2012"] 64 | app.columns[2].first.should == "Histórico" 65 | end 66 | 67 | it "should work with other separators" do 68 | Reckon::CSVParser.new(:string => "one;two\nthree;four", 69 | :csv_separator => ';').columns.should == [ 70 | ['one', 'three'], ['two', 'four'] 71 | ] 72 | end 73 | 74 | it 'should parse quoted lines' do 75 | file = %q("30.03.2015";"29.03.2015";"09.04.2015";"BARAUSZAHLUNGSENTGELT";"5266 xxxx xxxx 9454";"";"0";"EUR";"0,00";"EUR";"-3,50";"0") 76 | Reckon::CSVParser.new(string: file, csv_separator: ';', 77 | comma_separates_cents: true).columns.length.should == 12 78 | end 79 | 80 | it 'should parse csv with BOM' do 81 | file = File.expand_path(fixture_path("bom_utf8_file.csv")) 82 | Reckon::CSVParser.new(file: file).columns.length.should == 41 83 | end 84 | 85 | it 'should parse multi-line csv fields' do 86 | file = File.expand_path(fixture_path("multi-line-field.csv")) 87 | p = Reckon::CSVParser.new(file: file) 88 | expect(p.columns[0].length).to eq 2 89 | expected_field = "In case of errors or questions about your\n" + 90 | " electronic transfers:\n" + 91 | " This is a multi-line string\n" + 92 | " " 93 | expect(p.columns[-1][-1]).to eq expected_field 94 | end 95 | 96 | describe 'file with invalid csv in header' do 97 | let(:invalid_file) { fixture_path('invalid_header_example.csv') } 98 | 99 | it 'should ignore invalid header lines' do 100 | parser = Reckon::CSVParser.new(file: invalid_file, contains_header: 4) 101 | expect(parser.csv_data).to eq([ 102 | ["19/02/2016", "VIR RECU 508160", 103 | "VIR RECU 1234567834S DE: Francois REF: 123457891234567894561231 PROVENANCE: DE Allemagne ", "50,00", "EUR"], ["18/02/2016", "COTISATION JAZZ", "COTISATION JAZZ ", "-8,10", "EUR"] 104 | ]) 105 | end 106 | 107 | it 'should fail' do 108 | expect { Reckon::CSVParser.new(file: invalid_file, contains_header: 1) }.to( 109 | raise_error(CSV::MalformedCSVError) 110 | ) 111 | end 112 | end 113 | end 114 | 115 | describe "columns" do 116 | it "should return the csv transposed" do 117 | simple_csv.columns.should == [["entry1", "entry4"], ["entry2", "entry5"], 118 | ["entry3", "entry6"]] 119 | chase.columns.length.should == 4 120 | end 121 | 122 | it "should be ok with empty lines" do 123 | lambda { 124 | Reckon::CSVParser.new(:string => "one,two\nthree,four\n\n\n\n\n").columns.should == [ 125 | ['one', 'three'], ['two', 'four'] 126 | ] 127 | }.should_not raise_error 128 | end 129 | end 130 | 131 | describe "detect_columns" do 132 | let(:harder_date_example_csv) { 133 | Reckon::CSVParser.new(file: fixture_path('harder_date_example.csv')) 134 | } 135 | 136 | it "should detect the money column" do 137 | chase.money_column_indices.should == [3] 138 | some_other_bank.money_column_indices.should == [3] 139 | two_money_columns.money_column_indices.should == [3, 4] 140 | suntrust_csv.money_column_indices.should == [3, 4] 141 | nationwide.money_column_indices.should == [3, 4] 142 | harder_date_example_csv.money_column_indices.should == [1] 143 | danish_kroner_nordea.money_column_indices.should == [3] 144 | yyyymmdd_date.money_column_indices.should == [3] 145 | ing_csv.money_column_indices.should == [6] 146 | austrian_csv.money_column_indices.should == [4] 147 | french_csv.money_column_indices.should == [4] 148 | broker_canada.money_column_indices.should == [8] 149 | intuit_mint.money_column_indices.should == [3] 150 | end 151 | 152 | it "should detect the date column" do 153 | chase.date_column_index.should == 1 154 | some_other_bank.date_column_index.should == 1 155 | two_money_columns.date_column_index.should == 0 156 | harder_date_example_csv.date_column_index.should == 0 157 | danish_kroner_nordea.date_column_index.should == 0 158 | yyyymmdd_date.date_column_index.should == 1 159 | french_csv.date_column_index.should == 1 160 | broker_canada.date_column_index.should == 0 161 | intuit_mint.date_column_index.should == 0 162 | Reckon::CSVParser.new(:string => '2014-01-13,"22211100000",-10').date_column_index.should == 0 163 | end 164 | 165 | it "should consider all other columns to be description columns" do 166 | chase.description_column_indices.should == [0, 2] 167 | some_other_bank.description_column_indices.should == [0, 2] 168 | two_money_columns.description_column_indices.should == [1, 2, 5] 169 | harder_date_example_csv.description_column_indices.should == [2, 3, 4, 5, 6, 7] 170 | danish_kroner_nordea.description_column_indices.should == [1, 2, 4] 171 | yyyymmdd_date.description_column_indices.should == [0, 2] 172 | end 173 | end 174 | 175 | describe "money_column_indicies" do 176 | it "should prefer the option over the heuristic" do 177 | chase = Reckon::CSVParser.new(file: fixture_path('chase.csv')) 178 | expect(chase.money_column_indices).to eq([3]) 179 | 180 | chase = Reckon::CSVParser.new(file: fixture_path('chase.csv'), money_column: 2) 181 | expect(chase.money_column_indices).to eq([1]) 182 | end 183 | end 184 | 185 | describe "money_for" do 186 | it "should return the appropriate fields" do 187 | chase.money_for(1).should == -20 188 | chase.money_for(4).should == 1558.52 189 | chase.money_for(7).should == -116.22 190 | some_other_bank.money_for(1).should == -20 191 | some_other_bank.money_for(4).should == 1558.52 192 | some_other_bank.money_for(7).should == -116.22 193 | two_money_columns.money_for(0).should == -76 194 | two_money_columns.money_for(1).should == 327.49 195 | two_money_columns.money_for(2).should == -800 196 | two_money_columns.money_for(3).should == -88.55 197 | two_money_columns.money_for(4).should == 88.55 198 | nationwide.money_for(0).should == 500.00 199 | nationwide.money_for(1).should == -20.00 200 | danish_kroner_nordea.money_for(0).should == -48.00 201 | danish_kroner_nordea.money_for(1).should == -79.00 202 | danish_kroner_nordea.money_for(2).should == 497.90 203 | danish_kroner_nordea.money_for(3).should == -995.00 204 | danish_kroner_nordea.money_for(4).should == -3452.90 205 | danish_kroner_nordea.money_for(5).should == -655.00 206 | yyyymmdd_date.money_for(0).should == -123.45 207 | ing_csv.money_for(0).should == -136.13 208 | ing_csv.money_for(1).should == 375.00 209 | austrian_csv.money_for(0).should == -18.00 210 | austrian_csv.money_for(2).should == 120.00 211 | french_csv.money_for(0).should == -10.00 212 | french_csv.money_for(1).should == -5.76 213 | broker_canada.money_for(0).should == 12.55 214 | broker_canada.money_for(1).should == -81.57 215 | intuit_mint.money_for(0).should == 0.01 216 | intuit_mint.money_for(1).should == -331.63 217 | end 218 | 219 | it "should handle the comma_separates_cents option correctly" do 220 | european_csv = Reckon::CSVParser.new( 221 | :string => "$2,00;something\n1.025,67;something else", :csv_separator => ';', :comma_separates_cents => true 222 | ) 223 | european_csv.money_for(0).should == 2.00 224 | european_csv.money_for(1).should == 1025.67 225 | end 226 | 227 | it "should return negated values if the inverse option is passed" do 228 | inversed_csv = Reckon::CSVParser.new( 229 | file: fixture_path('inversed_credit_card.csv'), inverse: true 230 | ) 231 | inversed_csv.money_for(0).should == -30.00 232 | inversed_csv.money_for(3).should == 500.00 233 | end 234 | end 235 | 236 | describe "date_column_index" do 237 | it "should prefer the option over the heuristic" do 238 | chase = Reckon::CSVParser.new(file: fixture_path('chase.csv')) 239 | expect(chase.date_column_index).to eq(1) 240 | 241 | chase = Reckon::CSVParser.new(file: fixture_path('chase.csv'), date_column: 3) 242 | expect(chase.date_column_index).to eq(2) 243 | end 244 | end 245 | 246 | describe "date_for" do 247 | it "should return a parsed date object" do 248 | chase.date_for(1).year.should == Time.parse("2009/12/24").year 249 | chase.date_for(1).month.should == Time.parse("2009/12/24").month 250 | chase.date_for(1).day.should == Time.parse("2009/12/24").day 251 | some_other_bank.date_for(1).year.should == Time.parse("2010/12/24").year 252 | some_other_bank.date_for(1).month.should == Time.parse("2010/12/24").month 253 | some_other_bank.date_for(1).day.should == Time.parse("2010/12/24").day 254 | german_date.date_for(1).year.should == Time.parse("2009/12/24").year 255 | german_date.date_for(1).month.should == Time.parse("2009/12/24").month 256 | german_date.date_for(1).day.should == Time.parse("2009/12/24").day 257 | danish_kroner_nordea.date_for(0).year.should == Time.parse("2012/11/16").year 258 | danish_kroner_nordea.date_for(0).month.should == Time.parse("2012/11/16").month 259 | danish_kroner_nordea.date_for(0).day.should == Time.parse("2012/11/16").day 260 | yyyymmdd_date.date_for(0).year.should == Time.parse("2012/12/31").year 261 | yyyymmdd_date.date_for(0).month.should == Time.parse("2012/12/31").month 262 | yyyymmdd_date.date_for(0).day.should == Time.parse("2012/12/31").day 263 | spanish_date.date_for(1).year.should == Time.parse("2009/12/02").year 264 | spanish_date.date_for(1).month.should == Time.parse("2009/12/02").month 265 | spanish_date.date_for(1).day.should == Time.parse("2009/12/02").day 266 | english_date.date_for(1).year.should == Time.parse("2009/12/24").year 267 | english_date.date_for(1).month.should == Time.parse("2009/12/24").month 268 | english_date.date_for(1).day.should == Time.parse("2009/12/24").day 269 | nationwide.date_for(1).month.should == 10 270 | ing_csv.date_for(1).month.should == Time.parse("2012/11/12").month 271 | ing_csv.date_for(1).day.should == Time.parse("2012/11/12").day 272 | broker_canada.date_for(5).year.should == 2014 273 | broker_canada.date_for(5).month.should == 1 274 | broker_canada.date_for(5).day.should == 7 275 | intuit_mint.date_for(1).year.should == 2014 276 | intuit_mint.date_for(1).month.should == 2 277 | intuit_mint.date_for(1).day.should == 3 278 | end 279 | end 280 | 281 | describe "description_for" do 282 | it "should return the combined fields that are not money for date fields" do 283 | chase.description_for(1).should == "CHECK; CHECK 2656" 284 | chase.description_for(7).should == "CREDIT; PAYPAL TRANSFER PPD ID: PAYPALSDSL" 285 | end 286 | 287 | it "should not append empty description column" do 288 | parser = Reckon::CSVParser.new(:string => '01/09/2015,05354 SUBWAY,8.19,,', 289 | :date_format => '%d/%m/%Y') 290 | parser.description_for(0).should == '05354 SUBWAY' 291 | end 292 | 293 | it "should handle nil description" do 294 | parser = Reckon::CSVParser.new(string: '2015-09-01,test,3.99') 295 | expect(parser.description_for(1)).to eq("") 296 | end 297 | end 298 | 299 | describe "pretty_money_for" do 300 | it "work with negative and positive numbers" do 301 | some_other_bank.pretty_money_for(1).should == "-$20.00" 302 | some_other_bank.pretty_money_for(4).should == " $1,558.52" 303 | some_other_bank.pretty_money_for(7).should == "-$116.22" 304 | some_other_bank.pretty_money_for(5).should == " $0.23" 305 | some_other_bank.pretty_money_for(6).should == "-$0.96" 306 | end 307 | 308 | it "work with other currencies such as €" do 309 | euro_bank = Reckon::CSVParser.new(file: fixture_path('some_other.csv'), 310 | currency: "€", suffixed: false) 311 | euro_bank.pretty_money_for(1).should == "-€20.00" 312 | euro_bank.pretty_money_for(4).should == " €1,558.52" 313 | euro_bank.pretty_money_for(7).should == "-€116.22" 314 | euro_bank.pretty_money_for(5).should == " €0.23" 315 | euro_bank.pretty_money_for(6).should == "-€0.96" 316 | end 317 | 318 | it "work with suffixed currencies such as SEK" do 319 | swedish_bank = Reckon::CSVParser.new(file: fixture_path('some_other.csv'), 320 | currency: 'SEK', suffixed: true) 321 | swedish_bank.pretty_money_for(1).should == "-20.00 SEK" 322 | swedish_bank.pretty_money_for(4).should == " 1,558.52 SEK" 323 | swedish_bank.pretty_money_for(7).should == "-116.22 SEK" 324 | swedish_bank.pretty_money_for(5).should == " 0.23 SEK" 325 | swedish_bank.pretty_money_for(6).should == "-0.96 SEK" 326 | end 327 | 328 | it "should work with merge columns" do 329 | nationwide.pretty_money_for(0).should == " 500.00 POUND" 330 | nationwide.pretty_money_for(1).should == "-20.00 POUND" 331 | end 332 | end 333 | 334 | describe '85 regression test' do 335 | it 'should detect correct date column' do 336 | p = Reckon::CSVParser.new(file: fixture_path('85-date-example.csv')) 337 | expect(p.date_column_index).to eq(2) 338 | end 339 | end 340 | end 341 | -------------------------------------------------------------------------------- /spec/reckon/date_column_spec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "spec_helper" 5 | require "rubygems" 6 | require "reckon" 7 | 8 | # datecolumn specs 9 | module Reckon 10 | describe DateColumn do 11 | describe "#initialize" do 12 | it "should detect us and world time" do 13 | expect(DateColumn.new(%w[01/02/2013 01/14/2013]).endian_precedence) 14 | .to eq [:middle] 15 | expect(DateColumn.new(%w[01/02/2013 14/01/2013]).endian_precedence) 16 | .to eq [:little] 17 | end 18 | it "should set endian_precedence to default when date format cannot be misinterpreted" do 19 | expect(DateColumn.new(["2013/01/02"]).endian_precedence) 20 | .to eq %i[middle little] 21 | end 22 | it "should raise an error when in doubt" do 23 | expect { DateColumn.new(["01/02/2013", "01/03/2013"]) } 24 | .to raise_error(StandardError) 25 | end 26 | end 27 | 28 | describe "#for" do 29 | it "should detect the date" do 30 | expect(DateColumn.new(%w[13/12/2013]).for(0)).to eq(Date.new(2013, 12, 13)) 31 | expect(DateColumn.new(%w[01/14/2013]).for(0)).to eq(Date.new(2013, 1, 14)) 32 | expect(DateColumn.new(%w[13/12/2013 21/11/2013]).for(1)) 33 | .to eq(Date.new(2013, 11, 21)) 34 | expect(DateColumn.new(["2013-11-21"]).for(0)).to eq(Date.new(2013, 11, 21)) 35 | end 36 | 37 | it "should correctly use endian_precedence" do 38 | expect(DateColumn.new(%w[01/02/2013 01/14/2013]).for(0)) 39 | .to eq(Date.new(2013, 1, 2)) 40 | expect(DateColumn.new(%w[01/02/2013 14/01/2013]).for(0)) 41 | .to eq(Date.new(2013, 2, 1)) 42 | end 43 | end 44 | 45 | describe "#pretty_for" do 46 | it "should use ledger_date_format" do 47 | expect( 48 | DateColumn.new(["13/02/2013"], 49 | { ledger_date_format: "%d/%m/%Y" }).pretty_for(0) 50 | ) 51 | .to eq("13/02/2013") 52 | end 53 | 54 | it "should default to is" do 55 | expect(DateColumn.new(["13/12/2013"]).pretty_for(0)) 56 | .to eq("2013-12-13") 57 | end 58 | end 59 | 60 | describe "#likelihood" do 61 | it "should prefer numbers that looks like dates" do 62 | expect(DateColumn.likelihood("123456789")) 63 | .to be < DateColumn.likelihood("20160102") 64 | end 65 | 66 | # See https://github.com/cantino/reckon/issues/126 67 | it "Issue #126 - it shouldn't fail on invalid dates" do 68 | expect(DateColumn.likelihood("303909302970-07-2023")).to be > 0 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /spec/reckon/ledger_parser_spec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # encoding: utf-8 3 | 4 | require_relative "../spec_helper" 5 | require 'rubygems' 6 | require 'reckon' 7 | require 'pp' 8 | require 'rantly' 9 | require 'rantly/rspec_extensions' 10 | require 'shellwords' 11 | require 'stringio' 12 | 13 | describe Reckon::LedgerParser do 14 | before do 15 | @ledger = Reckon::LedgerParser.new(date_format: '%Y/%m/%d') 16 | @entries = @ledger.parse(StringIO.new(EXAMPLE_LEDGER)) 17 | end 18 | 19 | describe "parse" do 20 | it "should match ledger csv output" do 21 | # ledger only parses dates with - or / as separator, and separator is required 22 | formats = ["%Y/%m/%d", "%Y-%m-%d"] 23 | types = [' ! ', ' * ', ' '] 24 | delimiters = [" ", "\t", "\t\t"] 25 | comment_chars = ';#%*|' 26 | currency_delimiters = delimiters + [''] 27 | currencies = ['', '$', '£'] 28 | property_of do 29 | Rantly do 30 | description = Proc.new do 31 | sized(15) { 32 | string 33 | }.tr(%q{'`:*\\}, '').gsub(/\s+/, ' ').gsub(/^[!;<\[( #{comment_chars}]+/, '') 34 | end 35 | currency = choose(*currencies) # to be consistent within the transaction 36 | single_line_comments = ";#|%*".split('').map { |n| 37 | "#{n} #{call(description)}" 38 | } 39 | comments = ['', '; ', "\t;#{call(description)}", " ; #{call(description)}"] 40 | date = Time.at(range(0, 1_581_389_644)).strftime(choose(*formats)) 41 | codes = [' ', " (#{string(:alnum).tr('()', '')}) "] 42 | account = Proc.new { choose(*delimiters) + call(description) } 43 | account_money = Proc.new do 44 | sprintf("%.02f", (float * range(5, 10) + 1) * choose(1, -1)) 45 | end 46 | account_line = Proc.new do 47 | call(account) + \ 48 | choose(*delimiters) + \ 49 | currency + \ 50 | choose(*currency_delimiters) + \ 51 | call(account_money) + \ 52 | choose(*comments) 53 | end 54 | ledger = "#{date}#{choose(*types)}#{choose(*codes)}#{call(description)}\n" 55 | range(1, 5).times do 56 | ledger += "#{call(account_line)}\n" 57 | end 58 | ledger += "#{call(account)}\n" 59 | ledger += choose(*single_line_comments) + "\n" 60 | ledger 61 | end 62 | end.check(100) do |s| 63 | filter_format = lambda { |n| 64 | [n['date'], n['desc'], n['name'], 65 | sprintf("%.02f", n['amount'])] 66 | } 67 | headers = %w[date code desc name currency amount type commend] 68 | safe_s = Shellwords.escape(s) 69 | 70 | lp_csv = Reckon::LedgerParser.new(date_format: '%Y/%m/%d').to_csv(StringIO.new(s)).join("\n") 71 | actual = CSV.parse(lp_csv, headers: headers).map(&filter_format) 72 | 73 | ledger_csv = `echo #{safe_s} | ledger csv --date-format '%Y/%m/%d' -f - ` 74 | expected = CSV.parse(ledger_csv.gsub('\"', '""'), 75 | headers: headers).map(&filter_format) 76 | expected.length.times do |i| 77 | expect(actual[i]).to eq(expected[i]) 78 | end 79 | end 80 | end 81 | 82 | it 'should filter block comments' do 83 | ledger = <<~HERE 84 | 1970/11/01 Dinner should show up 85 | Assets:Checking -123.00 86 | Expenses:Restaurants 87 | 88 | comment 89 | 90 | 1970/11/01 Lunch should NOT show up 91 | Assets:Checking -12.00 92 | Expenses:Restaurants 93 | 94 | end comment 95 | HERE 96 | entries = Reckon::LedgerParser.new.parse(StringIO.new(ledger)) 97 | expect(entries.length).to eq(1) 98 | expect(entries.first[:desc]).to eq('Dinner should show up') 99 | end 100 | 101 | it 'should transaction comments' do 102 | ledger = <<~HERE 103 | 2020-03-27 AMZN Mktp USX999H3203; Shopping; Sale 104 | Expenses:Household $82.77 105 | Liabilities:ChaseSapphire -$81.77 106 | # END FINANCE SCRIPT OUTPUT Thu 02 Apr 2020 12:05:54 PM EDT 107 | HERE 108 | entries = Reckon::LedgerParser.new.parse(StringIO.new(ledger)) 109 | expect(entries.first[:accounts].map { |n| 110 | n[:name] 111 | }).to eq(['Expenses:Household', 'Liabilities:ChaseSapphire']) 112 | expect(entries.first[:accounts].size).to eq(2) 113 | expect(entries.length).to eq(1) 114 | end 115 | 116 | it "should ignore non-standard entries" do 117 | @entries.length.should == 7 118 | end 119 | 120 | it "should parse entries correctly" do 121 | @entries.first[:desc].should == "Checking balance" 122 | @entries.first[:date].should == Date.parse("2004-05-01") 123 | @entries.first[:accounts].first[:name].should == "Assets:Bank:Checking" 124 | @entries.first[:accounts].first[:amount].should == 1000 125 | @entries.first[:accounts].last[:name].should == "Equity:Opening Balances" 126 | @entries.first[:accounts].last[:amount].should == -1000 127 | 128 | @entries.last[:desc].should == "Credit card company" 129 | @entries.last[:date].should == Date.parse("2004/05/27") 130 | @entries.last[:accounts].first[:name].should == "Liabilities:MasterCard" 131 | @entries.last[:accounts].first[:amount].should == 20.24 132 | @entries.last[:accounts].last[:name].should == "Assets:Bank:Checking" 133 | @entries.last[:accounts].last[:amount].should == -20.24 134 | end 135 | 136 | it "should parse dot-separated dates" do 137 | ledger = <<~HERE 138 | 2024.03.12 groceries; 11223344556; 32095205940 139 | assets:bank:spending 530.00 NOK 140 | assets:bank:co:groceries 141 | 142 | 2024.03.13 autosave; 11223344555; 11223344556 143 | assets:bank:savings 144 | assets:bank:spending -10.00 NOK 145 | HERE 146 | options = { ledger_date_format: '%Y.%m.%d' } 147 | entries = Reckon::LedgerParser.new(options).parse(StringIO.new(ledger)) 148 | expect(entries.first[:date]).to eq(Date.new(2024, 3, 12)) 149 | expect(entries.last[:date]).to eq(Date.new(2024, 3, 13)) 150 | expect(entries.length).to eq(2) 151 | end 152 | end 153 | 154 | describe "balance" do 155 | it "it should balance out missing account values" do 156 | @ledger.send(:balance, [ 157 | { :name => "Account1", :amount => 1000 }, 158 | { :name => "Account2", :amount => nil } 159 | ]).should == [{ :name => "Account1", :amount => 1000 }, 160 | { :name => "Account2", :amount => -1000 }] 161 | end 162 | 163 | it "it should balance out missing account values" do 164 | @ledger.send(:balance, [ 165 | { :name => "Account1", :amount => 1000 }, 166 | { :name => "Account2", :amount => 100 }, 167 | { :name => "Account3", :amount => -200 }, 168 | { :name => "Account4", :amount => nil } 169 | ]).should == [ 170 | { :name => "Account1", :amount => 1000 }, 171 | { :name => "Account2", :amount => 100 }, 172 | { :name => "Account3", :amount => -200 }, 173 | { :name => "Account4", :amount => -900 } 174 | ] 175 | end 176 | 177 | it "it should work on normal values too" do 178 | @ledger.send(:balance, [ 179 | { :name => "Account1", :amount => 1000 }, 180 | { :name => "Account2", :amount => -1000 } 181 | ]).should == [{ :name => "Account1", :amount => 1000 }, 182 | { :name => "Account2", :amount => -1000 }] 183 | end 184 | end 185 | 186 | # Data 187 | 188 | EXAMPLE_LEDGER = (<<~LEDGER).strip 189 | = /^Expenses:Books/ 190 | (Liabilities:Taxes) -0.10 191 | 192 | ~ Monthly 193 | Assets:Bank:Checking $500.00 194 | Income:Salary 195 | 196 | 2004-05-01 * Checking balance 197 | Assets:Bank:Checking $1,000.00 198 | Equity:Opening Balances 199 | 200 | 2004-05-01 * Checking balance 201 | Assets:Bank:Checking €1,000.00 202 | Equity:Opening Balances 203 | 204 | 2004-05-01 * Checking balance 205 | Assets:Bank:Checking 1,000.00 SEK 206 | Equity:Opening Balances 207 | 208 | 2004/05/01 * Investment balance 209 | Assets:Brokerage 50 AAPL @ $30.00 210 | Equity:Opening Balances 211 | 212 | ; blah 213 | !account blah 214 | 215 | !end 216 | 217 | D $1,000 218 | 219 | 2004/05/14 * Pay day 220 | Assets:Bank:Checking $500.00 221 | Income:Salary 222 | 223 | 2004/05/27 Book Store 224 | Expenses:Books $20.00 225 | Liabilities:MasterCard 226 | 2004/05/27 (100) Credit card company 227 | Liabilities:MasterCard $20.24 228 | Assets:Bank:Checking 229 | LEDGER 230 | end 231 | -------------------------------------------------------------------------------- /spec/reckon/money_column_spec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # encoding: utf-8 3 | 4 | require "spec_helper" 5 | require 'rubygems' 6 | require 'reckon' 7 | 8 | describe Reckon::MoneyColumn do 9 | describe "initialize" do 10 | it "should convert strings into Money" do 11 | Reckon::MoneyColumn.new( ["1.00", "-2.00"] ).should == [ 12 | Reckon::Money.new( 1.00 ), Reckon::Money.new( -2.00 ) ] 13 | end 14 | it "should convert empty string into nil" do 15 | Reckon::MoneyColumn.new( ["1.00", ""] ).should == [ 16 | Reckon::Money.new( 1.00 ), nil ] 17 | Reckon::MoneyColumn.new( ["", "-2.00"] ).should == [ 18 | nil, Reckon::Money.new( -2.00 ) ] 19 | end 20 | end 21 | 22 | describe "positive?" do 23 | it "should return false if one entry negative" do 24 | Reckon::MoneyColumn.new( ["1.00", "-2.00"] ).positive?.should == false 25 | end 26 | 27 | it "should return true if all elements positive or nil" do 28 | Reckon::MoneyColumn.new( ["1.00", "2.00"] ).positive?.should == true 29 | Reckon::MoneyColumn.new( ["1.00", ""] ).positive?.should == true 30 | end 31 | end 32 | 33 | describe "merge" do 34 | it "should merge two columns" do 35 | m1 = Reckon::MoneyColumn.new(["1.00", ""]) 36 | m2 = Reckon::MoneyColumn.new(["", "-2.00"]) 37 | expect(m1.merge!(m2)).to( 38 | eq([Reckon::Money.new(1.00), Reckon::Money.new(-2.00)]) 39 | ) 40 | 41 | m1 = Reckon::MoneyColumn.new(["1.00", "0"]) 42 | m2 = Reckon::MoneyColumn.new(["0", "-2.00"]) 43 | expect(m1.merge!(m2)).to( 44 | eq([Reckon::Money.new(1.00), Reckon::Money.new(-2.00)]) 45 | ) 46 | end 47 | 48 | it "should return nil if columns cannot be merged" do 49 | m1 = Reckon::MoneyColumn.new(["1.00", ""]) 50 | m2 = Reckon::MoneyColumn.new(["1.00", "-2.00"]) 51 | expect(m1.merge!(m2)).to eq([Reckon::Money.new(0), Reckon::Money.new(-2)]) 52 | 53 | m1 = Reckon::MoneyColumn.new(["From1", "Names"]) 54 | m2 = Reckon::MoneyColumn.new(["Acc", "NL28 INGB 1200 3244 16,21817"]) 55 | expect(m1.merge!(m2)).to eq([Reckon::Money.new(-1), Reckon::Money.new("NL28 INGB 1200 3244 16,21817")]) 56 | end 57 | 58 | it "should invert first column if both positive" do 59 | expect( 60 | Reckon::MoneyColumn.new(["1.00", ""]).merge!(Reckon::MoneyColumn.new( ["", "2.00"])) 61 | ).to eq([Reckon::Money.new(-1.00), Reckon::Money.new(2.00)]) 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/reckon/money_spec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # encoding: utf-8 3 | 4 | require "spec_helper" 5 | require 'rubygems' 6 | require 'reckon' 7 | 8 | describe Reckon::Money do 9 | describe "parse" do 10 | it "should handle currency indicators" do 11 | expect(Reckon::Money.new( "$2.00" )).to eq(2.00) 12 | expect(Reckon::Money.new("-$1025.67")).to eq(-1025.67) 13 | expect(Reckon::Money.new("$-1025.67")).to eq(-1025.67) 14 | end 15 | 16 | it "should handle the comma_separates_cents option correctly" do 17 | expect(Reckon::Money.new("$2,00", comma_separates_cents: true)).to eq(2.00) 18 | expect(Reckon::Money.new("-$1025,67", comma_separates_cents: true)).to eq(-1025.67) 19 | expect(Reckon::Money.new("$-1025,67", comma_separates_cents: true)).to eq(-1025.67) 20 | end 21 | 22 | it "should return 0 for an empty string" do 23 | expect(Reckon::Money.new("")).to eq(0) 24 | end 25 | 26 | it "should handle 1000 indicators correctly" do 27 | expect(Reckon::Money.new("$2.000,00", comma_separates_cents: true)).to eq(2000.00) 28 | expect(Reckon::Money.new("-$1,025.67")).to eq(-1025.67) 29 | end 30 | end 31 | 32 | describe "pretty" do 33 | it "work with negative and positive numbers" do 34 | expect(Reckon::Money.new(-20.00).pretty).to eq("-$20.00") 35 | expect(Reckon::Money.new(1558.52).pretty).to eq(" $1,558.52") 36 | end 37 | 38 | it "work with other currencies such as €" do 39 | expect(Reckon::Money.new(-20.00, currency: "€", suffixed: false).pretty).to eq("-€20.00") 40 | expect(Reckon::Money.new(1558.52, currency: "€", suffixed: false).pretty).to eq(" €1,558.52") 41 | end 42 | 43 | it "work with suffixed currencies such as SEK" do 44 | expect(Reckon::Money.new(-20.00, currency: "SEK", suffixed: true).pretty).to eq("-20.00 SEK") 45 | expect(Reckon::Money.new(1558.52, currency: "SEK", suffixed: true).pretty).to eq(" 1,558.52 SEK") 46 | end 47 | end 48 | 49 | describe "likelihood" do 50 | it "should return the likelihood that a string represents money" do 51 | expect(Reckon::Money::likelihood("$20.00")).to eq(65) 52 | end 53 | 54 | it "should return neutral for empty string" do 55 | expect(Reckon::Money::likelihood("")).to eq(0) 56 | end 57 | 58 | it "should recognize non-us currencies" do 59 | expect(Reckon::Money::likelihood("£480.00")).to eq(30) 60 | expect(Reckon::Money::likelihood("£1.480,00")).to eq(30) 61 | end 62 | 63 | it 'should not identify date columns as money' do 64 | expect(Reckon::Money::likelihood("22.01.2014")).to eq(0) 65 | end 66 | end 67 | 68 | describe "equality" do 69 | it "should be comparable to other money" do 70 | expect(Reckon::Money.new(2.0)).to eq(Reckon::Money.new(2.0)) 71 | expect(Reckon::Money.new(1.0)).to be <= Reckon::Money.new(2.0) 72 | expect(Reckon::Money.new(3.0)).to be > Reckon::Money.new(2.0) 73 | end 74 | it "should be comparable to other float" do 75 | expect(Reckon::Money.new(2.0)).to eq(2.0) 76 | expect(Reckon::Money.new(1.0)).to be <= 2.0 77 | expect(Reckon::Money.new(3.0)).to be > 2.0 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /spec/reckon/options_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe '#parse_opts' do 4 | it 'should assign to :string option' do 5 | options = Reckon::Options.parse_command_line_options( 6 | %w[-f - --unattended --account bank], 7 | StringIO.new('foo,bar,baz') 8 | ) 9 | expect(options[:string]).to eq('foo,bar,baz') 10 | end 11 | 12 | it 'should require --unattended flag' do 13 | expect { Reckon::Options.parse_command_line_options(%w[-f - --account bank]) }.to( 14 | raise_error(RuntimeError, "--unattended is required to use STDIN as CSV source.") 15 | ) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/spec.opts: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rubygems' 4 | require 'rspec' 5 | require 'reckon' 6 | 7 | RSpec.configure do |config| 8 | config.before(:all, &:silence_output) 9 | config.after(:all, &:enable_output) 10 | def fixture_path(file) 11 | File.expand_path(File.join(File.dirname(__FILE__), "data_fixtures", file)) 12 | end 13 | end 14 | 15 | public 16 | 17 | # Redirects stderr and stout to /dev/null.txt 18 | def silence_output 19 | # Store the original stderr and stdout in order to restore them later 20 | @original_stdout = $stdout 21 | @original_stderr = $stderr 22 | 23 | # Redirect stderr and stdout 24 | $stderr = File.new(File.join(File.dirname(__FILE__), 'test_log.txt'), 'w') 25 | $stdout = $stderr 26 | Reckon::LOGGER.reopen $stderr 27 | end 28 | 29 | # Replace stderr and stdout so anything else is output correctly 30 | def enable_output 31 | $stdout = @original_stdout 32 | @original_stdout = nil 33 | $stderr = @original_stderr 34 | @original_stderr = nil 35 | end 36 | --------------------------------------------------------------------------------