reckon 0.9.0 → 0.9.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +2 -0
- data/.rubocop.yml +20 -0
- data/CHANGELOG.md +10 -0
- data/Gemfile.lock +1 -1
- data/Rakefile +2 -2
- data/bin/build-new-version.sh +3 -2
- data/bin/reckon +1 -1
- data/lib/reckon/app.rb +27 -24
- data/lib/reckon/beancount_parser.rb +150 -0
- data/lib/reckon/cosine_similarity.rb +0 -1
- data/lib/reckon/csv_parser.rb +73 -37
- data/lib/reckon/date_column.rb +18 -7
- data/lib/reckon/ledger_parser.rb +23 -15
- data/lib/reckon/money.rb +18 -16
- data/lib/reckon/options.rb +44 -19
- data/lib/reckon/version.rb +1 -1
- data/lib/reckon.rb +1 -0
- data/spec/cosine_training_and_test.rb +1 -1
- data/spec/data_fixtures/multi-line-field.csv +5 -0
- data/spec/integration/invalid_header_example/output.ledger +6 -7
- data/spec/integration/invalid_header_example/test_args +1 -1
- data/spec/integration/tab_delimited_file/input.csv +2 -0
- data/spec/integration/tab_delimited_file/output.ledger +8 -0
- data/spec/integration/tab_delimited_file/test_args +1 -0
- data/spec/reckon/csv_parser_spec.rb +85 -26
- data/spec/reckon/date_column_spec.rb +6 -0
- data/spec/reckon/ledger_parser_spec.rb +25 -23
- data/spec/reckon/options_spec.rb +2 -2
- data/spec/spec_helper.rb +2 -0
- metadata +8 -2
data/lib/reckon/date_column.rb
CHANGED
@@ -2,13 +2,17 @@ module Reckon
|
|
2
2
|
class DateColumn < Array
|
3
3
|
attr_accessor :endian_precedence
|
4
4
|
def initialize( arr = [], options = {} )
|
5
|
-
|
5
|
+
# output date format
|
6
|
+
@ledger_date_format = options[:ledger_date_format]
|
7
|
+
|
8
|
+
# input date format
|
9
|
+
date_format = options[:date_format]
|
6
10
|
arr.each do |value|
|
7
|
-
if
|
11
|
+
if date_format
|
8
12
|
begin
|
9
|
-
value = Date.strptime(value,
|
13
|
+
value = Date.strptime(value, date_format)
|
10
14
|
rescue
|
11
|
-
puts "I'm having trouble parsing '#{value}' with the desired format: #{
|
15
|
+
puts "I'm having trouble parsing '#{value}' with the desired format: #{date_format}"
|
12
16
|
exit 1
|
13
17
|
end
|
14
18
|
else
|
@@ -34,7 +38,7 @@ module Reckon
|
|
34
38
|
self.push( value )
|
35
39
|
end
|
36
40
|
# if endian_precedence still nil, raise error
|
37
|
-
unless @endian_precedence ||
|
41
|
+
unless @endian_precedence || date_format
|
38
42
|
raise( "Unable to determine date format. Please specify using --date-format" )
|
39
43
|
end
|
40
44
|
end
|
@@ -54,7 +58,7 @@ module Reckon
|
|
54
58
|
date = self.for(index)
|
55
59
|
return "" if date.nil?
|
56
60
|
|
57
|
-
date.strftime(@
|
61
|
+
date.strftime(@ledger_date_format || '%Y-%m-%d')
|
58
62
|
end
|
59
63
|
|
60
64
|
def self.likelihood(entry)
|
@@ -65,7 +69,14 @@ module Reckon
|
|
65
69
|
date_score -= entry.gsub(/[\-\/\.\d:\[\]]/, '').length
|
66
70
|
date_score += 30 if entry =~ /^\d+[:\/\.-]\d+[:\/\.-]\d+([ :]\d+[:\/\.]\d+)?$/
|
67
71
|
date_score += 10 if entry =~ /^\d+\[\d+:GMT\]$/i
|
68
|
-
|
72
|
+
|
73
|
+
begin
|
74
|
+
DateTime.parse(entry)
|
75
|
+
date_score += 20
|
76
|
+
rescue Date::Error, ArgumentError
|
77
|
+
end
|
78
|
+
|
79
|
+
date_score
|
69
80
|
end
|
70
81
|
end
|
71
82
|
end
|
data/lib/reckon/ledger_parser.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
1
|
# frozen_string_literal: true
|
3
2
|
|
4
3
|
# From: https://www.ledger-cli.org/3.0/doc/ledger3.html#Transactions-and-Comments
|
@@ -110,20 +109,20 @@ require 'rubygems'
|
|
110
109
|
module Reckon
|
111
110
|
class LedgerParser
|
112
111
|
|
113
|
-
|
114
|
-
|
115
|
-
def initialize(
|
112
|
+
# ledger is an object that response to #each_line,
|
113
|
+
# (i.e. a StringIO or an IO object)
|
114
|
+
def initialize(options = {})
|
116
115
|
@options = options
|
117
116
|
@date_format = options[:ledger_date_format] || options[:date_format] || '%Y-%m-%d'
|
118
|
-
parse(ledger)
|
119
117
|
end
|
120
118
|
|
121
119
|
def parse(ledger)
|
122
|
-
|
120
|
+
entries = []
|
123
121
|
new_entry = {}
|
124
122
|
in_comment = false
|
125
123
|
comment_chars = ';#%*|'
|
126
|
-
ledger.
|
124
|
+
ledger.each_line do |entry|
|
125
|
+
entry.rstrip!
|
127
126
|
# strip comment lines
|
128
127
|
in_comment = true if entry == 'comment'
|
129
128
|
in_comment = false if entry == 'end comment'
|
@@ -132,7 +131,7 @@ module Reckon
|
|
132
131
|
|
133
132
|
# (date, type, code, description), type and code are optional
|
134
133
|
if (m = entry.match(%r{^(\d+[\d/-]+)\s+([*!])?\s*(\([^)]+\))?\s*(.*)$}))
|
135
|
-
add_entry(new_entry)
|
134
|
+
add_entry(entries, new_entry)
|
136
135
|
new_entry = {
|
137
136
|
date: try_parse_date(m[1]),
|
138
137
|
type: m[2] || "",
|
@@ -141,23 +140,24 @@ module Reckon
|
|
141
140
|
accounts: []
|
142
141
|
}
|
143
142
|
elsif entry =~ /^\s*$/ && new_entry[:date]
|
144
|
-
add_entry(new_entry)
|
143
|
+
add_entry(entries,new_entry)
|
145
144
|
new_entry = {}
|
146
145
|
elsif new_entry[:date] && entry =~ /^\s+/
|
147
146
|
LOGGER.info("Adding new account #{entry}")
|
148
147
|
new_entry[:accounts] << parse_account_line(entry)
|
149
148
|
else
|
150
149
|
LOGGER.info("Unknown entry type: #{entry}")
|
151
|
-
add_entry(new_entry)
|
150
|
+
add_entry(entries, new_entry)
|
152
151
|
new_entry = {}
|
153
152
|
end
|
154
153
|
end
|
155
|
-
add_entry(new_entry)
|
154
|
+
add_entry(entries, new_entry)
|
155
|
+
entries
|
156
156
|
end
|
157
157
|
|
158
158
|
# roughly matches ledger csv format
|
159
|
-
def to_csv
|
160
|
-
return
|
159
|
+
def to_csv(ledger)
|
160
|
+
return parse(ledger).flat_map do |n|
|
161
161
|
n[:accounts].map do |a|
|
162
162
|
row = [
|
163
163
|
n[:date].strftime(@date_format),
|
@@ -174,13 +174,21 @@ module Reckon
|
|
174
174
|
end
|
175
175
|
end
|
176
176
|
|
177
|
+
def format_row(row, line1, line2)
|
178
|
+
out = "#{row[:pretty_date]}\t#{row[:description]}#{row[:note] ? "\t; " + row[:note]: ""}\n"
|
179
|
+
out += "\t#{line1.first}\t\t\t#{line1.last}\n"
|
180
|
+
out += "\t#{line2.first}\t\t\t#{line2.last}\n\n"
|
181
|
+
out
|
182
|
+
end
|
183
|
+
|
184
|
+
|
177
185
|
private
|
178
186
|
|
179
|
-
def add_entry(entry)
|
187
|
+
def add_entry(entries, entry)
|
180
188
|
return unless entry[:date] && entry[:accounts].length > 1
|
181
189
|
|
182
190
|
entry[:accounts] = balance(entry[:accounts])
|
183
|
-
|
191
|
+
entries << entry
|
184
192
|
end
|
185
193
|
|
186
194
|
def try_parse_date(date_str)
|
data/lib/reckon/money.rb
CHANGED
@@ -6,11 +6,10 @@ module Reckon
|
|
6
6
|
include Comparable
|
7
7
|
attr_accessor :amount, :currency, :suffixed
|
8
8
|
def initialize(amount, options = {})
|
9
|
-
@options = options
|
10
9
|
@amount_raw = amount
|
11
10
|
@raw = options[:raw]
|
12
11
|
|
13
|
-
@amount = parse(amount, options)
|
12
|
+
@amount = parse(amount, options[:comma_separates_cents])
|
14
13
|
@amount = -@amount if options[:inverse]
|
15
14
|
@currency = options[:currency] || "$"
|
16
15
|
@suffixed = options[:suffixed]
|
@@ -21,7 +20,7 @@ module Reckon
|
|
21
20
|
end
|
22
21
|
|
23
22
|
def to_s
|
24
|
-
return @
|
23
|
+
return @raw ? "#{@amount_raw} | #{@amount}" : @amount
|
25
24
|
end
|
26
25
|
|
27
26
|
# unary minus
|
@@ -60,34 +59,37 @@ module Reckon
|
|
60
59
|
return (@amount >= 0 ? " " : "") + amt
|
61
60
|
end
|
62
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
|
+
|
63
76
|
def pretty_amount(amount)
|
64
77
|
sprintf("%0.2f", amount).reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
|
65
78
|
end
|
66
79
|
|
67
|
-
def parse(value,
|
80
|
+
def parse(value, comma_separates_cents)
|
68
81
|
value = value.to_s
|
69
82
|
# Empty string is treated as money with value 0
|
70
83
|
return value.to_f if value.to_s.empty?
|
71
84
|
|
72
85
|
invert = value.match(/^\(.*\)$/)
|
73
86
|
value = value.gsub(/[^0-9,.-]/, '')
|
74
|
-
value = value.tr('.', '').tr(',', '.') if
|
87
|
+
value = value.tr('.', '').tr(',', '.') if comma_separates_cents
|
75
88
|
value = value.tr(',', '')
|
76
89
|
value = value.to_f
|
77
90
|
return invert ? -value : value
|
78
91
|
end
|
79
92
|
|
80
|
-
def Money::likelihood(entry)
|
81
|
-
money_score = 0
|
82
|
-
# digits separated by , or . with no more than 2 trailing digits
|
83
|
-
money_score += 40 if entry.match(/\d+[,.]\d{2}[^\d]*$/)
|
84
|
-
money_score += 10 if entry[/^\$?\-?\$?\d+[\.,\d]*?[\.,]\d\d$/]
|
85
|
-
money_score += 10 if entry[/\d+[\.,\d]*?[\.,]\d\d$/]
|
86
|
-
money_score += entry.gsub(/[^\d\.\-\+,\(\)]/, '').length if entry.length < 7
|
87
|
-
money_score -= entry.length if entry.length > 12
|
88
|
-
money_score -= 20 if (entry !~ /^[\$\+\.\-,\d\(\)]+$/) && entry.length > 0
|
89
|
-
money_score
|
90
|
-
end
|
91
93
|
end
|
92
94
|
|
93
95
|
class MoneyColumn < Array
|
data/lib/reckon/options.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Reckon
|
4
|
+
# Singleton class for parsing command line flags
|
2
5
|
class Options
|
3
|
-
|
4
|
-
|
5
|
-
def self.parse(args = ARGV, stdin = $stdin)
|
6
|
+
def self.parse_command_line_options(args = ARGV, stdin = $stdin)
|
7
|
+
cli = HighLine.new
|
6
8
|
options = { output_file: $stdout }
|
7
9
|
OptionParser.new do |opts|
|
8
10
|
opts.banner = "Usage: Reckon.rb [options]"
|
@@ -32,19 +34,23 @@ module Reckon
|
|
32
34
|
options[:output_file] = File.open(o, 'a')
|
33
35
|
end
|
34
36
|
|
35
|
-
opts.on("-l", "--learn-from FILE",
|
37
|
+
opts.on("-l", "--learn-from FILE",
|
38
|
+
"An existing ledger file to learn accounts from") do |l|
|
36
39
|
options[:existing_ledger_file] = l
|
37
40
|
end
|
38
41
|
|
39
|
-
opts.on("", "--ignore-columns 1,2,5",
|
42
|
+
opts.on("", "--ignore-columns 1,2,5",
|
43
|
+
"Columns to ignore, starts from 1") do |ignore|
|
40
44
|
options[:ignore_columns] = ignore.split(",").map(&:to_i)
|
41
45
|
end
|
42
46
|
|
43
|
-
opts.on("", "--money-column 2", Integer,
|
47
|
+
opts.on("", "--money-column 2", Integer,
|
48
|
+
"Column number of the money column, starts from 1") do |col|
|
44
49
|
options[:money_column] = col
|
45
50
|
end
|
46
51
|
|
47
|
-
opts.on("", "--money-columns 2,3",
|
52
|
+
opts.on("", "--money-columns 2,3",
|
53
|
+
"Column number of the money columns, starts from 1 (1 or 2 columns)") do |ignore|
|
48
54
|
options[:money_columns] = ignore.split(",").map(&:to_i)
|
49
55
|
end
|
50
56
|
|
@@ -52,20 +58,28 @@ module Reckon
|
|
52
58
|
options[:raw] = n
|
53
59
|
end
|
54
60
|
|
55
|
-
opts.on("", "--date-column 3", Integer,
|
61
|
+
opts.on("", "--date-column 3", Integer,
|
62
|
+
"Column number of the date column, starts from 1") do |col|
|
56
63
|
options[:date_column] = col
|
57
64
|
end
|
58
65
|
|
59
|
-
opts.on("", "--contains-header [N]", Integer,
|
66
|
+
opts.on("", "--contains-header [N]", Integer,
|
67
|
+
"Skip N header rows - default 1") do |hdr|
|
60
68
|
options[:contains_header] = 1
|
61
69
|
options[:contains_header] = hdr.to_i
|
62
70
|
end
|
63
71
|
|
72
|
+
opts.on("", "--contains-footer [N]", Integer,
|
73
|
+
"Skip N footer rows - default 0") do |hdr|
|
74
|
+
options[:contains_footer] = hdr.to_i || 0
|
75
|
+
end
|
76
|
+
|
64
77
|
opts.on("", "--csv-separator ','", "CSV separator (default ',')") do |sep|
|
65
78
|
options[:csv_separator] = sep
|
66
79
|
end
|
67
80
|
|
68
|
-
opts.on("", "--comma-separates-cents",
|
81
|
+
opts.on("", "--comma-separates-cents",
|
82
|
+
"Use comma to separate cents ($100,50 vs. $100.50)") do |c|
|
69
83
|
options[:comma_separates_cents] = c
|
70
84
|
end
|
71
85
|
|
@@ -73,23 +87,28 @@ module Reckon
|
|
73
87
|
options[:encoding] = e
|
74
88
|
end
|
75
89
|
|
76
|
-
opts.on("-c", "--currency '$'",
|
77
|
-
|
90
|
+
opts.on("-c", "--currency '$'",
|
91
|
+
"Currency symbol to use - default $ (ex £, EUR)") do |e|
|
92
|
+
options[:currency] = e || '$'
|
78
93
|
end
|
79
94
|
|
80
|
-
opts.on("", "--date-format FORMAT",
|
95
|
+
opts.on("", "--date-format FORMAT",
|
96
|
+
"CSV file date format (see `date` for format)") do |d|
|
81
97
|
options[:date_format] = d
|
82
98
|
end
|
83
99
|
|
84
|
-
opts.on("", "--ledger-date-format FORMAT",
|
100
|
+
opts.on("", "--ledger-date-format FORMAT",
|
101
|
+
"Ledger date format (see `date` for format)") do |d|
|
85
102
|
options[:ledger_date_format] = d
|
86
103
|
end
|
87
104
|
|
88
|
-
opts.on("-u", "--unattended",
|
105
|
+
opts.on("-u", "--unattended",
|
106
|
+
"Don't ask questions and guess all the accounts automatically. Use with --learn-from or --account-tokens options.") do |n|
|
89
107
|
options[:unattended] = n
|
90
108
|
end
|
91
109
|
|
92
|
-
opts.on("-t", "--account-tokens FILE",
|
110
|
+
opts.on("-t", "--account-tokens FILE",
|
111
|
+
"YAML file with manually-assigned tokens for each account (see README)") do |a|
|
93
112
|
options[:account_tokens_file] = a
|
94
113
|
end
|
95
114
|
|
@@ -107,7 +126,8 @@ module Reckon
|
|
107
126
|
options[:default_outof_account] = a
|
108
127
|
end
|
109
128
|
|
110
|
-
opts.on("", "--fail-on-unknown-account",
|
129
|
+
opts.on("", "--fail-on-unknown-account",
|
130
|
+
"Fail on unmatched transactions.") do |n|
|
111
131
|
options[:fail_on_unknown_account] = n
|
112
132
|
end
|
113
133
|
|
@@ -115,6 +135,11 @@ module Reckon
|
|
115
135
|
options[:suffixed] = e
|
116
136
|
end
|
117
137
|
|
138
|
+
opts.on("", "--ledger-format FORMAT",
|
139
|
+
"Output/Learn format: BEANCOUNT or LEDGER. Default: LEDGER") do |n|
|
140
|
+
options[:format] = n
|
141
|
+
end
|
142
|
+
|
118
143
|
opts.on_tail("-h", "--help", "Show this message") do
|
119
144
|
puts opts
|
120
145
|
exit
|
@@ -137,7 +162,7 @@ module Reckon
|
|
137
162
|
end
|
138
163
|
|
139
164
|
unless options[:file]
|
140
|
-
options[:file] =
|
165
|
+
options[:file] = cli.ask("What CSV file should I parse? ")
|
141
166
|
unless options[:file].empty?
|
142
167
|
puts "\nYou must provide a CSV file to parse.\n"
|
143
168
|
puts parser
|
@@ -148,7 +173,7 @@ module Reckon
|
|
148
173
|
unless options[:bank_account]
|
149
174
|
raise "Must specify --account in unattended mode" if options[:unattended]
|
150
175
|
|
151
|
-
options[:bank_account] =
|
176
|
+
options[:bank_account] = cli.ask("What is this account named in Ledger?\n") do |q|
|
152
177
|
q.readline = true
|
153
178
|
q.validate = /^.{2,}$/
|
154
179
|
q.default = "Assets:Bank:Checking"
|
data/lib/reckon/version.rb
CHANGED
data/lib/reckon.rb
CHANGED
@@ -15,6 +15,7 @@ require_relative 'reckon/cosine_similarity'
|
|
15
15
|
require_relative 'reckon/date_column'
|
16
16
|
require_relative 'reckon/money'
|
17
17
|
require_relative 'reckon/ledger_parser'
|
18
|
+
require_relative 'reckon/beancount_parser'
|
18
19
|
require_relative 'reckon/csv_parser'
|
19
20
|
require_relative 'reckon/options'
|
20
21
|
require_relative 'reckon/app'
|
@@ -8,7 +8,7 @@ ledger_file = ARGV[0]
|
|
8
8
|
account = ARGV[1]
|
9
9
|
seed = ARGV[2] ? ARGV[2].to_i : Random.new_seed
|
10
10
|
|
11
|
-
ledger = Reckon::LedgerParser.new(File.
|
11
|
+
ledger = Reckon::LedgerParser.new(File.new(ledger_file))
|
12
12
|
matcher = Reckon::CosineSimilarity.new({})
|
13
13
|
|
14
14
|
train = []
|
@@ -1,8 +1,7 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
4016-02-19
|
6
|
-
Income:Unknown
|
7
|
-
Assets:Bank:Checking $0.00
|
1
|
+
2016-02-18 COTISATION JAZZ; COTISATION JAZZ; EUR
|
2
|
+
Expenses:Unknown
|
3
|
+
Assets:Bank:Checking -$8.10
|
8
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
|
@@ -1 +1 @@
|
|
1
|
-
-f input.csv --unattended --account Assets:Bank:Checking --contains-header 4
|
1
|
+
-f input.csv --unattended --account Assets:Bank:Checking --contains-header 4 --comma-separates-cents --verbose
|
@@ -0,0 +1 @@
|
|
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
|
@@ -1,5 +1,4 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
# coding: utf-8
|
3
2
|
|
4
3
|
require_relative "../spec_helper"
|
5
4
|
require 'rubygems'
|
@@ -8,24 +7,53 @@ require_relative '../../lib/reckon'
|
|
8
7
|
describe Reckon::CSVParser do
|
9
8
|
let(:chase) { Reckon::CSVParser.new(file: fixture_path('chase.csv')) }
|
10
9
|
let(:some_other_bank) { Reckon::CSVParser.new(file: fixture_path('some_other.csv')) }
|
11
|
-
let(:two_money_columns) {
|
10
|
+
let(:two_money_columns) {
|
11
|
+
Reckon::CSVParser.new(file: fixture_path('two_money_columns.csv'))
|
12
|
+
}
|
12
13
|
let(:suntrust_csv) { Reckon::CSVParser.new(file: fixture_path('suntrust.csv')) }
|
13
14
|
let(:simple_csv) { Reckon::CSVParser.new(file: fixture_path('simple.csv')) }
|
14
|
-
let(:nationwide) {
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
let(:
|
19
|
-
|
20
|
-
|
21
|
-
let(:
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
+
}
|
25
53
|
|
26
54
|
describe "parse" do
|
27
55
|
it "should use binary encoding if none specified and chardet fails" do
|
28
|
-
allow(CharDet).to receive(:detect).and_return({'encoding' => nil})
|
56
|
+
allow(CharDet).to receive(:detect).and_return({ 'encoding' => nil })
|
29
57
|
app = Reckon::CSVParser.new(file: fixture_path("extratofake.csv"))
|
30
58
|
expect(app.send(:try_encoding, "foobarbaz")).to eq("BINARY")
|
31
59
|
end
|
@@ -37,12 +65,16 @@ describe Reckon::CSVParser do
|
|
37
65
|
end
|
38
66
|
|
39
67
|
it "should work with other separators" do
|
40
|
-
Reckon::CSVParser.new(:string => "one;two\nthree;four",
|
68
|
+
Reckon::CSVParser.new(:string => "one;two\nthree;four",
|
69
|
+
:csv_separator => ';').columns.should == [
|
70
|
+
['one', 'three'], ['two', 'four']
|
71
|
+
]
|
41
72
|
end
|
42
73
|
|
43
74
|
it 'should parse quoted lines' do
|
44
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")
|
45
|
-
Reckon::CSVParser.new(string: file, csv_separator: ';',
|
76
|
+
Reckon::CSVParser.new(string: file, csv_separator: ';',
|
77
|
+
comma_separates_cents: true).columns.length.should == 12
|
46
78
|
end
|
47
79
|
|
48
80
|
it 'should parse csv with BOM' do
|
@@ -50,11 +82,26 @@ describe Reckon::CSVParser do
|
|
50
82
|
Reckon::CSVParser.new(file: file).columns.length.should == 41
|
51
83
|
end
|
52
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
|
+
|
53
96
|
describe 'file with invalid csv in header' do
|
54
97
|
let(:invalid_file) { fixture_path('invalid_header_example.csv') }
|
55
98
|
|
56
99
|
it 'should ignore invalid header lines' do
|
57
|
-
Reckon::CSVParser.new(file: invalid_file, contains_header: 4)
|
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
|
+
])
|
58
105
|
end
|
59
106
|
|
60
107
|
it 'should fail' do
|
@@ -67,19 +114,24 @@ describe Reckon::CSVParser do
|
|
67
114
|
|
68
115
|
describe "columns" do
|
69
116
|
it "should return the csv transposed" do
|
70
|
-
simple_csv.columns.should == [["entry1", "entry4"], ["entry2", "entry5"],
|
117
|
+
simple_csv.columns.should == [["entry1", "entry4"], ["entry2", "entry5"],
|
118
|
+
["entry3", "entry6"]]
|
71
119
|
chase.columns.length.should == 4
|
72
120
|
end
|
73
121
|
|
74
122
|
it "should be ok with empty lines" do
|
75
123
|
lambda {
|
76
|
-
Reckon::CSVParser.new(:string => "one,two\nthree,four\n\n\n\n\n").columns.should == [
|
124
|
+
Reckon::CSVParser.new(:string => "one,two\nthree,four\n\n\n\n\n").columns.should == [
|
125
|
+
['one', 'three'], ['two', 'four']
|
126
|
+
]
|
77
127
|
}.should_not raise_error
|
78
128
|
end
|
79
129
|
end
|
80
130
|
|
81
131
|
describe "detect_columns" do
|
82
|
-
let(:harder_date_example_csv) {
|
132
|
+
let(:harder_date_example_csv) {
|
133
|
+
Reckon::CSVParser.new(file: fixture_path('harder_date_example.csv'))
|
134
|
+
}
|
83
135
|
|
84
136
|
it "should detect the money column" do
|
85
137
|
chase.money_column_indices.should == [3]
|
@@ -165,13 +217,17 @@ describe Reckon::CSVParser do
|
|
165
217
|
end
|
166
218
|
|
167
219
|
it "should handle the comma_separates_cents option correctly" do
|
168
|
-
european_csv = Reckon::CSVParser.new(
|
220
|
+
european_csv = Reckon::CSVParser.new(
|
221
|
+
:string => "$2,00;something\n1.025,67;something else", :csv_separator => ';', :comma_separates_cents => true
|
222
|
+
)
|
169
223
|
european_csv.money_for(0).should == 2.00
|
170
224
|
european_csv.money_for(1).should == 1025.67
|
171
225
|
end
|
172
226
|
|
173
227
|
it "should return negated values if the inverse option is passed" do
|
174
|
-
inversed_csv = Reckon::CSVParser.new(
|
228
|
+
inversed_csv = Reckon::CSVParser.new(
|
229
|
+
file: fixture_path('inversed_credit_card.csv'), inverse: true
|
230
|
+
)
|
175
231
|
inversed_csv.money_for(0).should == -30.00
|
176
232
|
inversed_csv.money_for(3).should == 500.00
|
177
233
|
end
|
@@ -229,7 +285,8 @@ describe Reckon::CSVParser do
|
|
229
285
|
end
|
230
286
|
|
231
287
|
it "should not append empty description column" do
|
232
|
-
parser = Reckon::CSVParser.new(:string => '01/09/2015,05354 SUBWAY,8.19,,'
|
288
|
+
parser = Reckon::CSVParser.new(:string => '01/09/2015,05354 SUBWAY,8.19,,',
|
289
|
+
:date_format => '%d/%m/%Y')
|
233
290
|
parser.description_for(0).should == '05354 SUBWAY'
|
234
291
|
end
|
235
292
|
|
@@ -249,7 +306,8 @@ describe Reckon::CSVParser do
|
|
249
306
|
end
|
250
307
|
|
251
308
|
it "work with other currencies such as €" do
|
252
|
-
euro_bank = Reckon::CSVParser.new(file: fixture_path('some_other.csv'),
|
309
|
+
euro_bank = Reckon::CSVParser.new(file: fixture_path('some_other.csv'),
|
310
|
+
currency: "€", suffixed: false)
|
253
311
|
euro_bank.pretty_money_for(1).should == "-€20.00"
|
254
312
|
euro_bank.pretty_money_for(4).should == " €1,558.52"
|
255
313
|
euro_bank.pretty_money_for(7).should == "-€116.22"
|
@@ -258,7 +316,8 @@ describe Reckon::CSVParser do
|
|
258
316
|
end
|
259
317
|
|
260
318
|
it "work with suffixed currencies such as SEK" do
|
261
|
-
swedish_bank = Reckon::CSVParser.new(file: fixture_path('some_other.csv'),
|
319
|
+
swedish_bank = Reckon::CSVParser.new(file: fixture_path('some_other.csv'),
|
320
|
+
currency: 'SEK', suffixed: true)
|
262
321
|
swedish_bank.pretty_money_for(1).should == "-20.00 SEK"
|
263
322
|
swedish_bank.pretty_money_for(4).should == " 1,558.52 SEK"
|
264
323
|
swedish_bank.pretty_money_for(7).should == "-116.22 SEK"
|
@@ -274,7 +333,7 @@ describe Reckon::CSVParser do
|
|
274
333
|
|
275
334
|
describe '85 regression test' do
|
276
335
|
it 'should detect correct date column' do
|
277
|
-
p = Reckon::CSVParser.new(file:fixture_path('85-date-example.csv'))
|
336
|
+
p = Reckon::CSVParser.new(file: fixture_path('85-date-example.csv'))
|
278
337
|
expect(p.date_column_index).to eq(2)
|
279
338
|
end
|
280
339
|
end
|
@@ -50,4 +50,10 @@ describe Reckon::DateColumn do
|
|
50
50
|
.to eq('2013-12-13')
|
51
51
|
end
|
52
52
|
end
|
53
|
+
|
54
|
+
describe "#likelihood" do
|
55
|
+
it "should prefer numbers that looks like dates" do
|
56
|
+
expect(Reckon::DateColumn.likelihood("123456789")).to be < Reckon::DateColumn.likelihood("20160102")
|
57
|
+
end
|
58
|
+
end
|
53
59
|
end
|