reckon 0.3.10 → 0.4.0
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/.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
|