reckon 0.5.4 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +50 -0
  3. data/.gitignore +3 -0
  4. data/.ruby-version +1 -1
  5. data/CHANGELOG.md +73 -4
  6. data/Gemfile.lock +1 -45
  7. data/README.md +84 -33
  8. data/Rakefile +17 -1
  9. data/bin/build-new-version.sh +26 -0
  10. data/bin/reckon +6 -1
  11. data/lib/reckon.rb +2 -2
  12. data/lib/reckon/app.rb +140 -194
  13. data/lib/reckon/csv_parser.rb +2 -7
  14. data/lib/reckon/date_column.rb +10 -0
  15. data/lib/reckon/money.rb +48 -48
  16. data/lib/reckon/options.rb +149 -0
  17. data/lib/reckon/version.rb +1 -1
  18. data/reckon.gemspec +1 -3
  19. data/spec/integration/another_bank_example/input.csv +9 -0
  20. data/spec/integration/another_bank_example/output.ledger +36 -0
  21. data/spec/integration/another_bank_example/test_args +1 -0
  22. data/spec/integration/austrian_example/input.csv +13 -0
  23. data/spec/integration/austrian_example/output.ledger +52 -0
  24. data/spec/integration/austrian_example/test_args +2 -0
  25. data/spec/integration/bom_utf8_file/input.csv +3 -0
  26. data/spec/integration/bom_utf8_file/output.ledger +4 -0
  27. data/spec/integration/bom_utf8_file/test_args +3 -0
  28. data/spec/integration/broker_canada_example/input.csv +12 -0
  29. data/spec/integration/broker_canada_example/output.ledger +48 -0
  30. data/spec/integration/broker_canada_example/test_args +1 -0
  31. data/spec/integration/chase/account_tokens_and_regex/output.ledger +36 -0
  32. data/spec/integration/chase/account_tokens_and_regex/test_args +2 -0
  33. data/spec/integration/chase/account_tokens_and_regex/tokens.yml +16 -0
  34. data/spec/integration/chase/default_account_names/output.ledger +36 -0
  35. data/spec/integration/chase/default_account_names/test_args +3 -0
  36. data/spec/integration/chase/input.csv +9 -0
  37. data/spec/integration/chase/learn_from_existing/learn.ledger +7 -0
  38. data/spec/integration/chase/learn_from_existing/output.ledger +36 -0
  39. data/spec/integration/chase/learn_from_existing/test_args +1 -0
  40. data/spec/integration/chase/simple/output.ledger +36 -0
  41. data/spec/integration/chase/simple/test_args +1 -0
  42. data/spec/integration/danish_kroner_nordea_example/input.csv +6 -0
  43. data/spec/integration/danish_kroner_nordea_example/output.ledger +24 -0
  44. data/spec/integration/danish_kroner_nordea_example/test_args +1 -0
  45. data/spec/integration/english_date_example/input.csv +3 -0
  46. data/spec/integration/english_date_example/output.ledger +12 -0
  47. data/spec/integration/english_date_example/test_args +1 -0
  48. data/spec/integration/extratofake/input.csv +24 -0
  49. data/spec/integration/extratofake/output.ledger +92 -0
  50. data/spec/integration/extratofake/test_args +1 -0
  51. data/spec/integration/french_example/input.csv +9 -0
  52. data/spec/integration/french_example/output.ledger +36 -0
  53. data/spec/integration/french_example/test_args +2 -0
  54. data/spec/integration/german_date_example/input.csv +3 -0
  55. data/spec/integration/german_date_example/output.ledger +12 -0
  56. data/spec/integration/german_date_example/test_args +1 -0
  57. data/spec/integration/harder_date_example/input.csv +5 -0
  58. data/spec/integration/harder_date_example/output.ledger +20 -0
  59. data/spec/integration/harder_date_example/test_args +1 -0
  60. data/spec/integration/ing/input.csv +3 -0
  61. data/spec/integration/ing/output.ledger +12 -0
  62. data/spec/integration/ing/test_args +1 -0
  63. data/spec/integration/intuit_mint_example/input.csv +7 -0
  64. data/spec/integration/intuit_mint_example/output.ledger +28 -0
  65. data/spec/integration/intuit_mint_example/test_args +1 -0
  66. data/spec/integration/invalid_header_example/input.csv +6 -0
  67. data/spec/integration/invalid_header_example/output.ledger +8 -0
  68. data/spec/integration/invalid_header_example/test_args +1 -0
  69. data/spec/integration/inversed_credit_card/input.csv +16 -0
  70. data/spec/integration/inversed_credit_card/output.ledger +64 -0
  71. data/spec/integration/inversed_credit_card/test_args +1 -0
  72. data/spec/integration/nationwide/input.csv +4 -0
  73. data/spec/integration/nationwide/output.ledger +16 -0
  74. data/spec/integration/nationwide/test_args +1 -0
  75. data/spec/integration/regression/issue_51_account_tokens/input.csv +8 -0
  76. data/spec/integration/regression/issue_51_account_tokens/output.ledger +32 -0
  77. data/spec/integration/regression/issue_51_account_tokens/test_args +4 -0
  78. data/spec/integration/regression/issue_51_account_tokens/tokens.yml +9 -0
  79. data/spec/integration/regression/issue_64_date_column/input.csv +3 -0
  80. data/spec/integration/regression/issue_64_date_column/output.ledger +8 -0
  81. data/spec/integration/regression/issue_64_date_column/test_args +1 -0
  82. data/spec/integration/regression/issue_73_account_token_matching/input.csv +2 -0
  83. data/spec/integration/regression/issue_73_account_token_matching/output.ledger +4 -0
  84. data/spec/integration/regression/issue_73_account_token_matching/test_args +6 -0
  85. data/spec/integration/regression/issue_73_account_token_matching/tokens.yml +8 -0
  86. data/spec/integration/regression/issue_85_date_example/input.csv +2 -0
  87. data/spec/integration/regression/issue_85_date_example/output.ledger +8 -0
  88. data/spec/integration/regression/issue_85_date_example/test_args +1 -0
  89. data/spec/integration/spanish_date_example/input.csv +3 -0
  90. data/spec/integration/spanish_date_example/output.ledger +12 -0
  91. data/spec/integration/spanish_date_example/test_args +1 -0
  92. data/spec/integration/suntrust/input.csv +7 -0
  93. data/spec/integration/suntrust/output.ledger +28 -0
  94. data/spec/integration/suntrust/test_args +1 -0
  95. data/spec/integration/test.sh +83 -0
  96. data/spec/integration/test_money_column/input.csv +3 -0
  97. data/spec/integration/test_money_column/output.ledger +8 -0
  98. data/spec/integration/test_money_column/test_args +1 -0
  99. data/spec/integration/two_money_columns/input.csv +5 -0
  100. data/spec/integration/two_money_columns/output.ledger +20 -0
  101. data/spec/integration/two_money_columns/test_args +1 -0
  102. data/spec/integration/yyyymmdd_date_example/input.csv +1 -0
  103. data/spec/integration/yyyymmdd_date_example/output.ledger +4 -0
  104. data/spec/integration/yyyymmdd_date_example/test_args +1 -0
  105. data/spec/reckon/app_spec.rb +25 -7
  106. data/spec/reckon/ledger_parser_spec.rb +2 -2
  107. data/spec/reckon/money_column_spec.rb +24 -24
  108. data/spec/reckon/money_spec.rb +13 -32
  109. data/spec/reckon/options_spec.rb +17 -0
  110. data/spec/spec_helper.rb +6 -1
  111. metadata +97 -35
  112. data/.travis.yml +0 -13
data/bin/reckon CHANGED
@@ -3,7 +3,12 @@
3
3
  require 'rubygems'
4
4
  require 'reckon'
5
5
 
6
- options = Reckon::App.parse_opts
6
+ begin
7
+ options = Reckon::Options.parse
8
+ rescue RuntimeError => e
9
+ puts("ERROR: #{e}")
10
+ exit(1)
11
+ end
7
12
  reckon = Reckon::App.new(options)
8
13
 
9
14
  if options[:print_table]
data/lib/reckon.rb CHANGED
@@ -4,9 +4,8 @@ require 'rubygems'
4
4
  require 'rchardet'
5
5
  require 'chronic'
6
6
  require 'csv'
7
- require 'highline/import'
7
+ require 'highline'
8
8
  require 'optparse'
9
- require 'terminal-table'
10
9
  require 'time'
11
10
  require 'logger'
12
11
 
@@ -17,4 +16,5 @@ require_relative 'reckon/date_column'
17
16
  require_relative 'reckon/money'
18
17
  require_relative 'reckon/ledger_parser'
19
18
  require_relative 'reckon/csv_parser'
19
+ require_relative 'reckon/options'
20
20
  require_relative 'reckon/app'
data/lib/reckon/app.rb CHANGED
@@ -1,18 +1,20 @@
1
1
  # coding: utf-8
2
+
2
3
  require 'pp'
3
4
  require 'yaml'
4
5
 
5
6
  module Reckon
6
7
  class App
7
8
  attr_accessor :options, :seen, :csv_parser, :regexps, :matcher
9
+ @@cli = HighLine.new
8
10
 
9
- def initialize(options = {})
11
+ def initialize(opts = {})
12
+ self.options = opts
10
13
  LOGGER.level = Logger::INFO if options[:verbose]
11
- self.options = options
14
+
12
15
  self.regexps = {}
13
- self.seen = {}
16
+ self.seen = Set.new
14
17
  self.options[:currency] ||= '$'
15
- options[:string] = File.read(options[:file]) unless options[:string]
16
18
  @csv_parser = CSVParser.new( options )
17
19
  @matcher = CosineSimilarity.new(options)
18
20
  learn!
@@ -20,22 +22,19 @@ module Reckon
20
22
 
21
23
  def interactive_output(str)
22
24
  return if options[:unattended]
25
+
23
26
  puts str
24
27
  end
25
28
 
26
29
  def learn!
27
30
  learn_from_account_tokens(options[:account_tokens_file])
28
-
29
- ledger_file = options[:existing_ledger_file]
30
- return unless ledger_file
31
- fail "#{ledger_file} doesn't exist!" unless File.exists?(ledger_file)
32
- learn_from(File.read(ledger_file))
31
+ learn_from_ledger_file(options[:existing_ledger_file])
33
32
  end
34
33
 
35
34
  def learn_from_account_tokens(filename)
36
35
  return unless filename
37
36
 
38
- fail "#{filename} doesn't exist!" unless File.exists?(filename)
37
+ raise "#{filename} doesn't exist!" unless File.exist?(filename)
39
38
 
40
39
  extract_account_tokens(YAML.load_file(filename)).each do |account, tokens|
41
40
  tokens.each do |t|
@@ -48,14 +47,27 @@ module Reckon
48
47
  end
49
48
  end
50
49
 
51
- def learn_from(ledger)
50
+ def learn_from_ledger_file(ledger_file)
51
+ return unless ledger_file
52
+
53
+ raise "#{ledger_file} doesn't exist!" unless File.exist?(ledger_file)
54
+
55
+ learn_from_ledger(File.read(ledger_file))
56
+ end
57
+
58
+ def learn_from_ledger(ledger)
59
+ LOGGER.info "learning from #{ledger}"
52
60
  LedgerParser.new(ledger).entries.each do |entry|
53
61
  entry[:accounts].each do |account|
54
62
  str = [entry[:desc], account[:amount]].join(" ")
55
- @matcher.add_document(account[:name], str) unless account[:name] == options[:bank_account]
63
+ if account[:name] != options[:bank_account]
64
+ LOGGER.info "adding document #{account[:name]} #{str}"
65
+ @matcher.add_document(account[:name], str)
66
+ end
56
67
  pretty_date = entry[:date].iso8601
57
- seen[pretty_date] ||= {}
58
- seen[pretty_date][@csv_parser.pretty_money(account[:amount])] = true
68
+ if account[:name] == options[:bank_account]
69
+ seen << seen_key(pretty_date, @csv_parser.pretty_money(account[:amount]))
70
+ end
59
71
  end
60
72
  end
61
73
  end
@@ -91,9 +103,10 @@ module Reckon
91
103
  end
92
104
 
93
105
  def walk_backwards
106
+ cmd_options = "[account]/[q]uit/[s]kip/[n]ote/[d]escription"
94
107
  seen_anything_new = false
95
108
  each_row_backwards do |row|
96
- interactive_output Terminal::Table.new(:rows => [ [ row[:pretty_date], row[:pretty_money], row[:description] ] ])
109
+ print_transaction([row])
97
110
 
98
111
  if already_seen?(row)
99
112
  interactive_output "NOTE: This row is very similar to a previous one!"
@@ -105,50 +118,28 @@ module Reckon
105
118
  seen_anything_new = true
106
119
  end
107
120
 
108
- possible_answers = suggest(row)
109
-
110
- ledger = if row[:money] > 0
111
- if options[:unattended]
112
- out_of_account = possible_answers.first || options[:default_outof_account] || 'Income:Unknown'
113
- else
114
- out_of_account = ask("Which account provided this income? ([account]/[q]uit/[s]kip) ") { |q|
115
- q.completion = possible_answers
116
- q.readline = true
117
- q.default = possible_answers.first
118
- }
119
- end
120
-
121
- finish if out_of_account == "quit" || out_of_account == "q"
122
- if out_of_account == "skip" || out_of_account == "s"
123
- interactive_output "Skipping"
124
- next
125
- end
126
-
127
- ledger_format( row,
128
- [options[:bank_account], row[:pretty_money]],
129
- [out_of_account, row[:pretty_money_negated]] )
121
+ if row[:money] > 0
122
+ # out_of_account
123
+ answer = ask_account_question("Which account provided this income? (#{cmd_options})", row)
124
+ line1 = [options[:bank_account], row[:pretty_money]]
125
+ line2 = [answer, ""]
130
126
  else
131
- if options[:unattended]
132
- into_account = possible_answers.first || options[:default_into_account] || 'Expenses:Unknown'
133
- else
134
- into_account = ask("To which account did this money go? ([account]/[q]uit/[s]kip) ") { |q|
135
- q.completion = possible_answers
136
- q.readline = true
137
- q.default = possible_answers.first
138
- }
139
- end
140
- finish if into_account == "quit" || into_account == 'q'
141
- if into_account == "skip" || into_account == 's'
142
- interactive_output "Skipping"
143
- next
144
- end
127
+ # into_account
128
+ answer = ask_account_question("To which account did this money go? (#{cmd_options})", row)
129
+ # line1 = [answer, row[:pretty_money_negated]]
130
+ line1 = [answer, ""]
131
+ line2 = [options[:bank_account], row[:pretty_money]]
132
+ end
145
133
 
146
- ledger_format( row,
147
- [into_account, row[:pretty_money_negated]],
148
- [options[:bank_account], row[:pretty_money]] )
134
+ finish if %w[quit q].include?(answer)
135
+ if %w[skip s].include?(answer)
136
+ interactive_output "Skipping"
137
+ next
149
138
  end
150
139
 
151
- learn_from(ledger) unless options[:account_tokens_file]
140
+ ledger = ledger_format(row, line1, line2)
141
+ LOGGER.info "ledger line: #{ledger}"
142
+ learn_from_ledger(ledger) unless options[:account_tokens_file]
152
143
  output(ledger)
153
144
  end
154
145
  end
@@ -167,16 +158,95 @@ module Reckon
167
158
  :money => @csv_parser.money_for(index),
168
159
  :description => @csv_parser.description_for(index) }
169
160
  end
170
- rows.sort_by { |n| n[:date] }.each {|row| yield row }
161
+ rows.sort_by { |n| [n[:date], -n[:money], n[:description]] }.each { |row| yield row }
171
162
  end
172
163
 
173
- def most_specific_regexp_match( row )
164
+ def print_transaction(rows)
165
+ str = "\n"
166
+ header = %w[Date Amount Description Note]
167
+ maxes = header.map(&:length)
168
+
169
+ rows = rows.map { |r| [r[:pretty_date], r[:pretty_money], r[:description], r[:note]] }
170
+
171
+ rows.each do |r|
172
+ r.length.times { |i| l = r[i] ? r[i].length : 0; maxes[i] = l if maxes[i] < l }
173
+ end
174
+
175
+ header.each_with_index do |n, i|
176
+ str += " #{n.center(maxes[i])} |"
177
+ end
178
+ str += "\n"
179
+
180
+ rows.each do |row|
181
+ row.each_with_index do |_, i|
182
+ just = maxes[i]
183
+ str += sprintf(" %#{just}s |", row[i])
184
+ end
185
+ str += "\n"
186
+ end
187
+
188
+ interactive_output str
189
+ end
190
+
191
+ def ask_account_question(msg, row)
192
+ possible_answers = suggest(row)
193
+ LOGGER.info "possible_answers===> #{possible_answers.inspect}"
194
+
195
+ if options[:unattended]
196
+ if options[:fail_on_unknown_account] && possible_answers.empty?
197
+ raise %(Couldn't find any matches for '#{row[:description]}'
198
+ Try adding an account token with --account-tokens)
199
+ end
200
+
201
+ default = options[:default_outof_account]
202
+ default = options[:default_into_account] if row[:pretty_money][0] == '-'
203
+ return possible_answers[0] || default
204
+ end
205
+
206
+ answer = @@cli.ask(msg) do |q|
207
+ q.completion = possible_answers
208
+ q.readline = true
209
+ q.default = possible_answers.first
210
+ end
211
+
212
+ # if answer isn't n/note/d/description, must be an account name, or skip, or quit
213
+ return answer unless %w[n note d description].include?(answer)
214
+
215
+ add_description(row) if %w[d description].include?(answer)
216
+ add_note(row) if %w[n note].include?(answer)
217
+
218
+ print_transaction([row])
219
+ # give user a chance to set account name or retry description
220
+ return ask_account_question(msg, row)
221
+ end
222
+
223
+ def add_description(row)
224
+ desc_answer = @@cli.ask("Enter a new description for this transaction (empty line aborts)\n") do |q|
225
+ q.overwrite = true
226
+ q.readline = true
227
+ q.default = row[:description]
228
+ end
229
+
230
+ row[:description] = desc_answer unless desc_answer.empty?
231
+ end
232
+
233
+ def add_note(row)
234
+ desc_answer = @@cli.ask("Enter a new note for this transaction (empty line aborts)\n") do |q|
235
+ q.overwrite = true
236
+ q.readline = true
237
+ q.default = row[:note]
238
+ end
239
+
240
+ row[:note] = desc_answer unless desc_answer.empty?
241
+ end
242
+
243
+ def most_specific_regexp_match(row)
174
244
  matches = regexps.map { |regexp, account|
175
245
  if match = regexp.match(row[:description])
176
246
  [account, match[0]]
177
247
  end
178
248
  }.compact
179
- matches.sort_by! { |account, matched_text| matched_text.length }.map(&:first)
249
+ matches.sort_by! { |_account, matched_text| matched_text.length }.map(&:first)
180
250
  end
181
251
 
182
252
  def suggest(row)
@@ -185,9 +255,9 @@ module Reckon
185
255
  end
186
256
 
187
257
  def ledger_format(row, line1, line2)
188
- out = "#{row[:pretty_date]}\t#{row[:description]}\n"
189
- out += "\t#{line1.first}\t\t\t\t\t#{line1.last}\n"
190
- out += "\t#{line2.first}\t\t\t\t\t#{line2.last}\n\n"
258
+ out = "#{row[:pretty_date]}\t#{row[:description]}#{row[:note] ? "\t; " + row[:note]: ""}\n"
259
+ out += "\t#{line1.first}\t\t\t#{line1.last}\n"
260
+ out += "\t#{line2.first}\t\t\t#{line2.last}\n\n"
191
261
  out
192
262
  end
193
263
 
@@ -196,8 +266,12 @@ module Reckon
196
266
  options[:output_file].flush
197
267
  end
198
268
 
269
+ def seen_key(date, amount)
270
+ return [date, amount].join("|")
271
+ end
272
+
199
273
  def already_seen?(row)
200
- seen[row[:pretty_date]] && seen[row[:pretty_date]][row[:pretty_money]]
274
+ seen.include?(seen_key(row[:pretty_date], row[:pretty_money]))
201
275
  end
202
276
 
203
277
  def finish
@@ -207,139 +281,11 @@ module Reckon
207
281
  end
208
282
 
209
283
  def output_table
210
- output = Terminal::Table.new do |t|
211
- t.headings = 'Date', 'Amount', 'Description'
212
- each_row_backwards do |row|
213
- t << [ row[:pretty_date], row[:pretty_money], row[:description] ]
214
- end
215
- end
216
- interactive_output output
217
- end
218
-
219
- def self.parse_opts(args = ARGV)
220
- options = { :output_file => STDOUT }
221
- parser = OptionParser.new do |opts|
222
- opts.banner = "Usage: Reckon.rb [options]"
223
- opts.separator ""
224
-
225
- opts.on("-f", "--file FILE", "The CSV file to parse") do |file|
226
- options[:file] = file
227
- end
228
-
229
- opts.on("-a", "--account NAME", "The Ledger Account this file is for") do |a|
230
- options[:bank_account] = a
231
- end
232
-
233
- opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
234
- options[:verbose] = v
235
- end
236
-
237
- opts.on("-i", "--inverse", "Use the negative of each amount") do |v|
238
- options[:inverse] = v
239
- end
240
-
241
- opts.on("-p", "--print-table", "Print out the parsed CSV in table form") do |p|
242
- options[:print_table] = p
243
- end
244
-
245
- opts.on("-o", "--output-file FILE", "The ledger file to append to") do |o|
246
- options[:output_file] = File.open(o, 'a')
247
- end
248
-
249
- opts.on("-l", "--learn-from FILE", "An existing ledger file to learn accounts from") do |l|
250
- options[:existing_ledger_file] = l
251
- end
252
-
253
- opts.on("", "--ignore-columns 1,2,5", "Columns to ignore in the CSV file - the first column is column 1") do |ignore|
254
- options[:ignore_columns] = ignore.split(",").map { |i| i.to_i }
255
- end
256
-
257
- opts.on("", "--money-column 2", Integer, "Specify the money column instead of letting Reckon guess - the first column is column 1") do |column_number|
258
- options[:money_column] = column_number
259
- end
260
-
261
- opts.on("", "--date-column 3", Integer, "Specify the date column instead of letting Reckon guess - the first column is column 1") do |column_number|
262
- options[:date_column] = column_number
263
- end
264
-
265
- opts.on("", "--contains-header [N]", "The first row of the CSV is a header and should be skipped. Optionally add the number of rows to skip.") do |contains_header|
266
- options[:contains_header] = 1
267
- options[:contains_header] = contains_header.to_i if contains_header
268
- end
269
-
270
- opts.on("", "--csv-separator ','", "Separator for parsing the CSV - default is comma.") do |csv_separator|
271
- options[:csv_separator] = csv_separator
272
- end
273
-
274
- opts.on("", "--comma-separates-cents", "Use comma instead of period to deliminate dollars from cents when parsing ($100,50 instead of $100.50)") do |c|
275
- options[:comma_separates_cents] = c
276
- end
277
-
278
- opts.on("", "--encoding 'UTF-8'", "Specify an encoding for the CSV file; not usually needed") do |e|
279
- options[:encoding] = e
280
- end
281
-
282
- opts.on("-c", "--currency '$'", "Currency symbol to use, defaults to $ (£, EUR)") do |e|
283
- options[:currency] = e
284
- end
285
-
286
- opts.on("", "--date-format '%d/%m/%Y'", "Force the date format (see Ruby DateTime strftime)") do |d|
287
- options[:date_format] = d
288
- end
289
-
290
- opts.on("-u", "--unattended", "Don't ask questions and guess all the accounts automatically. Used with --learn-from or --account-tokens options.") do |n|
291
- options[:unattended] = n
292
- end
293
-
294
- opts.on("-t", "--account-tokens FILE", "YAML file with manually-assigned tokens for each account (see README)") do |a|
295
- options[:account_tokens_file] = a
296
- end
297
-
298
- opts.on("", "--default-into-account NAME", "Default into account") do |a|
299
- options[:default_into_account] = a
300
- end
301
-
302
- opts.on("", "--default-outof-account NAME", "Default 'out of' account") do |a|
303
- options[:default_outof_account] = a
304
- end
305
-
306
- opts.on("", "--suffixed", "If --currency should be used as a suffix. Defaults to false.") do |e|
307
- options[:suffixed] = e
308
- end
309
-
310
- opts.on_tail("-h", "--help", "Show this message") do
311
- puts opts
312
- exit
313
- end
314
-
315
- opts.on_tail("--version", "Show version") do
316
- puts VERSION
317
- exit
318
- end
319
-
320
- opts.parse!(args)
321
- end
322
-
323
- unless options[:file]
324
- options[:file] = ask("What CSV file should I parse? ")
325
- unless options[:file].length > 0
326
- puts "\nYou must provide a CSV file to parse.\n"
327
- puts parser
328
- exit
329
- end
330
- end
331
-
332
- unless options[:bank_account]
333
- fail "Please specify --account for the unattended mode" if options[:unattended]
334
-
335
- options[:bank_account] = ask("What is the account name of this bank account in Ledger? ") do |q|
336
- q.readline = true
337
- q.validate = /^.{2,}$/
338
- q.default = "Assets:Bank:Checking"
339
- end
284
+ rows = []
285
+ each_row_backwards do |row|
286
+ rows << row
340
287
  end
341
-
342
- options
288
+ print_transaction(rows)
343
289
  end
344
290
  end
345
291
  end