reckon 0.9.0 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,13 +2,17 @@ module Reckon
2
2
  class DateColumn < Array
3
3
  attr_accessor :endian_precedence
4
4
  def initialize( arr = [], options = {} )
5
- @options = options
5
+ # output date format
6
+ @ledger_date_format = options[:ledger_date_format]
7
+
8
+ # input date format
9
+ date_format = options[:date_format]
6
10
  arr.each do |value|
7
- if options[:date_format]
11
+ if date_format
8
12
  begin
9
- value = Date.strptime(value, options[:date_format])
13
+ value = Date.strptime(value, date_format)
10
14
  rescue
11
- puts "I'm having trouble parsing '#{value}' with the desired format: #{options[:date_format]}"
15
+ puts "I'm having trouble parsing '#{value}' with the desired format: #{date_format}"
12
16
  exit 1
13
17
  end
14
18
  else
@@ -34,7 +38,7 @@ module Reckon
34
38
  self.push( value )
35
39
  end
36
40
  # if endian_precedence still nil, raise error
37
- unless @endian_precedence || options[:date_format]
41
+ unless @endian_precedence || date_format
38
42
  raise( "Unable to determine date format. Please specify using --date-format" )
39
43
  end
40
44
  end
@@ -54,7 +58,7 @@ module Reckon
54
58
  date = self.for(index)
55
59
  return "" if date.nil?
56
60
 
57
- date.strftime(@options[:ledger_date_format] || '%Y-%m-%d')
61
+ date.strftime(@ledger_date_format || '%Y-%m-%d')
58
62
  end
59
63
 
60
64
  def self.likelihood(entry)
@@ -65,7 +69,14 @@ module Reckon
65
69
  date_score -= entry.gsub(/[\-\/\.\d:\[\]]/, '').length
66
70
  date_score += 30 if entry =~ /^\d+[:\/\.-]\d+[:\/\.-]\d+([ :]\d+[:\/\.]\d+)?$/
67
71
  date_score += 10 if entry =~ /^\d+\[\d+:GMT\]$/i
68
- return date_score
72
+
73
+ begin
74
+ DateTime.parse(entry)
75
+ date_score += 20
76
+ rescue Date::Error, ArgumentError
77
+ end
78
+
79
+ date_score
69
80
  end
70
81
  end
71
82
  end
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env ruby
2
1
  # frozen_string_literal: true
3
2
 
4
3
  # From: https://www.ledger-cli.org/3.0/doc/ledger3.html#Transactions-and-Comments
@@ -110,20 +109,20 @@ require 'rubygems'
110
109
  module Reckon
111
110
  class LedgerParser
112
111
 
113
- attr_accessor :entries
114
-
115
- def initialize(ledger, options = {})
112
+ # ledger is an object that response to #each_line,
113
+ # (i.e. a StringIO or an IO object)
114
+ def initialize(options = {})
116
115
  @options = options
117
116
  @date_format = options[:ledger_date_format] || options[:date_format] || '%Y-%m-%d'
118
- parse(ledger)
119
117
  end
120
118
 
121
119
  def parse(ledger)
122
- @entries = []
120
+ entries = []
123
121
  new_entry = {}
124
122
  in_comment = false
125
123
  comment_chars = ';#%*|'
126
- ledger.strip.split("\n").each do |entry|
124
+ ledger.each_line do |entry|
125
+ entry.rstrip!
127
126
  # strip comment lines
128
127
  in_comment = true if entry == 'comment'
129
128
  in_comment = false if entry == 'end comment'
@@ -132,7 +131,7 @@ module Reckon
132
131
 
133
132
  # (date, type, code, description), type and code are optional
134
133
  if (m = entry.match(%r{^(\d+[\d/-]+)\s+([*!])?\s*(\([^)]+\))?\s*(.*)$}))
135
- add_entry(new_entry)
134
+ add_entry(entries, new_entry)
136
135
  new_entry = {
137
136
  date: try_parse_date(m[1]),
138
137
  type: m[2] || "",
@@ -141,23 +140,24 @@ module Reckon
141
140
  accounts: []
142
141
  }
143
142
  elsif entry =~ /^\s*$/ && new_entry[:date]
144
- add_entry(new_entry)
143
+ add_entry(entries,new_entry)
145
144
  new_entry = {}
146
145
  elsif new_entry[:date] && entry =~ /^\s+/
147
146
  LOGGER.info("Adding new account #{entry}")
148
147
  new_entry[:accounts] << parse_account_line(entry)
149
148
  else
150
149
  LOGGER.info("Unknown entry type: #{entry}")
151
- add_entry(new_entry)
150
+ add_entry(entries, new_entry)
152
151
  new_entry = {}
153
152
  end
154
153
  end
155
- add_entry(new_entry)
154
+ add_entry(entries, new_entry)
155
+ entries
156
156
  end
157
157
 
158
158
  # roughly matches ledger csv format
159
- def to_csv
160
- return @entries.flat_map do |n|
159
+ def to_csv(ledger)
160
+ return parse(ledger).flat_map do |n|
161
161
  n[:accounts].map do |a|
162
162
  row = [
163
163
  n[:date].strftime(@date_format),
@@ -174,13 +174,21 @@ module Reckon
174
174
  end
175
175
  end
176
176
 
177
+ def format_row(row, line1, line2)
178
+ out = "#{row[:pretty_date]}\t#{row[:description]}#{row[:note] ? "\t; " + row[:note]: ""}\n"
179
+ out += "\t#{line1.first}\t\t\t#{line1.last}\n"
180
+ out += "\t#{line2.first}\t\t\t#{line2.last}\n\n"
181
+ out
182
+ end
183
+
184
+
177
185
  private
178
186
 
179
- def add_entry(entry)
187
+ def add_entry(entries, entry)
180
188
  return unless entry[:date] && entry[:accounts].length > 1
181
189
 
182
190
  entry[:accounts] = balance(entry[:accounts])
183
- @entries << entry
191
+ entries << entry
184
192
  end
185
193
 
186
194
  def try_parse_date(date_str)
data/lib/reckon/money.rb CHANGED
@@ -6,11 +6,10 @@ module Reckon
6
6
  include Comparable
7
7
  attr_accessor :amount, :currency, :suffixed
8
8
  def initialize(amount, options = {})
9
- @options = options
10
9
  @amount_raw = amount
11
10
  @raw = options[:raw]
12
11
 
13
- @amount = parse(amount, options)
12
+ @amount = parse(amount, options[:comma_separates_cents])
14
13
  @amount = -@amount if options[:inverse]
15
14
  @currency = options[:currency] || "$"
16
15
  @suffixed = options[:suffixed]
@@ -21,7 +20,7 @@ module Reckon
21
20
  end
22
21
 
23
22
  def to_s
24
- return @options[:raw] ? "#{@amount_raw} | #{@amount}" : @amount
23
+ return @raw ? "#{@amount_raw} | #{@amount}" : @amount
25
24
  end
26
25
 
27
26
  # unary minus
@@ -60,34 +59,37 @@ module Reckon
60
59
  return (@amount >= 0 ? " " : "") + amt
61
60
  end
62
61
 
62
+ def self.likelihood(entry)
63
+ money_score = 0
64
+ # digits separated by , or . with no more than 2 trailing digits
65
+ money_score += 40 if entry.match(/\d+[,.]\d{2}[^\d]*$/)
66
+ money_score += 10 if entry[/^\$?\-?\$?\d+[\.,\d]*?[\.,]\d\d$/]
67
+ money_score += 10 if entry[/\d+[\.,\d]*?[\.,]\d\d$/]
68
+ money_score += entry.gsub(/[^\d\.\-\+,\(\)]/, '').length if entry.length < 7
69
+ money_score -= entry.length if entry.length > 12
70
+ money_score -= 20 if (entry !~ /^[\$\+\.\-,\d\(\)]+$/) && entry.length > 0
71
+ money_score
72
+ end
73
+
74
+ private
75
+
63
76
  def pretty_amount(amount)
64
77
  sprintf("%0.2f", amount).reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
65
78
  end
66
79
 
67
- def parse(value, options = {})
80
+ def parse(value, comma_separates_cents)
68
81
  value = value.to_s
69
82
  # Empty string is treated as money with value 0
70
83
  return value.to_f if value.to_s.empty?
71
84
 
72
85
  invert = value.match(/^\(.*\)$/)
73
86
  value = value.gsub(/[^0-9,.-]/, '')
74
- value = value.tr('.', '').tr(',', '.') if options[:comma_separates_cents]
87
+ value = value.tr('.', '').tr(',', '.') if comma_separates_cents
75
88
  value = value.tr(',', '')
76
89
  value = value.to_f
77
90
  return invert ? -value : value
78
91
  end
79
92
 
80
- def Money::likelihood(entry)
81
- money_score = 0
82
- # digits separated by , or . with no more than 2 trailing digits
83
- money_score += 40 if entry.match(/\d+[,.]\d{2}[^\d]*$/)
84
- money_score += 10 if entry[/^\$?\-?\$?\d+[\.,\d]*?[\.,]\d\d$/]
85
- money_score += 10 if entry[/\d+[\.,\d]*?[\.,]\d\d$/]
86
- money_score += entry.gsub(/[^\d\.\-\+,\(\)]/, '').length if entry.length < 7
87
- money_score -= entry.length if entry.length > 12
88
- money_score -= 20 if (entry !~ /^[\$\+\.\-,\d\(\)]+$/) && entry.length > 0
89
- money_score
90
- end
91
93
  end
92
94
 
93
95
  class MoneyColumn < Array
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Reckon
4
+ # Singleton class for parsing command line flags
2
5
  class Options
3
- @@cli = HighLine.new
4
-
5
- def self.parse(args = ARGV, stdin = $stdin)
6
+ def self.parse_command_line_options(args = ARGV, stdin = $stdin)
7
+ cli = HighLine.new
6
8
  options = { output_file: $stdout }
7
9
  OptionParser.new do |opts|
8
10
  opts.banner = "Usage: Reckon.rb [options]"
@@ -32,19 +34,23 @@ module Reckon
32
34
  options[:output_file] = File.open(o, 'a')
33
35
  end
34
36
 
35
- opts.on("-l", "--learn-from FILE", "An existing ledger file to learn accounts from") do |l|
37
+ opts.on("-l", "--learn-from FILE",
38
+ "An existing ledger file to learn accounts from") do |l|
36
39
  options[:existing_ledger_file] = l
37
40
  end
38
41
 
39
- opts.on("", "--ignore-columns 1,2,5", "Columns to ignore, starts from 1") do |ignore|
42
+ opts.on("", "--ignore-columns 1,2,5",
43
+ "Columns to ignore, starts from 1") do |ignore|
40
44
  options[:ignore_columns] = ignore.split(",").map(&:to_i)
41
45
  end
42
46
 
43
- opts.on("", "--money-column 2", Integer, "Column number of the money column, starts from 1") do |col|
47
+ opts.on("", "--money-column 2", Integer,
48
+ "Column number of the money column, starts from 1") do |col|
44
49
  options[:money_column] = col
45
50
  end
46
51
 
47
- opts.on("", "--money-columns 2,3", "Column number of the money columns, starts from 1 (1 or 2 columns)") do |ignore|
52
+ opts.on("", "--money-columns 2,3",
53
+ "Column number of the money columns, starts from 1 (1 or 2 columns)") do |ignore|
48
54
  options[:money_columns] = ignore.split(",").map(&:to_i)
49
55
  end
50
56
 
@@ -52,20 +58,28 @@ module Reckon
52
58
  options[:raw] = n
53
59
  end
54
60
 
55
- opts.on("", "--date-column 3", Integer, "Column number of the date column, starts from 1") do |col|
61
+ opts.on("", "--date-column 3", Integer,
62
+ "Column number of the date column, starts from 1") do |col|
56
63
  options[:date_column] = col
57
64
  end
58
65
 
59
- opts.on("", "--contains-header [N]", Integer, "Skip N header rows - default 1") do |hdr|
66
+ opts.on("", "--contains-header [N]", Integer,
67
+ "Skip N header rows - default 1") do |hdr|
60
68
  options[:contains_header] = 1
61
69
  options[:contains_header] = hdr.to_i
62
70
  end
63
71
 
72
+ opts.on("", "--contains-footer [N]", Integer,
73
+ "Skip N footer rows - default 0") do |hdr|
74
+ options[:contains_footer] = hdr.to_i || 0
75
+ end
76
+
64
77
  opts.on("", "--csv-separator ','", "CSV separator (default ',')") do |sep|
65
78
  options[:csv_separator] = sep
66
79
  end
67
80
 
68
- opts.on("", "--comma-separates-cents", "Use comma to separate cents ($100,50 vs. $100.50)") do |c|
81
+ opts.on("", "--comma-separates-cents",
82
+ "Use comma to separate cents ($100,50 vs. $100.50)") do |c|
69
83
  options[:comma_separates_cents] = c
70
84
  end
71
85
 
@@ -73,23 +87,28 @@ module Reckon
73
87
  options[:encoding] = e
74
88
  end
75
89
 
76
- opts.on("-c", "--currency '$'", "Currency symbol to use - default $ (ex £, EUR)") do |e|
77
- options[:currency] = e
90
+ opts.on("-c", "--currency '$'",
91
+ "Currency symbol to use - default $ (ex £, EUR)") do |e|
92
+ options[:currency] = e || '$'
78
93
  end
79
94
 
80
- opts.on("", "--date-format FORMAT", "CSV file date format (see `date` for format)") do |d|
95
+ opts.on("", "--date-format FORMAT",
96
+ "CSV file date format (see `date` for format)") do |d|
81
97
  options[:date_format] = d
82
98
  end
83
99
 
84
- opts.on("", "--ledger-date-format FORMAT", "Ledger date format (see `date` for format)") do |d|
100
+ opts.on("", "--ledger-date-format FORMAT",
101
+ "Ledger date format (see `date` for format)") do |d|
85
102
  options[:ledger_date_format] = d
86
103
  end
87
104
 
88
- opts.on("-u", "--unattended", "Don't ask questions and guess all the accounts automatically. Use with --learn-from or --account-tokens options.") do |n|
105
+ opts.on("-u", "--unattended",
106
+ "Don't ask questions and guess all the accounts automatically. Use with --learn-from or --account-tokens options.") do |n|
89
107
  options[:unattended] = n
90
108
  end
91
109
 
92
- opts.on("-t", "--account-tokens FILE", "YAML file with manually-assigned tokens for each account (see README)") do |a|
110
+ opts.on("-t", "--account-tokens FILE",
111
+ "YAML file with manually-assigned tokens for each account (see README)") do |a|
93
112
  options[:account_tokens_file] = a
94
113
  end
95
114
 
@@ -107,7 +126,8 @@ module Reckon
107
126
  options[:default_outof_account] = a
108
127
  end
109
128
 
110
- opts.on("", "--fail-on-unknown-account", "Fail on unmatched transactions.") do |n|
129
+ opts.on("", "--fail-on-unknown-account",
130
+ "Fail on unmatched transactions.") do |n|
111
131
  options[:fail_on_unknown_account] = n
112
132
  end
113
133
 
@@ -115,6 +135,11 @@ module Reckon
115
135
  options[:suffixed] = e
116
136
  end
117
137
 
138
+ opts.on("", "--ledger-format FORMAT",
139
+ "Output/Learn format: BEANCOUNT or LEDGER. Default: LEDGER") do |n|
140
+ options[:format] = n
141
+ end
142
+
118
143
  opts.on_tail("-h", "--help", "Show this message") do
119
144
  puts opts
120
145
  exit
@@ -137,7 +162,7 @@ module Reckon
137
162
  end
138
163
 
139
164
  unless options[:file]
140
- options[:file] = @@cli.ask("What CSV file should I parse? ")
165
+ options[:file] = cli.ask("What CSV file should I parse? ")
141
166
  unless options[:file].empty?
142
167
  puts "\nYou must provide a CSV file to parse.\n"
143
168
  puts parser
@@ -148,7 +173,7 @@ module Reckon
148
173
  unless options[:bank_account]
149
174
  raise "Must specify --account in unattended mode" if options[:unattended]
150
175
 
151
- options[:bank_account] = @@cli.ask("What is this account named in Ledger?\n") do |q|
176
+ options[:bank_account] = cli.ask("What is this account named in Ledger?\n") do |q|
152
177
  q.readline = true
153
178
  q.validate = /^.{2,}$/
154
179
  q.default = "Assets:Bank:Checking"
@@ -1,3 +1,3 @@
1
1
  module Reckon
2
- VERSION="0.9.0"
2
+ VERSION = "0.9.1"
3
3
  end
data/lib/reckon.rb CHANGED
@@ -15,6 +15,7 @@ require_relative 'reckon/cosine_similarity'
15
15
  require_relative 'reckon/date_column'
16
16
  require_relative 'reckon/money'
17
17
  require_relative 'reckon/ledger_parser'
18
+ require_relative 'reckon/beancount_parser'
18
19
  require_relative 'reckon/csv_parser'
19
20
  require_relative 'reckon/options'
20
21
  require_relative 'reckon/app'
@@ -8,7 +8,7 @@ ledger_file = ARGV[0]
8
8
  account = ARGV[1]
9
9
  seed = ARGV[2] ? ARGV[2].to_i : Random.new_seed
10
10
 
11
- ledger = Reckon::LedgerParser.new(File.read(ledger_file))
11
+ ledger = Reckon::LedgerParser.new(File.new(ledger_file))
12
12
  matcher = Reckon::CosineSimilarity.new({})
13
13
 
14
14
  train = []
@@ -0,0 +1,5 @@
1
+ ,311053760,2002-09-10T23:00:04,Merchant Transaction,Complete,,,"Lyft, Inc",- $21.59,,,,,,Venmo balance,,,,,Venmo,,
2
+ ,,,,,,,,,,,,,,,,,$23.40,$0.00,,$0.00,"In case of errors or questions about your
3
+ electronic transfers:
4
+ This is a multi-line string
5
+ "
@@ -1,8 +1,7 @@
1
- 4016-02-18
2
- Assets:Bank:Checking $10.00
3
- Income:Unknown
4
-
5
- 4016-02-19
6
- Income:Unknown
7
- Assets:Bank:Checking $0.00
1
+ 2016-02-18 COTISATION JAZZ; COTISATION JAZZ; EUR
2
+ Expenses:Unknown
3
+ Assets:Bank:Checking -$8.10
8
4
 
5
+ 2016-02-19 VIR RECU 508160; VIR RECU 1234567834S DE: Francois REF: 123457891234567894561231 PROVENANCE: DE Allemagne; EUR
6
+ Assets:Bank:Checking $50.00
7
+ Expenses:Unknown
@@ -1 +1 @@
1
- -f input.csv --unattended --account Assets:Bank:Checking --contains-header 4
1
+ -f input.csv --unattended --account Assets:Bank:Checking --contains-header 4 --comma-separates-cents --verbose
@@ -0,0 +1,2 @@
1
+ 123456789 EUR 20160102 15,00 10,00 20160102 -5,00 DESCRIPTION
2
+ 123456789 EUR 20160102 10,00 0,00 20160102 -10,00 DESCRIPTION
@@ -0,0 +1,8 @@
1
+ 2016-01-02 123456789; EUR; 20160102; DESCRIPTION
2
+ Expenses:Unknown
3
+ Test::Account -€5.00
4
+
5
+ 2016-01-02 123456789; EUR; 20160102; DESCRIPTION
6
+ Expenses:Unknown
7
+ Test::Account -€10.00
8
+
@@ -0,0 +1 @@
1
+ -f input.csv --unattended -c € --ignore-columns 4,5 --comma-separates-cents -v --date-format '%Y%m%d' --csv-separator '\t' -a Test::Account
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
- # coding: utf-8
3
2
 
4
3
  require_relative "../spec_helper"
5
4
  require 'rubygems'
@@ -8,24 +7,53 @@ require_relative '../../lib/reckon'
8
7
  describe Reckon::CSVParser do
9
8
  let(:chase) { Reckon::CSVParser.new(file: fixture_path('chase.csv')) }
10
9
  let(:some_other_bank) { Reckon::CSVParser.new(file: fixture_path('some_other.csv')) }
11
- let(:two_money_columns) { Reckon::CSVParser.new(file: fixture_path('two_money_columns.csv')) }
10
+ let(:two_money_columns) {
11
+ Reckon::CSVParser.new(file: fixture_path('two_money_columns.csv'))
12
+ }
12
13
  let(:suntrust_csv) { Reckon::CSVParser.new(file: fixture_path('suntrust.csv')) }
13
14
  let(:simple_csv) { Reckon::CSVParser.new(file: fixture_path('simple.csv')) }
14
- let(:nationwide) { Reckon::CSVParser.new(file: fixture_path('nationwide.csv'), csv_separator: ',', suffixed: true, currency: "POUND") }
15
- let(:german_date) { Reckon::CSVParser.new(file: fixture_path('german_date_example.csv')) }
16
- let(:danish_kroner_nordea) { Reckon::CSVParser.new(file: fixture_path('danish_kroner_nordea_example.csv'), csv_separator: ';', comma_separates_cents: true) }
17
- let(:yyyymmdd_date) { Reckon::CSVParser.new(file: fixture_path('yyyymmdd_date_example.csv')) }
18
- let(:spanish_date) { Reckon::CSVParser.new(file: fixture_path('spanish_date_example.csv'), date_format: '%d/%m/%Y') }
19
- let(:english_date) { Reckon::CSVParser.new(file: fixture_path('english_date_example.csv')) }
20
- let(:ing_csv) { Reckon::CSVParser.new(file: fixture_path('ing.csv'), comma_separates_cents: true ) }
21
- let(:austrian_csv) { Reckon::CSVParser.new(file: fixture_path('austrian_example.csv'), comma_separates_cents: true, csv_separator: ';' ) }
22
- let(:french_csv) { Reckon::CSVParser.new(file: fixture_path('french_example.csv'), csv_separator: ';', comma_separates_cents: true) }
23
- let(:broker_canada) { Reckon::CSVParser.new(file: fixture_path('broker_canada_example.csv')) }
24
- let(:intuit_mint) { Reckon::CSVParser.new(file: fixture_path('intuit_mint_example.csv')) }
15
+ let(:nationwide) {
16
+ Reckon::CSVParser.new(file: fixture_path('nationwide.csv'), csv_separator: ',',
17
+ suffixed: true, currency: "POUND")
18
+ }
19
+ let(:german_date) {
20
+ Reckon::CSVParser.new(file: fixture_path('german_date_example.csv'))
21
+ }
22
+ let(:danish_kroner_nordea) {
23
+ Reckon::CSVParser.new(file: fixture_path('danish_kroner_nordea_example.csv'),
24
+ csv_separator: ';', comma_separates_cents: true)
25
+ }
26
+ let(:yyyymmdd_date) {
27
+ Reckon::CSVParser.new(file: fixture_path('yyyymmdd_date_example.csv'))
28
+ }
29
+ let(:spanish_date) {
30
+ Reckon::CSVParser.new(file: fixture_path('spanish_date_example.csv'),
31
+ date_format: '%d/%m/%Y')
32
+ }
33
+ let(:english_date) {
34
+ Reckon::CSVParser.new(file: fixture_path('english_date_example.csv'))
35
+ }
36
+ let(:ing_csv) {
37
+ Reckon::CSVParser.new(file: fixture_path('ing.csv'), comma_separates_cents: true)
38
+ }
39
+ let(:austrian_csv) {
40
+ Reckon::CSVParser.new(file: fixture_path('austrian_example.csv'),
41
+ comma_separates_cents: true, csv_separator: ';')
42
+ }
43
+ let(:french_csv) {
44
+ Reckon::CSVParser.new(file: fixture_path('french_example.csv'), csv_separator: ';',
45
+ comma_separates_cents: true)
46
+ }
47
+ let(:broker_canada) {
48
+ Reckon::CSVParser.new(file: fixture_path('broker_canada_example.csv'))
49
+ }
50
+ let(:intuit_mint) {
51
+ Reckon::CSVParser.new(file: fixture_path('intuit_mint_example.csv'))
52
+ }
25
53
 
26
54
  describe "parse" do
27
55
  it "should use binary encoding if none specified and chardet fails" do
28
- allow(CharDet).to receive(:detect).and_return({'encoding' => nil})
56
+ allow(CharDet).to receive(:detect).and_return({ 'encoding' => nil })
29
57
  app = Reckon::CSVParser.new(file: fixture_path("extratofake.csv"))
30
58
  expect(app.send(:try_encoding, "foobarbaz")).to eq("BINARY")
31
59
  end
@@ -37,12 +65,16 @@ describe Reckon::CSVParser do
37
65
  end
38
66
 
39
67
  it "should work with other separators" do
40
- Reckon::CSVParser.new(:string => "one;two\nthree;four", :csv_separator => ';').columns.should == [['one', 'three'], ['two', 'four']]
68
+ Reckon::CSVParser.new(:string => "one;two\nthree;four",
69
+ :csv_separator => ';').columns.should == [
70
+ ['one', 'three'], ['two', 'four']
71
+ ]
41
72
  end
42
73
 
43
74
  it 'should parse quoted lines' do
44
75
  file = %q("30.03.2015";"29.03.2015";"09.04.2015";"BARAUSZAHLUNGSENTGELT";"5266 xxxx xxxx 9454";"";"0";"EUR";"0,00";"EUR";"-3,50";"0")
45
- Reckon::CSVParser.new(string: file, csv_separator: ';', comma_separates_cents: true).columns.length.should == 12
76
+ Reckon::CSVParser.new(string: file, csv_separator: ';',
77
+ comma_separates_cents: true).columns.length.should == 12
46
78
  end
47
79
 
48
80
  it 'should parse csv with BOM' do
@@ -50,11 +82,26 @@ describe Reckon::CSVParser do
50
82
  Reckon::CSVParser.new(file: file).columns.length.should == 41
51
83
  end
52
84
 
85
+ it 'should parse multi-line csv fields' do
86
+ file = File.expand_path(fixture_path("multi-line-field.csv"))
87
+ p = Reckon::CSVParser.new(file: file)
88
+ expect(p.columns[0].length).to eq 2
89
+ expected_field = "In case of errors or questions about your\n" +
90
+ " electronic transfers:\n" +
91
+ " This is a multi-line string\n" +
92
+ " "
93
+ expect(p.columns[-1][-1]).to eq expected_field
94
+ end
95
+
53
96
  describe 'file with invalid csv in header' do
54
97
  let(:invalid_file) { fixture_path('invalid_header_example.csv') }
55
98
 
56
99
  it 'should ignore invalid header lines' do
57
- Reckon::CSVParser.new(file: invalid_file, contains_header: 4)
100
+ parser = Reckon::CSVParser.new(file: invalid_file, contains_header: 4)
101
+ expect(parser.csv_data).to eq([
102
+ ["19/02/2016", "VIR RECU 508160",
103
+ "VIR RECU 1234567834S DE: Francois REF: 123457891234567894561231 PROVENANCE: DE Allemagne ", "50,00", "EUR"], ["18/02/2016", "COTISATION JAZZ", "COTISATION JAZZ ", "-8,10", "EUR"]
104
+ ])
58
105
  end
59
106
 
60
107
  it 'should fail' do
@@ -67,19 +114,24 @@ describe Reckon::CSVParser do
67
114
 
68
115
  describe "columns" do
69
116
  it "should return the csv transposed" do
70
- simple_csv.columns.should == [["entry1", "entry4"], ["entry2", "entry5"], ["entry3", "entry6"]]
117
+ simple_csv.columns.should == [["entry1", "entry4"], ["entry2", "entry5"],
118
+ ["entry3", "entry6"]]
71
119
  chase.columns.length.should == 4
72
120
  end
73
121
 
74
122
  it "should be ok with empty lines" do
75
123
  lambda {
76
- Reckon::CSVParser.new(:string => "one,two\nthree,four\n\n\n\n\n").columns.should == [['one', 'three'], ['two', 'four']]
124
+ Reckon::CSVParser.new(:string => "one,two\nthree,four\n\n\n\n\n").columns.should == [
125
+ ['one', 'three'], ['two', 'four']
126
+ ]
77
127
  }.should_not raise_error
78
128
  end
79
129
  end
80
130
 
81
131
  describe "detect_columns" do
82
- let(:harder_date_example_csv) { Reckon::CSVParser.new(file: fixture_path('harder_date_example.csv')) }
132
+ let(:harder_date_example_csv) {
133
+ Reckon::CSVParser.new(file: fixture_path('harder_date_example.csv'))
134
+ }
83
135
 
84
136
  it "should detect the money column" do
85
137
  chase.money_column_indices.should == [3]
@@ -165,13 +217,17 @@ describe Reckon::CSVParser do
165
217
  end
166
218
 
167
219
  it "should handle the comma_separates_cents option correctly" do
168
- european_csv = Reckon::CSVParser.new(:string => "$2,00;something\n1.025,67;something else", :csv_separator => ';', :comma_separates_cents => true)
220
+ european_csv = Reckon::CSVParser.new(
221
+ :string => "$2,00;something\n1.025,67;something else", :csv_separator => ';', :comma_separates_cents => true
222
+ )
169
223
  european_csv.money_for(0).should == 2.00
170
224
  european_csv.money_for(1).should == 1025.67
171
225
  end
172
226
 
173
227
  it "should return negated values if the inverse option is passed" do
174
- inversed_csv = Reckon::CSVParser.new(file: fixture_path('inversed_credit_card.csv'), inverse: true)
228
+ inversed_csv = Reckon::CSVParser.new(
229
+ file: fixture_path('inversed_credit_card.csv'), inverse: true
230
+ )
175
231
  inversed_csv.money_for(0).should == -30.00
176
232
  inversed_csv.money_for(3).should == 500.00
177
233
  end
@@ -229,7 +285,8 @@ describe Reckon::CSVParser do
229
285
  end
230
286
 
231
287
  it "should not append empty description column" do
232
- parser = Reckon::CSVParser.new(:string => '01/09/2015,05354 SUBWAY,8.19,,',:date_format => '%d/%m/%Y')
288
+ parser = Reckon::CSVParser.new(:string => '01/09/2015,05354 SUBWAY,8.19,,',
289
+ :date_format => '%d/%m/%Y')
233
290
  parser.description_for(0).should == '05354 SUBWAY'
234
291
  end
235
292
 
@@ -249,7 +306,8 @@ describe Reckon::CSVParser do
249
306
  end
250
307
 
251
308
  it "work with other currencies such as €" do
252
- euro_bank = Reckon::CSVParser.new(file: fixture_path('some_other.csv'), currency: "€", suffixed: false )
309
+ euro_bank = Reckon::CSVParser.new(file: fixture_path('some_other.csv'),
310
+ currency: "€", suffixed: false)
253
311
  euro_bank.pretty_money_for(1).should == "-€20.00"
254
312
  euro_bank.pretty_money_for(4).should == " €1,558.52"
255
313
  euro_bank.pretty_money_for(7).should == "-€116.22"
@@ -258,7 +316,8 @@ describe Reckon::CSVParser do
258
316
  end
259
317
 
260
318
  it "work with suffixed currencies such as SEK" do
261
- swedish_bank = Reckon::CSVParser.new(file: fixture_path('some_other.csv'), currency: 'SEK', suffixed: true )
319
+ swedish_bank = Reckon::CSVParser.new(file: fixture_path('some_other.csv'),
320
+ currency: 'SEK', suffixed: true)
262
321
  swedish_bank.pretty_money_for(1).should == "-20.00 SEK"
263
322
  swedish_bank.pretty_money_for(4).should == " 1,558.52 SEK"
264
323
  swedish_bank.pretty_money_for(7).should == "-116.22 SEK"
@@ -274,7 +333,7 @@ describe Reckon::CSVParser do
274
333
 
275
334
  describe '85 regression test' do
276
335
  it 'should detect correct date column' do
277
- p = Reckon::CSVParser.new(file:fixture_path('85-date-example.csv'))
336
+ p = Reckon::CSVParser.new(file: fixture_path('85-date-example.csv'))
278
337
  expect(p.date_column_index).to eq(2)
279
338
  end
280
339
  end
@@ -50,4 +50,10 @@ describe Reckon::DateColumn do
50
50
  .to eq('2013-12-13')
51
51
  end
52
52
  end
53
+
54
+ describe "#likelihood" do
55
+ it "should prefer numbers that looks like dates" do
56
+ expect(Reckon::DateColumn.likelihood("123456789")).to be < Reckon::DateColumn.likelihood("20160102")
57
+ end
58
+ end
53
59
  end