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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 17af5e887336c4c5b8a010ea22ad169c3df1efdd
4
- data.tar.gz: 7b57480844bc08468e254b8b4633ff78e25a020e
3
+ metadata.gz: 701e85b9ec558657003c57d9d33744840fcdcb55
4
+ data.tar.gz: d343b374bf5375ec34a915a84b7a27724b53b995
5
5
  SHA512:
6
- metadata.gz: 93ac7ab7c77cd1b411efc86fd7b373c644301d26dc468751a11ab536f4389c2b212437354e7f105ced0705943ecf196a692f5453345c4ddc4e1771376e49d134
7
- data.tar.gz: e7fa2a91210cf228cb8b4f96789212f380e9723119c5d25d58a1b3115cde6b1a63cbe7f09adce4104e4a8b5a8480f8695cbe1702fcf3aa392e18dd8cdadabe46
6
+ metadata.gz: 8489408851bf979388afb2f0ed8da24d30d738b82c83116f1551d8d1f43aebf17f4aeb9ad05a835323ca73199568b74265a556bb25e05c5f4f4585508d4aa863
7
+ data.tar.gz: 1a6845f390988e12286716351029a015165fa5dc8230aa1d0021616aa4a4e108a8fea64b0249176698a0c8edf8b4e4b4991517028e6a156a2dfecfd04fe067a3
@@ -1 +1 @@
1
- ruby-2.0.0-p353
1
+ ruby-2.2.0
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- reckon (0.3.10)
4
+ reckon (0.4.0)
5
5
  chronic (>= 0.3.0)
6
6
  fastercsv (>= 1.5.1)
7
7
  highline (>= 1.5.2)
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.
@@ -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[:existing_ledger_file]
37
- fail "#{options[:existing_ledger_file]} doesn't exist!" unless File.exists?(options[:existing_ledger_file])
38
- ledger_data = File.read(options[:existing_ledger_file])
39
- learn_from(ledger_data)
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
- puts Terminal::Table.new(:rows => [ [ row[:pretty_date], row[:pretty_money], row[:description] ] ])
80
+ interactive_output Terminal::Table.new(:rows => [ [ row[:pretty_date], row[:pretty_money], row[:description] ] ])
61
81
 
62
82
  if already_seen?(row)
63
- puts "NOTE: This row is very similar to a previous one!"
83
+ interactive_output "NOTE: This row is very similar to a previous one!"
64
84
  if !seen_anything_new
65
- puts "Skipping..."
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
- out_of_account = ask("Which account provided this income? ([account]/[q]uit/[s]kip) ") { |q| q.default = guess_account(row) }
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
- puts "Skipping"
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
- into_account = ask("To which account did this money go? ([account]/[q]uit/[s]kip) ") { |q| q.default = guess_account(row) }
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
- puts "Skipping"
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
- puts "Exiting."
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
- def guess_account(row)
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
- account_vectors.first && account_vectors.first[:account]
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
- puts output
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 e", "Specify an encoding for the CSV file") do |e|
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
@@ -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+[:\/\.]\d+[:\/\.]\d+([ :]\d+[:\/\.]\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]
@@ -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
@@ -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.3.10"
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}
@@ -0,0 +1,14 @@
1
+ Income:
2
+ Salary:
3
+ - 'LÖN'
4
+ - 'Salary'
5
+ Expenses:
6
+ Bank:
7
+ - 'Comission'
8
+ - 'MasterCard'
9
+ Rent:
10
+ - '0011223344' # Landlord bank number
11
+ Books:
12
+ - 'Book'
13
+ '[Internal:Transfer]': # Virtual account
14
+ - '4433221100' # Your own account number
@@ -6,27 +6,77 @@ require 'rubygems'
6
6
  require 'reckon'
7
7
 
8
8
  describe Reckon::App do
9
- before do
10
- @chase = Reckon::App.new(:string => BANK_CSV)
11
- @rows = []
12
- @chase.each_row_backwards { |row| @rows.push( row ) }
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
- describe "each_row_backwards" do
16
- it "should return rows with hashes" do
17
- @rows[0][:pretty_date].should == "2009/12/10"
18
- @rows[0][:pretty_money].should == " $2105.00"
19
- @rows[0][:description].should == "CREDIT; Some Company vendorpymt PPD ID: 5KL3832735"
20
- @rows[1][:pretty_date].should == "2009/12/11"
21
- @rows[1][:pretty_money].should == "-$116.22"
22
- @rows[1][:description].should == "CREDIT; PAYPAL TRANSFER PPD ID: PAYPALSDSL"
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],"CHECK 2656",-20.00
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.3.10
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: 2014-08-16 00:00:00.000000000 Z
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.1.11
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