reckon 0.9.0 → 0.9.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.
@@ -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