total_recall 0.2.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 162939627043b1aea142c3c8a6dedc9c0557801a
4
+ data.tar.gz: fd307f5b53406336a46225d36b9138d11d228bfd
5
+ SHA512:
6
+ metadata.gz: a78dcb6fa9e0d5d533dcc8bef4d34769ced4f8809c8f074158c4a3c3de2cd45a53cacd2364d0e0b941330cc7fccf35c8f443106eada6f6dfa7c2b48cc555db3f
7
+ data.tar.gz: 1ac9db0b80f7c7e08be72ccadb399d1eb209b56bbfa40bafcc0fa3c7037c691e3e329c6a9e4147bcda57b6893058a8b2f8a1b10459ebb31ca7fb428ec701a80f
@@ -0,0 +1,3 @@
1
+ rvm:
2
+ - 1.9.3
3
+ - 2.1
data/Gemfile CHANGED
@@ -2,5 +2,3 @@ source 'http://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in total_recall.gemspec
4
4
  gemspec
5
-
6
- gem 'rake'
data/README.md CHANGED
@@ -1,39 +1,100 @@
1
- TotalRecall
2
- ============
1
+ # TotalRecall [![travis](https://secure.travis-ci.org/eval/total_recall.png?branch=master)](https://secure.travis-ci.org/#!/eval/total_recall)
3
2
 
4
- Turn your bank record csv's into a [Ledger](http://ledger-cli.org/3.0/doc/ledger3.html) journal.
3
+ Turn **any** csv-file into a [Ledger](http://ledger-cli.org/) journal.
5
4
 
6
- Usage
7
- ------------
5
+ ## Quickstart
8
6
 
9
- $ total_recall
10
- Tasks:
11
- total_recall help [TASK] # Describe available tasks or one specific task
12
- total_recall ledger -i, --input=INPUT -p, --parser=PARSER # Convert input to ledger-transactions
13
- total_recall parse -i, --input=INPUT -p, --parser=PARSER # Parses input-file and prints it
7
+ ### Generate a sample config
14
8
 
15
- # typically you would do:
16
- $ total_recall ledger -i abn.csv -p abn > abn.dat
9
+ ```bash
10
+ total_recall sample
11
+ # An annotated config 'sample.yml' will be written to CWD.
12
+ ```
17
13
 
18
- Develop
19
- ------------
20
-
21
- $ git clone git://github.com/eval/total_recall.git
22
- $ cd total_recall
23
- $ bundle
24
- $ bundle exec rake spec
14
+ ### Generate a ledger
15
+
16
+ The sample config contains 2 transactions.
17
+ Let's get 'em in a journal:
18
+
19
+ ```bash
20
+ total_recall ledger -c sample.yml > sample.dat
21
+ # What account provides these transactions? |Assets:Checking|
22
+ #
23
+ # +------------+-----+----------+
24
+ # | 03.11.2013 | Foo | 1.638,00 |
25
+ # +------------+-----+----------+
26
+ # To what account did the money go?
27
+ # Expenses:Foo
28
+ # +------------+-----+---------+
29
+ # | 03.11.2013 | Bar | -492,93 |
30
+ # +------------+-----+---------+
31
+ # To what account did the money go?
32
+ # Expenses:Bar
33
+ ```
34
+ Now verify the ledger file:
35
+
36
+ ```bash
37
+ ledger -f sample.dat balance
38
+
39
+ # $ -1.145,07 Assets:Checking
40
+ # $ 1.145,07 Expenses
41
+ # $ -492,93 Bar
42
+ # $ 1.638,00 Foo
43
+ #--------------------
44
+ # 0
45
+ ```
46
+
47
+ ### Now what?
48
+
49
+ May I suggest:
50
+ * read `sample.yml`
51
+
52
+ It explains what options are available.
53
+ * put your own csv in `sample.yml` and adjust the context
54
+
55
+ ## Usage
25
56
 
26
- Author
27
- ------
57
+ ```bash
58
+ total_recall
59
+
60
+ # Commands:
61
+ # total_recall help [COMMAND] # Describe available commands or one specific command
62
+ # total_recall init NAME # Generate a minimal config NAME.yml
63
+ # total_recall ledger -c, --config=CONFIG # Convert the config to a ledger
64
+ # total_recall sample # Generate an annotated config
65
+
66
+ # typically you would do:
67
+ total_recall init my-bank
68
+
69
+ # fiddle with the settings in 'my-bank.yml' and test-run it:
70
+ total_recal ledger -c my-bank.yml
71
+ # to skip prompts just provide dummy-data:
72
+ yes 'Dummy' | total_recal ledger -c my-bank.yml
73
+
74
+ # export it to a journal:
75
+ total_recall ledger -c my-bank.yml > my-bank.dat
76
+
77
+ # verify correctness with ledger:
78
+ ledger -f my-bank.dat bal
79
+ ```
80
+
81
+ ## Develop
82
+
83
+ ```bash
84
+ git clone git://github.com/eval/total_recall.git
85
+ cd total_recall
86
+ bundle
87
+ bundle exec rake spec
88
+ ```
89
+ ## Author
28
90
 
29
91
  Gert Goet (eval) :: gert@thinkcreate.nl :: @gertgoet
30
92
 
31
- License
32
- ------
93
+ ## License
33
94
 
34
95
  (The MIT license)
35
96
 
36
- Copyright (c) 2011 Gert Goet, ThinkCreate
97
+ Copyright (c) 2014 Gert Goet, ThinkCreate
37
98
 
38
99
  Permission is hereby granted, free of charge, to any person obtaining
39
100
  a copy of this software and associated documentation files (the
@@ -53,3 +114,4 @@ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
53
114
  LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
54
115
  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
55
116
  WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
117
+
@@ -1,323 +1,162 @@
1
- require "total_recall/version"
1
+ require 'yaml'
2
2
  require "thor"
3
3
  require "mustache"
4
+ require 'csv'
4
5
 
5
6
  module TotalRecall
6
- module ParseStrategy
7
- class Ing
8
- require 'time'
9
-
10
- # Expected: Hash with:
11
- #:amount => Float,
12
- #:currency => String,
13
- #:description => String,
14
- #:date => Date
15
- def parse_row(row)
16
- amount = row[6].sub(/,/,'.').to_f
17
- {
18
- :amount => (row[5] == 'Bij' ? amount : -amount),
19
- :date => Date.parse(row[0]),
20
- :description => [row[1], row[8]].map{|i| i.strip.gsub(/\s+/, ' ')}.join(' '),
21
- :currency => 'EUR'
22
- }
23
- end
24
-
25
-
26
- def self.options
27
- {:col_sep => ",", :headers => true}
28
- end
29
-
30
- def options
31
- self.class.options
32
- end
33
- end # /Ing
34
-
35
- class Abn
36
- require 'time'
37
-
38
- # Expected: Hash with:
39
- #:amount => Float,
40
- #:currency => String,
41
- #:description => String,
42
- #:date => Date
43
- def parse_row(row)
44
- {
45
- :amount => row[6].sub(/,/,'.').to_f,
46
- :date => Date.parse(row[2]),
47
- :description => row[7].strip.gsub(/\s+/,' '),
48
- :currency => row[1]
49
- }
50
- end
51
-
52
- def self.options
53
- {:col_sep => "\t"}
54
- end
55
-
56
- def options
57
- self.class.options
58
- end
59
- end # /Abn
60
-
61
- class AbnCC
62
- require 'time'
7
+ class Helper
8
+ require 'highline/import'
9
+ require "terminal-table"
63
10
 
64
- # Expected: Hash with:
65
- #:amount => Float,
66
- #:currency => String,
67
- #:description => String,
68
- #:date => Date
69
- def parse_row(row)
70
- amount = row[2].sub(/,/,'.').to_f
71
- {
72
- :amount => (row[8] == 'D' ? -amount : amount),
73
- :date => Date.parse(row[0]),
74
- :description => row[3],
75
- :currency => 'EUR'
76
- }
77
- end
11
+ attr_reader :config
12
+ attr_accessor :row
78
13
 
79
- def self.options
80
- {:col_sep => ",", :headers => true}
81
- end
14
+ def initialize(config = {})
15
+ @config = config
16
+ end
82
17
 
83
- def options
84
- self.class.options
85
- end
86
- end # /AbnCC
87
- end # /ParseStrategy
18
+ def with_row(row, &block)
19
+ @row = row
20
+ instance_eval(&block)
21
+ ensure
22
+ @row = nil
23
+ end
88
24
 
89
- class Ledger < Mustache
90
- attr_reader :default_account, :options
25
+ def highline
26
+ @highline ||= HighLine.new($stdin, $stderr)
27
+ end
91
28
 
92
- def initialize(default_account, transactions, options={})
93
- @options = {:max_account_width => 80, :decimal_mark => ','}.merge(options)
94
- @default_account = default_account
95
- @_transactions = transactions
29
+ def ask(question, &block)
30
+ highline.ask(question, &block)
96
31
  end
97
32
 
98
- def transactions
99
- @transactions ||= begin
100
- @_transactions.map do |i|
101
- res = {}
102
- res[:to], res[:from] = (i[:amount] > 0 ? [default_account, i[:account]] : [i[:account], default_account])
103
- if i[:tags].any?
104
- tagsline = "\n ; :%s:" % (i[:tags] * ':')
105
- tagskey = i[:amount] > 0 ? :from_tags : :to_tags
106
- res[tagskey] = tagsline
107
- end
108
- res[:amount] = fmt_amount(i[:amount].abs)
109
- res[:description], res[:date], res[:currency] = i[:description], i[:date], i[:currency]
110
- res[:spacer] = " " * (options[:max_account_width] - res[:to].size)
111
- res
112
- end
33
+ # Prompts the user for an account-name.
34
+ #
35
+ # @param question [String] the question
36
+ # @param options [Hash]
37
+ # @option options [String] :default (nil) account-name that will be used
38
+ # if no input is provided.
39
+ #
40
+ # @example
41
+ # ask_account("What account did this money come from?",
42
+ # default: 'Expenses:Various')
43
+ # What account did this money come from? |Expenses:Various|
44
+ # _
45
+ #
46
+ # @return [String] the account-name
47
+ def ask_account(question, options = {})
48
+ options = { default: nil }.merge(options)
49
+ highline.ask(question) do |q|
50
+ q.default = options[:default] if options[:default]
113
51
  end
114
52
  end
115
53
 
116
-
117
- def self.template;<<-TEMPLATE
118
- {{# transactions}}
119
- {{date}} {{description}}
120
- {{to}} {{spacer}}{{currency}} {{amount}}{{to_tags}}
121
- {{from}}{{from_tags}}
122
-
123
- {{/ transactions}}
124
- TEMPLATE
54
+ def render_row(options = {})
55
+ options = { columns: [] }.merge(options)
56
+ _row = options[:columns].map{|i| row[i] }
57
+ $stderr.puts Terminal::Table.new(rows: [ _row ])
125
58
  end
126
-
127
- protected
128
- def fmt_amount(amount)
129
- ("%10.2f" % amount).split('.') * options[:decimal_mark]
130
- end
131
59
  end
132
60
 
133
- class AccountGuesser
134
- attr_reader :accounts, :tokens
61
+ class Config
62
+ YAML::add_builtin_type('proc') {|_, val| eval("proc{ #{val} }") }
135
63
 
136
- def initialize
137
- @accounts = {}
138
- @tokens = {}
64
+ def initialize(options = {})
65
+ options = {file: 'total_recall.yml'}.merge(options)
66
+ @config_file = File.expand_path(options[:file])
139
67
  end
140
68
 
141
- # copied from reckon(https://github.com/iterationlabs/reckon)
142
- def guess(data)
143
- query_tokens = tokenize(data)
144
-
145
- search_vector = []
146
- account_vectors = {}
147
-
148
- query_tokens.each do |token|
149
- idf = Math.log((accounts.keys.length + 1) / ((tokens[token] || {}).keys.length.to_f + 1))
150
- tf = 1.0 / query_tokens.length.to_f
151
- search_vector << tf*idf
69
+ def config
70
+ @config ||= YAML.load_file(@config_file)
71
+ end
152
72
 
153
- accounts.each do |account, total_terms|
154
- tf = (tokens[token] && tokens[token][account]) ? tokens[token][account] / total_terms.to_f : 0
155
- account_vectors[account] ||= []
156
- account_vectors[account] << tf*idf
157
- end
158
- end
73
+ def csv_file
74
+ config[:csv][:file] &&
75
+ File.expand_path(config[:csv][:file], File.dirname(@config_file))
76
+ end
159
77
 
160
- # Should I normalize the vectors? Probably unnecessary due to tf-idf and short documents.
161
- account_vectors = account_vectors.to_a.map do |account, account_vector|
162
- { :cosine => (0...account_vector.length).to_a.inject(0) { |m, i| m + search_vector[i] * account_vector[i] },
163
- :account => account }
78
+ def csv
79
+ @csv ||= begin
80
+ csv_raw = csv_file ? File.read(csv_file) : config[:csv][:raw]
81
+ CSV.parse(csv_raw, config[:csv][:options] || {})
164
82
  end
165
-
166
- account_vectors.sort! {|a, b| b[:cosine] <=> a[:cosine] }
167
- account_vectors.first && account_vectors.first[:account]
168
83
  end
169
84
 
170
- # copied from reckon(https://github.com/iterationlabs/reckon)
171
- def learn(account, data)
172
- accounts[account] ||= 0
173
- tokenize(data).each do |token|
174
- tokens[token] ||= {}
175
- tokens[token][account] ||= 0
176
- tokens[token][account] += 1
177
- accounts[account] += 1
178
- end
85
+ def template_file
86
+ config[:template][:file] &&
87
+ File.expand_path(config[:template][:file], File.dirname(@config_file))
179
88
  end
180
89
 
181
- protected
182
- def tokenize(str)
183
- str.downcase.split(/[\s\-]/)
90
+ def template
91
+ @template ||= begin
92
+ template_file ? File.read(template_file) : config[:template][:raw]
184
93
  end
185
- end
186
-
187
- class BankParser
188
- require 'csv'
189
- attr_reader :strategy
190
-
191
- def initialize(options={})
192
- @strategy = options[:strategy]
193
94
  end
194
95
 
195
- # Parses csv content and returns array of hashes.
196
- #
197
- # @example
198
- # parser = TotalRecall::BankParser.new(:strategy => TotalRecall::ParseStrategy::Some.new)
199
- # parser.parse("12.1\n1.99") #=> [{:amount => 12.1}, {:amount => 1.99}]
200
- #
201
- # @param [String] str content of csv to parse.
202
- # @return [Array<Hash>]
203
- def parse(str, options={})
204
- options = strategy.options.merge(options)
205
-
206
- result = []
207
- CSV.parse(str, options){|row| result << strategy.parse_row(row)}
208
- result
96
+ def session
97
+ @session ||= Helper.new(config)
209
98
  end
210
- end #/BankParser
211
-
212
99
 
213
- class Cli < Thor
214
- require "terminal-table"
215
- require "highline/import"
216
- require "bayes_motel"
217
-
218
- no_tasks do
219
- def self.strategies
220
- {
221
- 'abn' => TotalRecall::ParseStrategy::Abn,
222
- 'ing' => TotalRecall::ParseStrategy::Ing,
223
- 'abncc' => TotalRecall::ParseStrategy::AbnCC
224
- }
225
- end
100
+ def context
101
+ @context ||= config[:context].merge(transactions: transactions)
102
+ end
226
103
 
227
- def highline
228
- @highline ||= HighLine.new($stdin, $stderr)
104
+ def transactions
105
+ @transactions ||= begin
106
+ csv.each_with_object([]) do |row, result|
107
+ result << transaction_defaults.merge(transactions_config).each_with_object({}) do |(k,v), cfg|
108
+ next if k[/^__/]
109
+ cfg[k] = v.respond_to?(:call) ? session.with_row(row, &v) : v
110
+ end
111
+ end
229
112
  end
113
+ end
230
114
 
231
- def strategies
232
- self.class.strategies
115
+ def transaction_defaults
116
+ @transaction_defaults ||= begin
117
+ defaults = transactions_config[:__defaults__] || {}
118
+ defaults.each_with_object({}) do |(k,v), result|
119
+ result[k] = v.respond_to?(:call) ? session.with_row(nil, &v) : v
120
+ end
233
121
  end
122
+ end
234
123
 
235
- def parser(strategy)
236
- TotalRecall::BankParser.new(:strategy => strategy)
237
- end
124
+ def transactions_config
125
+ config[:context][:transactions]
238
126
  end
239
127
 
240
- desc "ledger", "Convert input to ledger-transactions"
241
- method_option :input, :aliases => "-i", :desc => "CSV file to use for input", :required => true
242
- method_option :parser, :aliases => "-p", :desc => "Parser to use (one of #{strategies.keys.inspect})", :required => true
243
128
  def ledger
244
- strategy = strategies[options[:parser]]
245
- file_contents = File.read(options[:input])
246
- rows = parser(strategy.new).parse(file_contents)
247
-
248
- guesser = AccountGuesser.new
249
-
250
- default_account = highline.ask("What is the account name of this bank account in Ledger?\n> ")
251
- transactions = [] # [{<row>, :account => 'Expenses:Car'}, ...]
252
- tags = []
253
- start_from = 0
254
- begin
255
- rows.each_with_index do |row, ix|
256
- if start_from && start_from != ix
257
- next
258
- elsif start_from
259
- start_from = nil
260
- end
261
-
262
- guessed = guesser.guess(row[:description])
263
- question = row[:amount] > 0 ? "What account provided this income?" : "To which account did this money go?"
129
+ Mustache.render(template, context)
130
+ end
131
+ end
264
132
 
265
- $stderr.puts Terminal::Table.new(:rows => [ [ row[:date], row[:amount], row[:description] ] ])
266
- account = highline.ask("#{question} (#{guessed}) or [d]one, [s]kip, [p]revious\n> ")
267
- account = account.empty? ? guessed : account # defaults not working
268
- case account
269
- when 'd'
270
- break
271
- when 's'
272
- next
273
- when 'p'
274
- transactions.pop
275
- start_from = [0, (ix - 1)].max
276
- raise
277
- end
133
+ class Cli < Thor
134
+ require 'total_recall/version'
278
135
 
279
- transaction_tags = []
280
- loop do
281
- tag = highline.choose do |menu|
282
- menu.shell = true
283
- menu.prompt = "Add tags? "
284
- menu.prompt << "(#{transaction_tags.join(',')})" if transaction_tags.any?
136
+ include Thor::Actions
137
+ source_root File.expand_path('../total_recall/templates', __FILE__)
285
138
 
286
- menu.choices('Done'){ nil }
287
- menu.choices(*tags - transaction_tags)
288
- menu.choice('New...'){ newtag = highline.ask('Tag: ');tags << newtag; newtag }
289
- end
290
- break unless tag
291
- transaction_tags << tag
292
- end
139
+ desc "ledger", "Convert the config to a ledger"
140
+ method_option :config, :aliases => "-c", :desc => "Config file", :required => true
141
+ def ledger
142
+ puts TotalRecall::Config.new(file: File.expand_path(options[:config])).ledger
143
+ end
293
144
 
294
- guesser.learn(account, row[:description])
295
- transactions << row.merge(:account => account, :tags => transaction_tags)
296
- end
297
- rescue
298
- $stderr.puts "Let's do that again"
299
- retry
300
- end
145
+ desc "sample", "Generate an annotated config"
146
+ def sample
147
+ @version = TotalRecall::VERSION
148
+ template("sample.yml.tt")
301
149
 
302
- puts Ledger.new(default_account, transactions).render
150
+ say "Now run '#{$0} ledger -c sample.yml' to generate the ledger"
303
151
  end
304
152
 
305
- desc "parse", "Parses input-file and prints it"
306
- method_option :input, :aliases => "-i", :desc => "CSV file to use for input", :required => true
307
- method_option :parser, :aliases => "-p", :desc => "Parser to use (one of #{strategies.keys.join(',')})", :required => true
308
- def parse
309
- strategy = strategies[options[:parser]]
310
- file_contents = File.read(options[:input])
311
- data = parser(strategy.new).parse(file_contents)
153
+ desc "init NAME", "Generate a minimal config NAME.yml"
154
+ def init(name = "total_recall")
155
+ destination = name + ".yml"
312
156
 
313
- table = Terminal::Table.new do |t|
314
- t.headings = 'Date', 'Amount', 'Description'
315
- data.each do |row|
316
- t << [ row[:date], row[:amount], row[:description] ]
317
- end
318
- end
319
- puts table
157
+ @version = TotalRecall::VERSION
158
+ @name = name
159
+ template("simple.yml.tt", destination)
320
160
  end
321
-
322
161
  end
323
162
  end
@@ -0,0 +1,79 @@
1
+ ---
2
+ :total_recall:
3
+ # The version of total_recall that generated this file
4
+ :version: <%= @version %>
5
+ :template:
6
+ # 1). Provide the [mustache-template](https://github.com/defunkt/mustache)
7
+ # for your ledger.
8
+ #
9
+ # Either assign a template to :raw: or point to a file via :file:.
10
+ # The location of :file: is relative to this file.
11
+ # :file: takes precedence over :raw:
12
+ #
13
+ # The template MUST contain a section `transactions`.
14
+ #
15
+ #:file: ledger.mustache
16
+ :raw: |-
17
+ ; -*- ledger -*-¬
18
+ ; Generated by total_recall at {{generated_at}}
19
+ --decimal-comma
20
+
21
+ {{# transactions}}
22
+ {{date}} {{{description}}}
23
+ {{to}} {{currency}} {{amount}}
24
+ {{from}}
25
+
26
+ {{/ transactions}}
27
+
28
+ :csv:
29
+ # 2). Provide the csv
30
+ #
31
+ # Either assign csv to :raw: or point to a csv-file via :file:.
32
+ # :raw: is ideal for experimenting with the csv-format.
33
+ #
34
+ # The location of :file: is relative to this file.
35
+ # :file: takes precedence over :raw:
36
+ #
37
+ #:file: sample.csv
38
+ :raw: |-
39
+ "03.11.2013";"Foo";"04.11.2013";"1.638,00";""
40
+ "03.11.2013";"Bar";"04.11.2013";"-492,93";""
41
+ :options:
42
+ # Any option accepted by CSV#new (http://www.ruby-doc.org/stdlib-2.1.1/libdoc/csv/rdoc/CSV.html#method-c-new).
43
+ :col_sep: ";"
44
+ :headers: false
45
+
46
+ :context:
47
+ # 3). Define the context
48
+ #
49
+ # The context is in essence the dictionary that will be applied to the template.
50
+ # The value for transactions will be expanded to an array, one for every line in the csv.
51
+ :transactions:
52
+ # transactions have defaults...
53
+ :__defaults__:
54
+ :from: !!proc |
55
+ ask_account("What account provides these transactions?",
56
+ default: 'Assets:Checking')
57
+ :currency: $
58
+ # ...and fields that vary per transaction.
59
+ # Helper method exist such as `row`, `ask_account` (see `TotalRecall::Helper` for more).
60
+ :date: !!proc |
61
+ # For weird date formats consider using Date#strptime (http://www.ruby-doc.org/stdlib-2.1.1/libdoc/date/rdoc/Date.html#method-c-strptime)
62
+ Date.parse(row[0])
63
+ :description: !!proc row[1]
64
+ :amount: !!proc row[3]
65
+ :to: !!proc |
66
+ # show the row and let the user decide
67
+ render_row(columns: [0, 1, 3])
68
+ ask_account("To what account did the money go?")
69
+ :generated_at: !!proc Time.now
70
+
71
+ # You can add any other data needed:
72
+ # :ticker_sybols:
73
+ # "Apple": AAPL
74
+ # "Google Inc.": GOOG
75
+ #
76
+ # Example usage:
77
+ # :context:
78
+ # :transactions:
79
+ # :symbol: !!proc config[:ticker_symbols][row[1]]
@@ -0,0 +1,35 @@
1
+ ---
2
+ :total_recall:
3
+ :version: <%= @version %>
4
+ :template:
5
+ :raw: |-
6
+ ; -*- ledger -*-¬
7
+
8
+ {{# transactions}}
9
+ {{date}} {{{description}}}
10
+ {{to}} {{currency}} {{amount}}
11
+ {{from}}
12
+
13
+ {{/ transactions}}
14
+
15
+ :csv:
16
+ #:file: <%= @name %>.csv
17
+ :raw: |-
18
+ "03.11.2013";"Foo";"04.11.2013";"1.638,00";""
19
+ "03.11.2013";"Bar";"04.11.2013";"-492,93";""
20
+ :options:
21
+ :col_sep: ";"
22
+ :headers: false
23
+
24
+ :context:
25
+ :transactions:
26
+ :__defaults__:
27
+ :from: !!proc |
28
+ ask_account("What account provides these transactions?", default: 'Assets:Checking')
29
+ :currency: $
30
+ :date: !!proc Date.parse(row[0])
31
+ :description: !!proc row[1]
32
+ :amount: !!proc row[3]
33
+ :to: !!proc |
34
+ render_row(columns: [0, 1, 3, 4])
35
+ ask_account("To what account did the money go?")
@@ -1,3 +1,3 @@
1
1
  module TotalRecall
2
- VERSION = "0.2.0"
2
+ VERSION = "0.4.0"
3
3
  end
@@ -4,12 +4,4 @@ Bundler.setup
4
4
 
5
5
  $:.unshift File.expand_path("../../lib", __FILE__)
6
6
  require "total_recall"
7
-
8
- # fullpath to csv-file
9
- def fixture_pathname(str)
10
- Pathname(%W(spec fixtures #{str}.csv).join(File::SEPARATOR)).realpath
11
- end
12
-
13
- def fixture_contents(str)
14
- File.read(fixture_pathname(str))
15
- end
7
+ require 'fakefs/spec_helpers'
@@ -0,0 +1,176 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe TotalRecall::Config do
4
+ include FakeFS::SpecHelpers
5
+
6
+ def stubbed_file(path, content)
7
+ # SOURCE http://edgeapi.rubyonrails.org/classes/String.html#method-i-strip_heredoc
8
+ indent = content.scan(/^[ \t]*(?=\S)/).min.size rescue 0
9
+ content = content.gsub(/^[ \t]{#{indent}}/, '')
10
+
11
+ FakeFS do
12
+ File.open(path, 'w'){|f| f.print content }
13
+ end
14
+ end
15
+
16
+ def instance_with_config(config, options = {})
17
+ options = {file: 'config.yml'}.merge(options)
18
+ stubbed_file(options[:file], config)
19
+ described_class.new(options)
20
+ end
21
+
22
+ describe '#config' do
23
+ it 'yields the config as hash' do
24
+ instance = instance_with_config(<<-CONFIG)
25
+ :csv:
26
+ :raw: Some csv
27
+ :a: 1
28
+ CONFIG
29
+
30
+ expect(instance.config).to eql({csv: { raw: 'Some csv'}, a: 1})
31
+ end
32
+ end
33
+
34
+ describe '#csv' do
35
+ it 'yields csv assigned to :raw' do
36
+ instance = instance_with_config(<<-CONFIG)
37
+ :csv:
38
+ :raw: Some csv
39
+ CONFIG
40
+
41
+ expect(instance.csv).to eql(CSV.parse('Some csv'))
42
+ end
43
+
44
+ it 'yields csv from file :file' do
45
+ csv_file = stubbed_file('some.csv', 'Some csv')
46
+ instance = instance_with_config(<<-CONFIG)
47
+ :csv:
48
+ :file: some.csv
49
+ CONFIG
50
+
51
+ expect(instance.csv).to eql(CSV.parse('Some csv'))
52
+ end
53
+
54
+ it 'yields csv from :file when both :raw and :file are configured' do
55
+ csv_file = stubbed_file('some.csv', 'Some csv')
56
+ instance = instance_with_config(<<-CONFIG)
57
+ :csv:
58
+ :file: some.csv
59
+ :raw: Some raw csv
60
+ CONFIG
61
+
62
+ expect(instance.csv).to eql(CSV.parse('Some csv'))
63
+ end
64
+
65
+ specify 'csv-options are passed on to CSV#read' do
66
+ instance = instance_with_config(<<-CONFIG)
67
+ :csv:
68
+ :options:
69
+ :option1: true
70
+ CONFIG
71
+
72
+ expect(CSV).to receive(:parse).with(anything(), { option1: true })
73
+ instance.csv
74
+ end
75
+ end
76
+
77
+ describe '#template' do
78
+ it 'yields template assigned to :raw' do
79
+ instance = instance_with_config(<<-CONFIG)
80
+ :template:
81
+ :raw: |-
82
+ Raw template
83
+ here
84
+ CONFIG
85
+
86
+ expect(instance.template).to eql("Raw template\nhere")
87
+ end
88
+
89
+ it 'yields template from file :file' do
90
+ template_file = stubbed_file('template.mustache', 'File template')
91
+ instance = instance_with_config(<<-CONFIG)
92
+ :template:
93
+ :file: template.mustache
94
+ CONFIG
95
+
96
+ expect(instance.template).to eql('File template')
97
+ end
98
+
99
+ it 'yields template from :file when both :raw and :file are configured' do
100
+ template_file = stubbed_file('template.mustache', 'File template')
101
+ instance = instance_with_config(<<-CONFIG)
102
+ :template:
103
+ :file: template.mustache
104
+ :raw: Raw template
105
+ CONFIG
106
+
107
+ expect(instance.template).to eql('File template')
108
+ end
109
+ end
110
+
111
+ describe 'YAML types' do
112
+ it 'allows proc-types' do
113
+ instance = instance_with_config(<<-CONFIG)
114
+ :a: !!proc 1 + 1
115
+ :b: !!proc |
116
+ 1 + 1
117
+ CONFIG
118
+
119
+ expect(instance.config[:a].call).to eq 2
120
+ expect(instance.config[:b].call).to eq 2
121
+ end
122
+ end
123
+
124
+ describe '#context' do
125
+ it 'has a transaction per line of csv' do
126
+ instance = instance_with_config(<<-CONFIG)
127
+ :csv:
128
+ :raw: |-
129
+ 1
130
+ 1
131
+ :context:
132
+ :transactions:
133
+ :from: From
134
+ :to: !!proc 1 + 1
135
+ :amount: !!proc row[0]
136
+ CONFIG
137
+
138
+ transactions = instance.context[:transactions]
139
+ expect(transactions.size).to eq 2
140
+
141
+ transaction = transactions.first
142
+ expect(transaction).to match({from: 'From', to: 2, amount: "1"})
143
+ end
144
+
145
+ it 'adds defaults to every transaction' do
146
+ instance = instance_with_config(<<-CONFIG)
147
+ :csv:
148
+ :raw: |-
149
+ line 1
150
+ line 2
151
+ :context:
152
+ :transactions:
153
+ :__defaults__:
154
+ :default: !!proc 1
155
+ :from: From
156
+ CONFIG
157
+
158
+ transaction = instance.context[:transactions].first
159
+
160
+ expect(transaction).to match({from: 'From', default: 1})
161
+ end
162
+
163
+ it 'may contain any other settings' do
164
+ instance = instance_with_config(<<-CONFIG)
165
+ :csv:
166
+ :raw: some csv
167
+ :context:
168
+ :transactions:
169
+ :from: From
170
+ :a: 1
171
+ CONFIG
172
+
173
+ expect(instance.context).to match(transactions: [{from: 'From'}], a: 1)
174
+ end
175
+ end
176
+ end
@@ -1,24 +1,29 @@
1
- # -*- encoding: utf-8 -*-
2
- require File.expand_path('../lib/total_recall/version', __FILE__)
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'total_recall/version'
3
5
 
4
6
  Gem::Specification.new do |gem|
7
+ gem.name = "total_recall"
8
+ gem.version = TotalRecall::VERSION
5
9
  gem.authors = ["Gert Goet"]
6
10
  gem.email = ["gert@thinkcreate.nl"]
7
- gem.description = %q{Turn your bank records csv's into Ledger journals}
8
- gem.summary = %q{Turn your bank records csv's into Ledger journals}
9
- gem.homepage = ""
11
+ gem.description = %q{Turn any csv into a Ledger journal}
12
+ gem.summary = %q{Turn any csv into a Ledger journal}
13
+ gem.homepage = "https://github.com/eval/total_recall"
14
+ gem.license = "MIT"
10
15
 
11
- gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
12
- gem.files = `git ls-files`.split("\n")
13
- gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14
- gem.name = "total_recall"
16
+ gem.files = `git ls-files -z`.split("\x0")
17
+ gem.executables = gem.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
15
19
  gem.require_paths = ["lib"]
16
- gem.version = TotalRecall::VERSION
17
20
 
18
- gem.add_dependency('thor', '~> 0.14.6')
19
- gem.add_dependency('terminal-table', '~> 1.4.4')
20
- gem.add_dependency('highline', '~> 1.6.1')
21
- gem.add_dependency('bayes_motel', '~> 0.1.0')
22
- gem.add_dependency('mustache', '~> 0.99.4')
23
- gem.add_development_dependency('rspec', '~> 2.7.0')
21
+ gem.add_dependency 'thor', '~> 0.19'
22
+ gem.add_dependency 'terminal-table', '~> 1.4'
23
+ gem.add_dependency 'highline', '~> 1.6'
24
+ gem.add_dependency 'mustache', '~> 0.99'
25
+ gem.add_development_dependency "bundler", "~> 1.6"
26
+ gem.add_development_dependency "rake"
27
+ gem.add_development_dependency 'rspec', '~> 3.0.0'
28
+ gem.add_development_dependency 'fakefs'
24
29
  end
metadata CHANGED
@@ -1,83 +1,128 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: total_recall
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
5
- prerelease:
4
+ version: 0.4.0
6
5
  platform: ruby
7
6
  authors:
8
7
  - Gert Goet
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2012-04-15 00:00:00.000000000Z
11
+ date: 2014-06-04 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: thor
16
- requirement: &70134896727920 !ruby/object:Gem::Requirement
17
- none: false
15
+ requirement: !ruby/object:Gem::Requirement
18
16
  requirements:
19
- - - ~>
17
+ - - "~>"
20
18
  - !ruby/object:Gem::Version
21
- version: 0.14.6
19
+ version: '0.19'
22
20
  type: :runtime
23
21
  prerelease: false
24
- version_requirements: *70134896727920
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.19'
25
27
  - !ruby/object:Gem::Dependency
26
28
  name: terminal-table
27
- requirement: &70134896727220 !ruby/object:Gem::Requirement
28
- none: false
29
+ requirement: !ruby/object:Gem::Requirement
29
30
  requirements:
30
- - - ~>
31
+ - - "~>"
31
32
  - !ruby/object:Gem::Version
32
- version: 1.4.4
33
+ version: '1.4'
33
34
  type: :runtime
34
35
  prerelease: false
35
- version_requirements: *70134896727220
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.4'
36
41
  - !ruby/object:Gem::Dependency
37
42
  name: highline
38
- requirement: &70134896726600 !ruby/object:Gem::Requirement
39
- none: false
43
+ requirement: !ruby/object:Gem::Requirement
40
44
  requirements:
41
- - - ~>
45
+ - - "~>"
42
46
  - !ruby/object:Gem::Version
43
- version: 1.6.1
47
+ version: '1.6'
44
48
  type: :runtime
45
49
  prerelease: false
46
- version_requirements: *70134896726600
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.6'
47
55
  - !ruby/object:Gem::Dependency
48
- name: bayes_motel
49
- requirement: &70134896726040 !ruby/object:Gem::Requirement
50
- none: false
56
+ name: mustache
57
+ requirement: !ruby/object:Gem::Requirement
51
58
  requirements:
52
- - - ~>
59
+ - - "~>"
53
60
  - !ruby/object:Gem::Version
54
- version: 0.1.0
61
+ version: '0.99'
55
62
  type: :runtime
56
63
  prerelease: false
57
- version_requirements: *70134896726040
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.99'
58
69
  - !ruby/object:Gem::Dependency
59
- name: mustache
60
- requirement: &70134896725360 !ruby/object:Gem::Requirement
61
- none: false
70
+ name: bundler
71
+ requirement: !ruby/object:Gem::Requirement
62
72
  requirements:
63
- - - ~>
73
+ - - "~>"
64
74
  - !ruby/object:Gem::Version
65
- version: 0.99.4
66
- type: :runtime
75
+ version: '1.6'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.6'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
67
91
  prerelease: false
68
- version_requirements: *70134896725360
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
69
97
  - !ruby/object:Gem::Dependency
70
98
  name: rspec
71
- requirement: &70134896724820 !ruby/object:Gem::Requirement
72
- none: false
99
+ requirement: !ruby/object:Gem::Requirement
73
100
  requirements:
74
- - - ~>
101
+ - - "~>"
75
102
  - !ruby/object:Gem::Version
76
- version: 2.7.0
103
+ version: 3.0.0
77
104
  type: :development
78
105
  prerelease: false
79
- version_requirements: *70134896724820
80
- description: Turn your bank records csv's into Ledger journals
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 3.0.0
111
+ - !ruby/object:Gem::Dependency
112
+ name: fakefs
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description: Turn any csv into a Ledger journal
81
126
  email:
82
127
  - gert@thinkcreate.nl
83
128
  executables:
@@ -86,57 +131,46 @@ executables:
86
131
  extensions: []
87
132
  extra_rdoc_files: []
88
133
  files:
89
- - .gitignore
134
+ - ".gitignore"
135
+ - ".travis.yml"
90
136
  - Gemfile
91
137
  - README.md
92
138
  - Rakefile
93
139
  - bin/total_recall
94
140
  - bin/total_recall.rb
95
141
  - lib/total_recall.rb
142
+ - lib/total_recall/templates/sample.yml.tt
143
+ - lib/total_recall/templates/simple.yml.tt
96
144
  - lib/total_recall/version.rb
97
- - spec/fixtures/.keep
98
- - spec/fixtures/abn.csv
99
- - spec/fixtures/abncc.csv
100
- - spec/fixtures/ing.csv
101
145
  - spec/spec.opts
102
146
  - spec/spec_helper.rb
103
- - spec/total_recall/bank_parser_spec.rb
147
+ - spec/total_recall/total_recall_spec.rb
104
148
  - total_recall.gemspec
105
- homepage: ''
106
- licenses: []
149
+ homepage: https://github.com/eval/total_recall
150
+ licenses:
151
+ - MIT
152
+ metadata: {}
107
153
  post_install_message:
108
154
  rdoc_options: []
109
155
  require_paths:
110
156
  - lib
111
157
  required_ruby_version: !ruby/object:Gem::Requirement
112
- none: false
113
158
  requirements:
114
- - - ! '>='
159
+ - - ">="
115
160
  - !ruby/object:Gem::Version
116
161
  version: '0'
117
- segments:
118
- - 0
119
- hash: -1362278848564129285
120
162
  required_rubygems_version: !ruby/object:Gem::Requirement
121
- none: false
122
163
  requirements:
123
- - - ! '>='
164
+ - - ">="
124
165
  - !ruby/object:Gem::Version
125
166
  version: '0'
126
- segments:
127
- - 0
128
- hash: -1362278848564129285
129
167
  requirements: []
130
168
  rubyforge_project:
131
- rubygems_version: 1.8.10
169
+ rubygems_version: 2.2.2
132
170
  signing_key:
133
- specification_version: 3
134
- summary: Turn your bank records csv's into Ledger journals
171
+ specification_version: 4
172
+ summary: Turn any csv into a Ledger journal
135
173
  test_files:
136
- - spec/fixtures/.keep
137
- - spec/fixtures/abn.csv
138
- - spec/fixtures/abncc.csv
139
- - spec/fixtures/ing.csv
140
174
  - spec/spec.opts
141
175
  - spec/spec_helper.rb
142
- - spec/total_recall/bank_parser_spec.rb
176
+ - spec/total_recall/total_recall_spec.rb
File without changes
@@ -1,2 +0,0 @@
1
- 123456789 EUR 20101230 100,00 70,00 20101230 -30,00 BEA NR:12AA34 30.12.10/22.24 iCentre CS AMSTERDAM,PAS123
2
- 123456789 EUR 20101231 70,00 50,00 20101231 -20,00 BEA NR:12AA34 31.12.10/22.24 iCentre CS AMSTERDAM,PAS123
@@ -1,3 +0,0 @@
1
- Transactie Datum , Boekingsdatum , In rekening gebracht bedrag , Handelsnaam , Stad , Land , Postcode , Referentienummer , Debet/Credit , SICMCC Code
2
- 29/12/2011,30/12/2011,100.00,"Handelsnaam I",,,,,D,
3
- 30/12/2011,31/12/2011,200.00,"Handelsnaam II",,,,,D,
@@ -1,3 +0,0 @@
1
- "Datum","Naam / Omschrijving","Rekening","Tegenrekening","Code","Af Bij","Bedrag (EUR)","MutatieSoort","Mededelingen"
2
- "20101231","Omschrijving ","123456789","012345678","GT","Af","100,00","Internetbankieren","mededeling eèéêë"
3
- "20101230","Omschrijving ","123456789","012345678","GT","Af","200,00","Internetbankieren","mededeling "
@@ -1,57 +0,0 @@
1
- require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
-
3
- describe TotalRecall::BankParser do
4
- context '#parse' do
5
- subject{ described_class.new(:strategy => TotalRecall::ParseStrategy::Abn.new) }
6
-
7
- it "should return an Array" do
8
- subject.parse(fixture_contents('abn')).class.should == Array
9
- end
10
- end
11
- end
12
-
13
-
14
- shared_examples "a parser" do
15
- context '#parse_row' do
16
- it "should return a hash" do
17
- CSV.parse(fixture_contents(@fixture), subject.options).each do |i|
18
- parsed_row = subject.parse_row(i)
19
-
20
- parsed_row.class.should == Hash
21
- end
22
- end
23
-
24
- it "should return the correct keys and classes" do
25
- expected_keys_and_classes = {
26
- :amount => Float,
27
- :currency => String,
28
- :description => String,
29
- :date => Date
30
- }
31
-
32
- CSV.parse(fixture_contents(@fixture), subject.options).each do |i|
33
- parsed_row = subject.parse_row(i)
34
-
35
- parsed_row.keys.should =~ expected_keys_and_classes.keys
36
- expected_keys_and_classes.each do |key, klass|
37
- parsed_row.send(:[], key).class.should == klass
38
- end
39
- end
40
- end
41
- end
42
- end
43
-
44
- describe TotalRecall::ParseStrategy::Abn do
45
- before{ @fixture = 'abn' }
46
- it_behaves_like "a parser"
47
- end
48
-
49
- describe TotalRecall::ParseStrategy::Ing do
50
- before{ @fixture = 'ing' }
51
- it_behaves_like "a parser"
52
- end
53
-
54
- describe TotalRecall::ParseStrategy::AbnCC do
55
- before{ @fixture = 'abncc' }
56
- it_behaves_like "a parser"
57
- end