reckon 0.3.10 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/Gemfile.lock +1 -1
- data/README.md +44 -8
- data/lib/reckon/app.rb +87 -20
- data/lib/reckon/csv_parser.rb +5 -5
- data/lib/reckon/money.rb +7 -5
- data/reckon.gemspec +1 -1
- data/spec/data_fixtures/tokens.yaml +14 -0
- data/spec/reckon/app_spec.rb +75 -14
- data/spec/reckon/csv_parser_spec.rb +9 -7
- data/spec/reckon/date_column_spec.rb +15 -12
- metadata +22 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 701e85b9ec558657003c57d9d33744840fcdcb55
|
4
|
+
data.tar.gz: d343b374bf5375ec34a915a84b7a27724b53b995
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8489408851bf979388afb2f0ed8da24d30d738b82c83116f1551d8d1f43aebf17f4aeb9ad05a835323ca73199568b74265a556bb25e05c5f4f4585508d4aa863
|
7
|
+
data.tar.gz: 1a6845f390988e12286716351029a015165fa5dc8230aa1d0021616aa4a4e108a8fea64b0249176698a0c8edf8b4e4b4991517028e6a156a2dfecfd04fe067a3
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
ruby-2.
|
1
|
+
ruby-2.2.0
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -15,25 +15,26 @@ Assuming you have Ruby and [Rubygems](http://rubygems.org/pages/download) instal
|
|
15
15
|
First, login to your bank and export your transaction data as a CSV file.
|
16
16
|
|
17
17
|
To see how the CSV parses:
|
18
|
-
|
18
|
+
|
19
19
|
reckon -f bank.csv -p
|
20
20
|
|
21
21
|
If your CSV file has a header on the first line, include `--contains-header`.
|
22
22
|
|
23
23
|
To convert to ledger format and label everything, do:
|
24
|
-
|
24
|
+
|
25
25
|
reckon -f bank.csv -o output.dat
|
26
26
|
|
27
27
|
To have reckon learn from an existing ledger file, provide it with -l:
|
28
|
-
|
28
|
+
|
29
29
|
reckon -f bank.csv -l 2010.dat -o output.dat
|
30
30
|
|
31
31
|
Learn more:
|
32
32
|
|
33
33
|
> reckon -h
|
34
|
-
|
34
|
+
|
35
35
|
Usage: Reckon.rb [options]
|
36
36
|
|
37
|
+
|
37
38
|
-f, --file FILE The CSV file to parse
|
38
39
|
-a, --account name The Ledger Account this file is for
|
39
40
|
-v, --[no-]verbose Run verbosely
|
@@ -43,17 +44,23 @@ Learn more:
|
|
43
44
|
-l, --learn-from FILE An existing ledger file to learn accounts from
|
44
45
|
--ignore-columns 1,2,5
|
45
46
|
Columns to ignore in the CSV file - the first column is column 1
|
46
|
-
--contains-header
|
47
|
-
The first row of the CSV is a header and should be skipped
|
47
|
+
--contains-header [N]
|
48
|
+
The first row of the CSV is a header and should be skipped. Optionally add the number of rows to skip.
|
48
49
|
--csv-separator ','
|
49
50
|
Separator for parsing the CSV - default is comma.
|
50
51
|
--comma-separates-cents
|
51
52
|
Use comma instead of period to deliminate dollars from cents when parsing ($100,50 instead of $100.50)
|
52
|
-
--encoding
|
53
|
-
Specify an encoding for the CSV file
|
53
|
+
--encoding 'UTF-8'
|
54
|
+
Specify an encoding for the CSV file; not usually needed
|
54
55
|
-c, --currency '$' Currency symbol to use, defaults to $ (£, EUR)
|
55
56
|
--date-format '%d/%m/%Y'
|
56
57
|
Force the date format (see Ruby DateTime strftime)
|
58
|
+
-u, --unattended Don't ask questions and guess all the accounts automatically. Used with --learn-from or --account-tokens options.
|
59
|
+
-t, --account-tokens FILE YAML file with manually-assigned tokens for each account (see README)
|
60
|
+
--default-into-account name
|
61
|
+
Default into account
|
62
|
+
--default-outof-account name
|
63
|
+
Default 'out of' account
|
57
64
|
--suffixed
|
58
65
|
If --currency should be used as a suffix. Defaults to false.
|
59
66
|
-h, --help Show this message
|
@@ -61,6 +68,35 @@ Learn more:
|
|
61
68
|
|
62
69
|
If you find CSV files that it can't parse, send me examples or pull requests!
|
63
70
|
|
71
|
+
## Unattended mode
|
72
|
+
|
73
|
+
You can run reckon in a non-interactive mode.
|
74
|
+
To guess the accounts reckon can use an existing ledger file or a token file with keywords.
|
75
|
+
|
76
|
+
`reckon --unattended -l 2010.dat -f bank.csv -o ledger.dat`
|
77
|
+
|
78
|
+
`reckon --unattended --account-tokens tokens.yaml -f bank.csv -o ledger.dat`
|
79
|
+
|
80
|
+
Here's an example of `tokens.yaml`:
|
81
|
+
|
82
|
+
```
|
83
|
+
Income:
|
84
|
+
Salary:
|
85
|
+
- 'LÖN'
|
86
|
+
- 'Salary'
|
87
|
+
Expenses:
|
88
|
+
Bank:
|
89
|
+
- 'Comission'
|
90
|
+
- 'MasterCard'
|
91
|
+
Rent:
|
92
|
+
- '0011223344' # Landlord bank number
|
93
|
+
'[Internal:Transfer]': # Virtual account
|
94
|
+
- '4433221100' # Your own account number
|
95
|
+
```
|
96
|
+
|
97
|
+
If reckon can not guess the accounts it will use `Income:Unknown` or `Expenses:Unknown` names.
|
98
|
+
You can override them with `--default_outof_account` and `--default_into_account` options.
|
99
|
+
|
64
100
|
## Note on Patches/Pull Requests
|
65
101
|
|
66
102
|
* Fork the project.
|
data/lib/reckon/app.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
#coding: utf-8
|
2
2
|
require 'pp'
|
3
|
+
require 'yaml'
|
3
4
|
|
4
5
|
module Reckon
|
5
6
|
class App
|
@@ -17,6 +18,11 @@ module Reckon
|
|
17
18
|
learn!
|
18
19
|
end
|
19
20
|
|
21
|
+
def interactive_output(str)
|
22
|
+
return if options[:unattended]
|
23
|
+
puts str
|
24
|
+
end
|
25
|
+
|
20
26
|
def learn_from(ledger)
|
21
27
|
LedgerParser.new(ledger).entries.each do |entry|
|
22
28
|
entry[:accounts].each do |account|
|
@@ -32,12 +38,26 @@ module Reckon
|
|
32
38
|
seen[row[:pretty_date]] && seen[row[:pretty_date]][row[:pretty_money]]
|
33
39
|
end
|
34
40
|
|
41
|
+
def extract_account_tokens(subtree, account = nil)
|
42
|
+
if subtree.is_a?(Array)
|
43
|
+
{ account => subtree }
|
44
|
+
else
|
45
|
+
at = subtree.map { |k, v| extract_account_tokens(v, [account, k].compact.join(':')) }
|
46
|
+
at.inject({}) { |k, v| k = k.merge(v)}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
35
50
|
def learn!
|
36
|
-
if options[:
|
37
|
-
fail "#{options[:
|
38
|
-
|
39
|
-
|
51
|
+
if options[:account_tokens_file]
|
52
|
+
fail "#{options[:account_tokens_file]} doesn't exist!" unless File.exists?(options[:account_tokens_file])
|
53
|
+
extract_account_tokens(YAML.load_file(options[:account_tokens_file])).each do |account, tokens|
|
54
|
+
tokens.each { |t| learn_about_account(account, t) }
|
55
|
+
end
|
40
56
|
end
|
57
|
+
return unless options[:existing_ledger_file]
|
58
|
+
fail "#{options[:existing_ledger_file]} doesn't exist!" unless File.exists?(options[:existing_ledger_file])
|
59
|
+
ledger_data = File.read(options[:existing_ledger_file])
|
60
|
+
learn_from(ledger_data)
|
41
61
|
end
|
42
62
|
|
43
63
|
def learn_about_account(account, data)
|
@@ -57,23 +77,34 @@ module Reckon
|
|
57
77
|
def walk_backwards
|
58
78
|
seen_anything_new = false
|
59
79
|
each_row_backwards do |row|
|
60
|
-
|
80
|
+
interactive_output Terminal::Table.new(:rows => [ [ row[:pretty_date], row[:pretty_money], row[:description] ] ])
|
61
81
|
|
62
82
|
if already_seen?(row)
|
63
|
-
|
83
|
+
interactive_output "NOTE: This row is very similar to a previous one!"
|
64
84
|
if !seen_anything_new
|
65
|
-
|
85
|
+
interactive_output "Skipping..."
|
66
86
|
next
|
67
87
|
end
|
68
88
|
else
|
69
89
|
seen_anything_new = true
|
70
90
|
end
|
71
91
|
|
92
|
+
possible_answers = weighted_account_match( row ).map! { |a| a[:account] }
|
93
|
+
|
72
94
|
ledger = if row[:money] > 0
|
73
|
-
|
95
|
+
if options[:unattended]
|
96
|
+
out_of_account = possible_answers.first || options[:default_outof_account] || 'Income:Unknown'
|
97
|
+
else
|
98
|
+
out_of_account = ask("Which account provided this income? ([account]/[q]uit/[s]kip) ") { |q|
|
99
|
+
q.completion = possible_answers
|
100
|
+
q.readline = true
|
101
|
+
q.default = possible_answers.first
|
102
|
+
}
|
103
|
+
end
|
104
|
+
|
74
105
|
finish if out_of_account == "quit" || out_of_account == "q"
|
75
106
|
if out_of_account == "skip" || out_of_account == "s"
|
76
|
-
|
107
|
+
interactive_output "Skipping"
|
77
108
|
next
|
78
109
|
end
|
79
110
|
|
@@ -81,10 +112,18 @@ module Reckon
|
|
81
112
|
[options[:bank_account], row[:pretty_money]],
|
82
113
|
[out_of_account, row[:pretty_money_negated]] )
|
83
114
|
else
|
84
|
-
|
115
|
+
if options[:unattended]
|
116
|
+
into_account = possible_answers.first || options[:default_into_account] || 'Expenses:Unknown'
|
117
|
+
else
|
118
|
+
into_account = ask("To which account did this money go? ([account]/[q]uit/[s]kip) ") { |q|
|
119
|
+
q.completion = possible_answers
|
120
|
+
q.readline = true
|
121
|
+
q.default = possible_answers.first
|
122
|
+
}
|
123
|
+
end
|
85
124
|
finish if into_account == "quit" || into_account == 'q'
|
86
125
|
if into_account == "skip" || into_account == 's'
|
87
|
-
|
126
|
+
interactive_output "Skipping"
|
88
127
|
next
|
89
128
|
end
|
90
129
|
|
@@ -93,14 +132,14 @@ module Reckon
|
|
93
132
|
[options[:bank_account], row[:pretty_money]] )
|
94
133
|
end
|
95
134
|
|
96
|
-
learn_from(ledger)
|
135
|
+
learn_from(ledger) unless options[:account_tokens_file]
|
97
136
|
output(ledger)
|
98
137
|
end
|
99
138
|
end
|
100
139
|
|
101
140
|
def finish
|
102
141
|
options[:output_file].close unless options[:output_file] == STDOUT
|
103
|
-
|
142
|
+
interactive_output "Exiting."
|
104
143
|
exit
|
105
144
|
end
|
106
145
|
|
@@ -109,7 +148,8 @@ module Reckon
|
|
109
148
|
options[:output_file].flush
|
110
149
|
end
|
111
150
|
|
112
|
-
|
151
|
+
# Weigh accounts by how well they match the row
|
152
|
+
def weighted_account_match( row )
|
113
153
|
query_tokens = tokenize(row[:description])
|
114
154
|
|
115
155
|
search_vector = []
|
@@ -133,9 +173,16 @@ module Reckon
|
|
133
173
|
{ :cosine => (0...account_vector.length).to_a.inject(0) { |m, i| m + search_vector[i] * account_vector[i] },
|
134
174
|
:account => account }
|
135
175
|
end
|
136
|
-
|
137
176
|
account_vectors.sort! {|a, b| b[:cosine] <=> a[:cosine] }
|
138
|
-
|
177
|
+
|
178
|
+
# Return empty set if no accounts matched so that we can fallback to the defaults in the unattended mode
|
179
|
+
if options[:unattended]
|
180
|
+
if account_vectors.first && account_vectors.first[:account]
|
181
|
+
account_vectors = [] if account_vectors.first[:cosine] == 0
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
return account_vectors
|
139
186
|
end
|
140
187
|
|
141
188
|
def ledger_format(row, line1, line2)
|
@@ -152,15 +199,15 @@ module Reckon
|
|
152
199
|
t << [ row[:pretty_date], row[:pretty_money], row[:description] ]
|
153
200
|
end
|
154
201
|
end
|
155
|
-
|
202
|
+
interactive_output output
|
156
203
|
end
|
157
204
|
|
158
205
|
def each_row_backwards
|
159
206
|
rows = []
|
160
207
|
(0...@csv_parser.columns.first.length).to_a.each do |index|
|
161
|
-
rows << { :date => @csv_parser.date_for(index),
|
208
|
+
rows << { :date => @csv_parser.date_for(index),
|
162
209
|
:pretty_date => @csv_parser.pretty_date_for(index),
|
163
|
-
:pretty_money => @csv_parser.pretty_money_for(index),
|
210
|
+
:pretty_money => @csv_parser.pretty_money_for(index),
|
164
211
|
:pretty_money_negated => @csv_parser.pretty_money_for(index, :negate),
|
165
212
|
:money => @csv_parser.money_for(index),
|
166
213
|
:description => @csv_parser.description_for(index) }
|
@@ -221,7 +268,7 @@ module Reckon
|
|
221
268
|
options[:comma_separates_cents] = c
|
222
269
|
end
|
223
270
|
|
224
|
-
opts.on("", "--encoding
|
271
|
+
opts.on("", "--encoding 'UTF-8'", "Specify an encoding for the CSV file; not usually needed") do |e|
|
225
272
|
options[:encoding] = e
|
226
273
|
end
|
227
274
|
|
@@ -233,6 +280,22 @@ module Reckon
|
|
233
280
|
options[:date_format] = d
|
234
281
|
end
|
235
282
|
|
283
|
+
opts.on("-u", "--unattended", "Don't ask questions and guess all the accounts automatically. Used with --learn-from or --account-tokens options.") do |n|
|
284
|
+
options[:unattended] = n
|
285
|
+
end
|
286
|
+
|
287
|
+
opts.on("-t", "--account-tokens FILE", "YAML file with manually-assigned tokens for each account (see README)") do |a|
|
288
|
+
options[:account_tokens_file] = a
|
289
|
+
end
|
290
|
+
|
291
|
+
opts.on("", "--default-into-account name", "Default into account") do |a|
|
292
|
+
options[:default_into_account] = a
|
293
|
+
end
|
294
|
+
|
295
|
+
opts.on("", "--default-outof-account name", "Default 'out of' account") do |a|
|
296
|
+
options[:default_outof_account] = a
|
297
|
+
end
|
298
|
+
|
236
299
|
opts.on("", "--suffixed", "If --currency should be used as a suffix. Defaults to false.") do |e|
|
237
300
|
options[:suffixed] = e
|
238
301
|
end
|
@@ -260,7 +323,11 @@ module Reckon
|
|
260
323
|
end
|
261
324
|
|
262
325
|
unless options[:bank_account]
|
326
|
+
|
327
|
+
fail "Please specify --account for the unattended mode" if options[:unattended]
|
328
|
+
|
263
329
|
options[:bank_account] = ask("What is the account name of this bank account in Ledger? ") do |q|
|
330
|
+
q.readline = true
|
264
331
|
q.validate = /^.{2,}$/
|
265
332
|
q.default = "Assets:Bank:Checking"
|
266
333
|
end
|
data/lib/reckon/csv_parser.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
require 'pp'
|
3
3
|
|
4
4
|
module Reckon
|
5
|
-
class CSVParser
|
5
|
+
class CSVParser
|
6
6
|
attr_accessor :options, :csv_data, :money_column_indices, :date_column_index, :description_column_indices, :money_column, :date_column
|
7
7
|
|
8
8
|
def initialize(options = {})
|
@@ -63,7 +63,7 @@ module Reckon
|
|
63
63
|
date_score += 5 if entry =~ /^[\-\/\.\d:\[\]]+$/
|
64
64
|
date_score += entry.gsub(/[^\-\/\.\d:\[\]]/, '').length if entry.gsub(/[^\-\/\.\d:\[\]]/, '').length > 3
|
65
65
|
date_score -= entry.gsub(/[\-\/\.\d:\[\]]/, '').length
|
66
|
-
date_score += 30 if entry =~ /^\d+[
|
66
|
+
date_score += 30 if entry =~ /^\d+[:\/\.-]\d+[:\/\.-]\d+([ :]\d+[:\/\.]\d+)?$/
|
67
67
|
date_score += 10 if entry =~ /^\d+\[\d+:GMT\]$/i
|
68
68
|
|
69
69
|
# Try to determine if this is a balance column
|
@@ -132,7 +132,7 @@ module Reckon
|
|
132
132
|
puts "please report this issue to us so we can take a look!\n"
|
133
133
|
end
|
134
134
|
end
|
135
|
-
|
135
|
+
|
136
136
|
# Some csv files negative/positive amounts are indicated in separate account
|
137
137
|
def detect_sign_column
|
138
138
|
return if columns[0].length <= 2 # This test needs requires more than two rows otherwise will lead to false positives
|
@@ -141,13 +141,13 @@ module Reckon
|
|
141
141
|
column = columns[ @money_column_indices[0] - 1 ]
|
142
142
|
signs = column.uniq
|
143
143
|
end
|
144
|
-
if (signs.length != 2 &&
|
144
|
+
if (signs.length != 2 &&
|
145
145
|
(@money_column_indices[0] + 1 < columns.length))
|
146
146
|
column = columns[ @money_column_indices[0] + 1 ]
|
147
147
|
signs = column.uniq
|
148
148
|
end
|
149
149
|
if signs.length == 2
|
150
|
-
negative_first = true
|
150
|
+
negative_first = true
|
151
151
|
negative_first = false if signs[0] == "Bij" || signs[0].downcase =~ /^cr/ # look for known debit indicators
|
152
152
|
@money_column.each_with_index do |money, i|
|
153
153
|
if negative_first && column[i] == signs[0]
|
data/lib/reckon/money.rb
CHANGED
@@ -39,7 +39,7 @@ module Reckon
|
|
39
39
|
(@amount >= 0 ? " " : "") + sprintf("%0.2f #{@currency}", @amount * (negate ? -1 : 1))
|
40
40
|
else
|
41
41
|
(@amount >= 0 ? " " : "") + sprintf("%0.2f", @amount * (negate ? -1 : 1)).gsub(/^((\-)|)(?=\d)/, "\\1#{@currency}")
|
42
|
-
end
|
42
|
+
end
|
43
43
|
end
|
44
44
|
|
45
45
|
def Money::from_s( value, options = {} )
|
@@ -108,11 +108,13 @@ module Reckon
|
|
108
108
|
value = [$1, $2, $3].join("/") if value =~ /^(\d{4})(\d{2})(\d{2})\d+\[\d+\:GMT\]$/ # chase format
|
109
109
|
value = [$3, $2, $1].join("/") if value =~ /^(\d{2})\.(\d{2})\.(\d{4})$/ # german format
|
110
110
|
value = [$3, $2, $1].join("/") if value =~ /^(\d{2})\-(\d{2})\-(\d{4})$/ # nordea format
|
111
|
+
value = [$1, $2, $3].join("/") if value =~ /^(\d{4})\-(\d{2})\-(\d{2})$/ # yyyy-mm-dd format
|
111
112
|
value = [$1, $2, $3].join("/") if value =~ /^(\d{4})(\d{2})(\d{2})/ # yyyymmdd format
|
112
113
|
|
114
|
+
|
113
115
|
unless @endian_precedence # Try to detect endian_precedence
|
114
116
|
reg_match = value.match( /^(\d\d)\/(\d\d)\/\d\d\d?\d?/ )
|
115
|
-
# If first one is not \d\d/\d\d/\d\d\d?\d set it to default
|
117
|
+
# If first one is not \d\d/\d\d/\d\d\d?\d set it to default
|
116
118
|
if !reg_match
|
117
119
|
@endian_precedence = [:middle, :little]
|
118
120
|
elsif reg_match[1].to_i > 12
|
@@ -122,7 +124,7 @@ module Reckon
|
|
122
124
|
end
|
123
125
|
end
|
124
126
|
end
|
125
|
-
self.push( value )
|
127
|
+
self.push( value )
|
126
128
|
end
|
127
129
|
# if endian_precedence still nil, raise error
|
128
130
|
unless @endian_precedence || options[:date_format]
|
@@ -132,10 +134,10 @@ module Reckon
|
|
132
134
|
|
133
135
|
def for( index )
|
134
136
|
value = self.at( index )
|
135
|
-
guess = Chronic.parse(value, :context => :past,
|
137
|
+
guess = Chronic.parse(value, :context => :past,
|
136
138
|
:endian_precedence => @endian_precedence )
|
137
139
|
if guess.to_i < 953236800 && value =~ /\//
|
138
|
-
guess = Chronic.parse((value.split("/")[0...-1] + [(2000 + value.split("/").last.to_i).to_s]).join("/"), :context => :past,
|
140
|
+
guess = Chronic.parse((value.split("/")[0...-1] + [(2000 + value.split("/").last.to_i).to_s]).join("/"), :context => :past,
|
139
141
|
:endian_precedence => @endian_precedence)
|
140
142
|
end
|
141
143
|
guess
|
data/reckon.gemspec
CHANGED
@@ -3,7 +3,7 @@ $:.push File.expand_path("../lib", __FILE__)
|
|
3
3
|
|
4
4
|
Gem::Specification.new do |s|
|
5
5
|
s.name = %q{reckon}
|
6
|
-
s.version = "0.
|
6
|
+
s.version = "0.4.0"
|
7
7
|
s.authors = ["Andrew Cantino", "BlackEdder"]
|
8
8
|
s.email = %q{andrew@iterationlabs.com}
|
9
9
|
s.homepage = %q{https://github.com/cantino/reckon}
|
data/spec/reckon/app_spec.rb
CHANGED
@@ -6,27 +6,77 @@ require 'rubygems'
|
|
6
6
|
require 'reckon'
|
7
7
|
|
8
8
|
describe Reckon::App do
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
9
|
+
context 'with chase csv input' do
|
10
|
+
before do
|
11
|
+
@chase = Reckon::App.new(:string => BANK_CSV)
|
12
|
+
@chase.learn_from( BANK_LEDGER )
|
13
|
+
@rows = []
|
14
|
+
@chase.each_row_backwards { |row| @rows.push( row ) }
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "each_row_backwards" do
|
18
|
+
it "should return rows with hashes" do
|
19
|
+
@rows[0][:pretty_date].should == "2009/12/10"
|
20
|
+
@rows[0][:pretty_money].should == " $2105.00"
|
21
|
+
@rows[0][:description].should == "CREDIT; Some Company vendorpymt PPD ID: 5KL3832735"
|
22
|
+
@rows[1][:pretty_date].should == "2009/12/11"
|
23
|
+
@rows[1][:pretty_money].should == "-$116.22"
|
24
|
+
@rows[1][:description].should == "CREDIT; PAYPAL TRANSFER PPD ID: PAYPALSDSL"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "weighted_account_match" do
|
29
|
+
it "should guess the correct account" do
|
30
|
+
@chase.weighted_account_match( @rows[7] ).first[:account].should == "Expenses:Books"
|
31
|
+
end
|
32
|
+
end
|
13
33
|
end
|
14
34
|
|
15
|
-
|
16
|
-
|
17
|
-
@
|
18
|
-
@
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
35
|
+
context 'unattended mode with chase csv input' do
|
36
|
+
before do
|
37
|
+
@output_file = StringIO.new
|
38
|
+
@chase = Reckon::App.new(:string => BANK_CSV, :unattended => true, :output_file => @output_file)
|
39
|
+
end
|
40
|
+
|
41
|
+
describe 'walk backwards' do
|
42
|
+
it 'should assign Income:Unknown and Expenses:Unknown by default' do
|
43
|
+
@chase.walk_backwards
|
44
|
+
@output_file.string.scan('Expenses:Unknown').count.should == 6
|
45
|
+
@output_file.string.scan('Income:Unknown').count.should == 3
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'should change default account names' do
|
49
|
+
@chase = Reckon::App.new(:string => BANK_CSV,
|
50
|
+
:unattended => true,
|
51
|
+
:output_file => @output_file,
|
52
|
+
:default_into_account => 'Expenses:Default',
|
53
|
+
:default_outof_account => 'Income:Default')
|
54
|
+
@chase.walk_backwards
|
55
|
+
@output_file.string.scan('Expenses:Default').count.should == 6
|
56
|
+
@output_file.string.scan('Income:Default').count.should == 3
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'should learn from a ledger file' do
|
60
|
+
@chase.learn_from( BANK_LEDGER )
|
61
|
+
@chase.walk_backwards
|
62
|
+
@output_file.string.scan('Expenses:Books').count.should == 1
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'should learn from an account tokens file' do
|
66
|
+
@chase = Reckon::App.new(:string => BANK_CSV,
|
67
|
+
:unattended => true,
|
68
|
+
:output_file => @output_file,
|
69
|
+
:account_tokens_file => 'spec/data_fixtures/tokens.yaml')
|
70
|
+
@chase.walk_backwards
|
71
|
+
@output_file.string.scan('Expenses:Books').count.should == 1
|
72
|
+
end
|
23
73
|
end
|
24
74
|
end
|
25
|
-
|
75
|
+
|
26
76
|
#DATA
|
27
77
|
BANK_CSV = (<<-CSV).strip
|
28
78
|
DEBIT,20091224120000[0:GMT],"HOST 037196321563 MO 12/22SLICEHOST",-85.00
|
29
|
-
CHECK,20091224120000[0:GMT],"
|
79
|
+
CHECK,20091224120000[0:GMT],"Book Store",-20.00
|
30
80
|
DEBIT,20091224120000[0:GMT],"GITHUB 041287430274 CA 12/22GITHUB 04",-7.00
|
31
81
|
CREDIT,20091223120000[0:GMT],"Some Company vendorpymt PPD ID: 59728JSL20",3520.00
|
32
82
|
CREDIT,20091223120000[0:GMT],"Blarg BLARG REVENUE PPD ID: 00jah78563",1558.52
|
@@ -35,4 +85,15 @@ describe Reckon::App do
|
|
35
85
|
CREDIT,20091211120000[0:GMT],"PAYPAL TRANSFER PPD ID: PAYPALSDSL",-116.22
|
36
86
|
CREDIT,20091210120000[0:GMT],"Some Company vendorpymt PPD ID: 5KL3832735",2105.00
|
37
87
|
CSV
|
88
|
+
|
89
|
+
BANK_LEDGER = (<<-LEDGER).strip
|
90
|
+
2004/05/14 * Pay day
|
91
|
+
Assets:Bank:Checking $500.00
|
92
|
+
Income:Salary
|
93
|
+
|
94
|
+
2004/05/27 Book Store
|
95
|
+
Expenses:Books $20.00
|
96
|
+
Liabilities:MasterCard
|
97
|
+
LEDGER
|
98
|
+
|
38
99
|
end
|
@@ -30,7 +30,7 @@ describe Reckon::CSVParser do
|
|
30
30
|
@chase.settings[:testing].should be_true
|
31
31
|
Reckon::CSVParser.settings[:testing].should be_true
|
32
32
|
end
|
33
|
-
|
33
|
+
|
34
34
|
describe "parse" do
|
35
35
|
it "should work with foreign character encodings" do
|
36
36
|
app = Reckon::CSVParser.new(:file => File.expand_path(File.join(File.dirname(__FILE__), "..", "data_fixtures", "extratofake.csv")))
|
@@ -48,7 +48,7 @@ describe Reckon::CSVParser do
|
|
48
48
|
@simple_csv.columns.should == [["entry1", "entry4"], ["entry2", "entry5"], ["entry3", "entry6"]]
|
49
49
|
@chase.columns.length.should == 4
|
50
50
|
end
|
51
|
-
|
51
|
+
|
52
52
|
it "should be ok with empty lines" do
|
53
53
|
lambda {
|
54
54
|
Reckon::CSVParser.new(:string => "one,two\nthree,four\n\n\n\n\n").columns.should == [['one', 'three'], ['two', 'four']]
|
@@ -60,7 +60,7 @@ describe Reckon::CSVParser do
|
|
60
60
|
before do
|
61
61
|
@harder_date_example_csv = Reckon::CSVParser.new(:string => HARDER_DATE_EXAMPLE)
|
62
62
|
end
|
63
|
-
|
63
|
+
|
64
64
|
it "should detect the money column" do
|
65
65
|
@chase.money_column_indices.should == [3]
|
66
66
|
@some_other_bank.money_column_indices.should == [3]
|
@@ -86,6 +86,7 @@ describe Reckon::CSVParser do
|
|
86
86
|
@french_csv.date_column_index.should == 2
|
87
87
|
@broker_canada.date_column_index.should == 0
|
88
88
|
@intuit_mint.date_column_index.should == 0
|
89
|
+
Reckon::CSVParser.new(:string => '2014-01-13,"22211100000",-10').date_column_index.should == 0
|
89
90
|
end
|
90
91
|
|
91
92
|
it "should consider all other columns to be description columns" do
|
@@ -121,7 +122,7 @@ describe Reckon::CSVParser do
|
|
121
122
|
@danish_kroner_nordea.money_for(5).should == -655.00
|
122
123
|
@yyyymmdd_date.money_for(0).should == -123.45
|
123
124
|
@ing_csv.money_for(0).should == -136.13
|
124
|
-
@ing_csv.money_for(1).should == 375.00
|
125
|
+
@ing_csv.money_for(1).should == 375.00
|
125
126
|
@austrian_csv.money_for(0).should == -18.00
|
126
127
|
@austrian_csv.money_for(2).should == 120.00
|
127
128
|
@french_csv.money_for(0).should == -10.00
|
@@ -294,8 +295,8 @@ describe Reckon::CSVParser do
|
|
294
295
|
|
295
296
|
ING_CSV = (<<-CSV).strip
|
296
297
|
20121115,From1,Acc,T1,IC,Af,"136,13",Incasso,SEPA Incasso, Opm1
|
297
|
-
20121112,Names,NL28 INGB 1200 3244 16,21817,GT,Bij,"375,00", Opm2
|
298
|
-
20091117,Names,NL28 INGB 1200 3244 16,21817,GT,Af,"257,50", Opm3
|
298
|
+
20121112,Names,NL28 INGB 1200 3244 16,21817,GT,Bij,"375,00", Opm2
|
299
|
+
20091117,Names,NL28 INGB 1200 3244 16,21817,GT,Af,"257,50", Opm3
|
299
300
|
CSV
|
300
301
|
|
301
302
|
HARDER_DATE_EXAMPLE = (<<-CSV).strip
|
@@ -377,7 +378,7 @@ describe Reckon::CSVParser do
|
|
377
378
|
2013-06-27,2013-06-27,Dividend,ICICI BK SPONSORED ADR,IBN,100,,,66.70,USD
|
378
379
|
2013-06-19,2013-06-24,Buy,ISHARES S&P/TSX CAPPED REIT IN,XRE,300,15.90,CDN,-4779.95,CAD
|
379
380
|
2013-06-17,2013-06-17,Contribution,CONTRIBUTION,,,,,600.00,CAD
|
380
|
-
2013-05-22,2013-05-22,Dividend,NATBK,NA,70,,,58.10,CAD
|
381
|
+
2013-05-22,2013-05-22,Dividend,NATBK,NA,70,,,58.10,CAD
|
381
382
|
CSV
|
382
383
|
|
383
384
|
INTUIT_MINT_EXAMPLE = (<<-CSV).strip
|
@@ -390,4 +391,5 @@ describe Reckon::CSVParser do
|
|
390
391
|
"1/30/2014","Costco","[PR]COSTCO WHOLESAL","559.96","debit","Business Services","Chequing","",""
|
391
392
|
CSV
|
392
393
|
|
394
|
+
|
393
395
|
end
|
@@ -8,11 +8,11 @@ require 'reckon'
|
|
8
8
|
describe Reckon::DateColumn do
|
9
9
|
describe "initialize" do
|
10
10
|
it "should detect us and world time" do
|
11
|
-
Reckon::DateColumn.new( ["01/02/2013", "01/14/2013"] ).endian_precedence.should == [:middle]
|
12
|
-
Reckon::DateColumn.new( ["01/02/2013", "14/01/2013"] ).endian_precedence.should == [:little]
|
11
|
+
Reckon::DateColumn.new( ["01/02/2013", "01/14/2013"] ).endian_precedence.should == [:middle]
|
12
|
+
Reckon::DateColumn.new( ["01/02/2013", "14/01/2013"] ).endian_precedence.should == [:little]
|
13
13
|
end
|
14
14
|
it "should set endian_precedence to default when date format cannot be misinterpreted" do
|
15
|
-
Reckon::DateColumn.new( ["2013/01/02"] ).endian_precedence.should == [:middle,:little]
|
15
|
+
Reckon::DateColumn.new( ["2013/01/02"] ).endian_precedence.should == [:middle,:little]
|
16
16
|
end
|
17
17
|
it "should raise an error when in doubt" do
|
18
18
|
expect{ Reckon::DateColumn.new( ["01/02/2013", "01/03/2013"] )}.to raise_error( StandardError )
|
@@ -20,20 +20,23 @@ describe Reckon::DateColumn do
|
|
20
20
|
end
|
21
21
|
describe "for" do
|
22
22
|
it "should detect the date" do
|
23
|
-
Reckon::DateColumn.new( ["13/12/2013"] ).for( 0 ).should ==
|
24
|
-
Time.new( 2013, 12, 13, 12 )
|
25
|
-
Reckon::DateColumn.new( ["01/14/2013"] ).for( 0 ).should ==
|
26
|
-
Time.new( 2013, 01, 14, 12 )
|
27
|
-
Reckon::DateColumn.new( ["13/12/2013", "21/11/2013"] ).for( 1 ).should ==
|
28
|
-
Time.new( 2013, 11, 21, 12 )
|
23
|
+
Reckon::DateColumn.new( ["13/12/2013"] ).for( 0 ).should ==
|
24
|
+
Time.new( 2013, 12, 13, 12 )
|
25
|
+
Reckon::DateColumn.new( ["01/14/2013"] ).for( 0 ).should ==
|
26
|
+
Time.new( 2013, 01, 14, 12 )
|
27
|
+
Reckon::DateColumn.new( ["13/12/2013", "21/11/2013"] ).for( 1 ).should ==
|
28
|
+
Time.new( 2013, 11, 21, 12 )
|
29
|
+
Reckon::DateColumn.new( ["2013-11-21"] ).for( 0 ).should ==
|
30
|
+
Time.new( 2013, 11, 21, 12 )
|
31
|
+
|
29
32
|
end
|
30
33
|
|
31
34
|
it "should correctly use endian_precedence" do
|
32
|
-
Reckon::DateColumn.new( ["01/02/2013", "01/14/2013"] ).for(0).should ==
|
35
|
+
Reckon::DateColumn.new( ["01/02/2013", "01/14/2013"] ).for(0).should ==
|
33
36
|
Time.new( 2013, 01, 02, 12 )
|
34
|
-
Reckon::DateColumn.new( ["01/02/2013", "14/01/2013"] ).for(0).should ==
|
37
|
+
Reckon::DateColumn.new( ["01/02/2013", "14/01/2013"] ).for(0).should ==
|
35
38
|
Time.new( 2013, 02, 01, 12 )
|
36
39
|
end
|
37
40
|
end
|
38
41
|
end
|
39
|
-
|
42
|
+
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: reckon
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Cantino
|
@@ -9,76 +9,76 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2015-06-05 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rspec
|
16
16
|
requirement: !ruby/object:Gem::Requirement
|
17
17
|
requirements:
|
18
|
-
- -
|
18
|
+
- - ">="
|
19
19
|
- !ruby/object:Gem::Version
|
20
20
|
version: 1.2.9
|
21
21
|
type: :development
|
22
22
|
prerelease: false
|
23
23
|
version_requirements: !ruby/object:Gem::Requirement
|
24
24
|
requirements:
|
25
|
-
- -
|
25
|
+
- - ">="
|
26
26
|
- !ruby/object:Gem::Version
|
27
27
|
version: 1.2.9
|
28
28
|
- !ruby/object:Gem::Dependency
|
29
29
|
name: fastercsv
|
30
30
|
requirement: !ruby/object:Gem::Requirement
|
31
31
|
requirements:
|
32
|
-
- -
|
32
|
+
- - ">="
|
33
33
|
- !ruby/object:Gem::Version
|
34
34
|
version: 1.5.1
|
35
35
|
type: :runtime
|
36
36
|
prerelease: false
|
37
37
|
version_requirements: !ruby/object:Gem::Requirement
|
38
38
|
requirements:
|
39
|
-
- -
|
39
|
+
- - ">="
|
40
40
|
- !ruby/object:Gem::Version
|
41
41
|
version: 1.5.1
|
42
42
|
- !ruby/object:Gem::Dependency
|
43
43
|
name: chronic
|
44
44
|
requirement: !ruby/object:Gem::Requirement
|
45
45
|
requirements:
|
46
|
-
- -
|
46
|
+
- - ">="
|
47
47
|
- !ruby/object:Gem::Version
|
48
48
|
version: 0.3.0
|
49
49
|
type: :runtime
|
50
50
|
prerelease: false
|
51
51
|
version_requirements: !ruby/object:Gem::Requirement
|
52
52
|
requirements:
|
53
|
-
- -
|
53
|
+
- - ">="
|
54
54
|
- !ruby/object:Gem::Version
|
55
55
|
version: 0.3.0
|
56
56
|
- !ruby/object:Gem::Dependency
|
57
57
|
name: highline
|
58
58
|
requirement: !ruby/object:Gem::Requirement
|
59
59
|
requirements:
|
60
|
-
- -
|
60
|
+
- - ">="
|
61
61
|
- !ruby/object:Gem::Version
|
62
62
|
version: 1.5.2
|
63
63
|
type: :runtime
|
64
64
|
prerelease: false
|
65
65
|
version_requirements: !ruby/object:Gem::Requirement
|
66
66
|
requirements:
|
67
|
-
- -
|
67
|
+
- - ">="
|
68
68
|
- !ruby/object:Gem::Version
|
69
69
|
version: 1.5.2
|
70
70
|
- !ruby/object:Gem::Dependency
|
71
71
|
name: terminal-table
|
72
72
|
requirement: !ruby/object:Gem::Requirement
|
73
73
|
requirements:
|
74
|
-
- -
|
74
|
+
- - ">="
|
75
75
|
- !ruby/object:Gem::Version
|
76
76
|
version: 1.4.2
|
77
77
|
type: :runtime
|
78
78
|
prerelease: false
|
79
79
|
version_requirements: !ruby/object:Gem::Requirement
|
80
80
|
requirements:
|
81
|
-
- -
|
81
|
+
- - ">="
|
82
82
|
- !ruby/object:Gem::Version
|
83
83
|
version: 1.4.2
|
84
84
|
description: Reckon automagically converts CSV files for use with the command-line
|
@@ -90,11 +90,11 @@ executables:
|
|
90
90
|
extensions: []
|
91
91
|
extra_rdoc_files: []
|
92
92
|
files:
|
93
|
-
- .document
|
94
|
-
- .gitignore
|
95
|
-
- .ruby-gemset
|
96
|
-
- .ruby-version
|
97
|
-
- .travis.yml
|
93
|
+
- ".document"
|
94
|
+
- ".gitignore"
|
95
|
+
- ".ruby-gemset"
|
96
|
+
- ".ruby-version"
|
97
|
+
- ".travis.yml"
|
98
98
|
- CHANGES.md
|
99
99
|
- Gemfile
|
100
100
|
- Gemfile.lock
|
@@ -109,6 +109,7 @@ files:
|
|
109
109
|
- lib/reckon/money.rb
|
110
110
|
- reckon.gemspec
|
111
111
|
- spec/data_fixtures/extratofake.csv
|
112
|
+
- spec/data_fixtures/tokens.yaml
|
112
113
|
- spec/reckon/app_spec.rb
|
113
114
|
- spec/reckon/csv_parser_spec.rb
|
114
115
|
- spec/reckon/date_column_spec.rb
|
@@ -126,23 +127,24 @@ require_paths:
|
|
126
127
|
- lib
|
127
128
|
required_ruby_version: !ruby/object:Gem::Requirement
|
128
129
|
requirements:
|
129
|
-
- -
|
130
|
+
- - ">="
|
130
131
|
- !ruby/object:Gem::Version
|
131
132
|
version: '0'
|
132
133
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
133
134
|
requirements:
|
134
|
-
- -
|
135
|
+
- - ">="
|
135
136
|
- !ruby/object:Gem::Version
|
136
137
|
version: '0'
|
137
138
|
requirements: []
|
138
139
|
rubyforge_project:
|
139
|
-
rubygems_version: 2.
|
140
|
+
rubygems_version: 2.4.6
|
140
141
|
signing_key:
|
141
142
|
specification_version: 4
|
142
143
|
summary: Utility for interactively converting and labeling CSV files for the Ledger
|
143
144
|
accounting tool.
|
144
145
|
test_files:
|
145
146
|
- spec/data_fixtures/extratofake.csv
|
147
|
+
- spec/data_fixtures/tokens.yaml
|
146
148
|
- spec/reckon/app_spec.rb
|
147
149
|
- spec/reckon/csv_parser_spec.rb
|
148
150
|
- spec/reckon/date_column_spec.rb
|