reckon 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|