qif 0.5 → 1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +2 -0
- data/LICENSE +2 -2
- data/Rakefile +7 -0
- data/lib/qif/reader.rb +42 -19
- data/lib/qif/transaction.rb +51 -24
- data/qif.gemspec +3 -4
- data/spec/lib/reader_spec.rb +35 -3
- data/spec/lib/transaction_spec.rb +34 -17
- data/spec/lib/writer_spec.rb +2 -2
- metadata +6 -6
data/CHANGELOG
CHANGED
data/LICENSE
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
Copyright (c)
|
1
|
+
Copyright (c) 2011 Jeremy Wells
|
2
2
|
|
3
3
|
Permission is hereby granted, free of charge, to any person
|
4
4
|
obtaining a copy of this software and associated documentation
|
@@ -19,4 +19,4 @@ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
19
19
|
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
20
20
|
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
21
21
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
-
OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
CHANGED
@@ -1,6 +1,13 @@
|
|
1
1
|
require 'echoe'
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
|
2
4
|
Echoe.new('qif') do |gem|
|
3
5
|
gem.author = "Jeremy Wells"
|
4
6
|
gem.summary = "A library for reading and writing quicken QIF files."
|
5
7
|
gem.email = "jemmyw@gmail.com"
|
6
8
|
end
|
9
|
+
|
10
|
+
desc "Run specs"
|
11
|
+
RSpec::Core::RakeTask.new :spec
|
12
|
+
|
13
|
+
task :default => :spec
|
data/lib/qif/reader.rb
CHANGED
@@ -16,15 +16,26 @@ module Qif
|
|
16
16
|
class Reader
|
17
17
|
include Enumerable
|
18
18
|
|
19
|
+
SUPPORTED_ACCOUNTS = {
|
20
|
+
"!Type:Bank" => "Bank account transactions",
|
21
|
+
"!Type:Cash" => "Cash account transactions",
|
22
|
+
"!Type:CCard" => "Credit card account transactions",
|
23
|
+
"!Type:Oth A" => "Asset account transactions",
|
24
|
+
"!Type:Oth L" => "Liability account transactions"
|
25
|
+
}
|
26
|
+
|
27
|
+
class UnknownAccountType < StandardError; end
|
28
|
+
|
19
29
|
# Create a new Qif::Reader object. The data argument must be
|
20
30
|
# either an IO object or a String containing the Qif file data.
|
21
31
|
#
|
22
|
-
# The format argument specifies the date format in the file.
|
23
|
-
# defaults to 'dd/mm/yyyy'
|
24
|
-
def initialize(data, format =
|
25
|
-
@format = DateFormat.new(format)
|
32
|
+
# The optional format argument specifies the date format in the file. Giving a format will force it, otherwise the format will guissed reading the transactions in the file, this
|
33
|
+
# defaults to 'dd/mm/yyyy' if guessing method fails.
|
34
|
+
def initialize(data, format = nil)
|
26
35
|
@data = data.respond_to?(:read) ? data : StringIO.new(data.to_s)
|
36
|
+
@format = DateFormat.new(format || guess_date_format || 'dd/mm/yyyy')
|
27
37
|
read_header
|
38
|
+
raise(UnknownAccountType, "Unknown account type. Should be one of followings :\n#{SUPPORTED_ACCOUNTS.keys.inspect}") unless SUPPORTED_ACCOUNTS.keys.collect(&:downcase).include? @header.downcase
|
28
39
|
reset
|
29
40
|
end
|
30
41
|
|
@@ -57,7 +68,22 @@ module Qif
|
|
57
68
|
transaction_cache.size
|
58
69
|
end
|
59
70
|
alias length size
|
60
|
-
|
71
|
+
|
72
|
+
# Guess the file format of dates, reading the beginning of file, or return nil if no dates are found (?!).
|
73
|
+
def guess_date_format
|
74
|
+
begin
|
75
|
+
line = @data.gets
|
76
|
+
break if line.nil?
|
77
|
+
date = line.strip.scan(/^D(\d{1,2}).(\d{1,2}).(\d{2,4})/).flatten
|
78
|
+
if date.count == 3
|
79
|
+
guessed_format = date[0].to_i.between?(1, 12) ? (date[1].to_i.between?(1, 12) ? nil : 'mm/dd') : 'dd/mm'
|
80
|
+
guessed_format += '/' + 'y'*date[2].length if guessed_format
|
81
|
+
end
|
82
|
+
end until guessed_format
|
83
|
+
@data.rewind
|
84
|
+
guessed_format
|
85
|
+
end
|
86
|
+
|
61
87
|
private
|
62
88
|
|
63
89
|
def read_all_transactions
|
@@ -90,7 +116,7 @@ module Qif
|
|
90
116
|
@data.readline
|
91
117
|
end
|
92
118
|
end
|
93
|
-
|
119
|
+
|
94
120
|
def read_header
|
95
121
|
headers = []
|
96
122
|
begin
|
@@ -99,7 +125,7 @@ module Qif
|
|
99
125
|
end until line !~ /^!/
|
100
126
|
|
101
127
|
@header = headers.shift
|
102
|
-
@options = headers.map{|h| h.split(':').last
|
128
|
+
@options = headers.map{|h| h.split(':') }.last
|
103
129
|
|
104
130
|
unless line =~ /^\^/
|
105
131
|
rewind_to @data.lineno - 1
|
@@ -119,24 +145,21 @@ module Qif
|
|
119
145
|
|
120
146
|
def read_record
|
121
147
|
record = {}
|
122
|
-
|
123
148
|
begin
|
124
149
|
line = @data.readline
|
125
150
|
key = line[0,1]
|
151
|
+
record[key] = record.key?(key) ? record[key] + "\n" + line[1..-1].strip : line[1..-1].strip
|
152
|
+
|
153
|
+
record[key].sub!(',','') if %w(T U $).include? key
|
154
|
+
record[key] = @format.parse(record[key]) if %w(D).include? key
|
126
155
|
|
127
|
-
record[key] = line[1..-1].strip
|
128
|
-
|
129
|
-
if date = @format.parse(record[key])
|
130
|
-
record[key] = date
|
131
|
-
end
|
132
156
|
end until line =~ /^\^/
|
133
|
-
|
134
157
|
record
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
158
|
+
rescue EOFError => e
|
159
|
+
@data.close
|
160
|
+
nil
|
161
|
+
rescue Exception => e
|
162
|
+
nil
|
140
163
|
end
|
141
164
|
end
|
142
165
|
end
|
data/lib/qif/transaction.rb
CHANGED
@@ -3,38 +3,65 @@ require 'qif/date_format'
|
|
3
3
|
module Qif
|
4
4
|
# The Qif::Transaction class represents transactions in a qif file.
|
5
5
|
class Transaction
|
6
|
-
|
7
|
-
|
6
|
+
SUPPORTED_FIELDS = {
|
7
|
+
:date => {"D" => "Date"},
|
8
|
+
:amount => {"T" => "Amount"},
|
9
|
+
:status => {"C" => "Cleared status"},
|
10
|
+
:number => {"N" => "Num (check or reference number)"},
|
11
|
+
:payee => {"P" => "Payee"},
|
12
|
+
:memo => {"M" => "Memo"},
|
13
|
+
:adress => {"A" => "Address (up to five lines; the sixth line is an optional message)"},
|
14
|
+
:category => {"L" => "Category (Category/Subcategory/Transfer/Class)"},
|
15
|
+
:split_category => {"S" => "Category in split (Category/Transfer/Class)"},
|
16
|
+
:split_memo => {"E" => "Memo in split"},
|
17
|
+
:split_amount => {"$" => "Dollar amount of split"},
|
18
|
+
:end => {"^" => "End of entry"}
|
19
|
+
}
|
20
|
+
DEPRECATION_FIELDS = {
|
21
|
+
:reference => :payee,
|
22
|
+
:name => :category,
|
23
|
+
:description => :memo
|
24
|
+
}
|
25
|
+
SUPPORTED_FIELDS.keys.each{|s| attr_accessor s}
|
26
|
+
|
8
27
|
def self.read(record) #::nodoc
|
9
28
|
return nil unless record['D'].respond_to?(:strftime)
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
:amount => record['T'].to_f,
|
14
|
-
:name => record['L'],
|
15
|
-
:description => record['M'],
|
16
|
-
:reference => record['P']
|
17
|
-
)
|
29
|
+
SUPPORTED_FIELDS.each{|k,v| record[k] = record.delete(v.keys.first)}
|
30
|
+
record.reject{|k,v| v.nil?}.each{|k,v| record[k] = record[k].to_f if k.to_s.include? "amount"}
|
31
|
+
Transaction.new record
|
18
32
|
end
|
19
|
-
|
33
|
+
|
20
34
|
def initialize(attributes = {})
|
21
|
-
|
22
|
-
|
23
|
-
@name = attributes[:name]
|
24
|
-
@description = attributes[:description]
|
25
|
-
@reference = attributes[:reference]
|
35
|
+
deprecate_attributes!(attributes)
|
36
|
+
SUPPORTED_FIELDS.keys.each{|s| instance_variable_set("@#{s.to_s}", attributes[s])}
|
26
37
|
end
|
27
|
-
|
38
|
+
|
28
39
|
# Returns a representation of the transaction as it
|
29
40
|
# would appear in a qif file.
|
30
41
|
def to_s(format = 'dd/mm/yyyy')
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
42
|
+
SUPPORTED_FIELDS.collect do |k,v|
|
43
|
+
next unless current = instance_variable_get("@#{k}")
|
44
|
+
field = v.keys
|
45
|
+
case current.class.to_s
|
46
|
+
when "Time"
|
47
|
+
"#{field}#{DateFormat.new(format).format(current)}"
|
48
|
+
when "Float"
|
49
|
+
"#{field}#{'%.2f'%current}"
|
50
|
+
when "String"
|
51
|
+
current.split("\n").collect {|x| "#{field}#{x}" }
|
52
|
+
else
|
53
|
+
"#{field}#{current}"
|
54
|
+
end
|
55
|
+
end.flatten.compact.join("\n")
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def deprecate_attributes!(attributes = {})
|
61
|
+
attributes.select {|k,v| DEPRECATION_FIELDS.keys.include? k}.each do |key,value|
|
62
|
+
warn("DEPRECATION WARNING : :#{key} HAS BEEN DEPRECATED IN FAVOR OF :#{DEPRECATION_FIELDS[key]} IN ORDER TO COMPLY WITH QIF SPECS.")
|
63
|
+
attributes[DEPRECATION_FIELDS[key]] = attributes.delete(key)
|
64
|
+
end
|
38
65
|
end
|
39
66
|
end
|
40
67
|
end
|
data/qif.gemspec
CHANGED
@@ -2,11 +2,11 @@
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |s|
|
4
4
|
s.name = %q{qif}
|
5
|
-
s.version = "0
|
5
|
+
s.version = "1.0"
|
6
6
|
|
7
7
|
s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
|
8
8
|
s.authors = ["Jeremy Wells"]
|
9
|
-
s.date = %q{
|
9
|
+
s.date = %q{2011-02-19}
|
10
10
|
s.description = %q{A library for reading and writing quicken QIF files.}
|
11
11
|
s.email = %q{jemmyw@gmail.com}
|
12
12
|
s.extra_rdoc_files = ["CHANGELOG", "LICENSE", "lib/qif.rb", "lib/qif/date_format.rb", "lib/qif/reader.rb", "lib/qif/transaction.rb", "lib/qif/writer.rb"]
|
@@ -15,11 +15,10 @@ Gem::Specification.new do |s|
|
|
15
15
|
s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Qif", "--main", "README.rdoc"]
|
16
16
|
s.require_paths = ["lib"]
|
17
17
|
s.rubyforge_project = %q{qif}
|
18
|
-
s.rubygems_version = %q{1.
|
18
|
+
s.rubygems_version = %q{1.5.0}
|
19
19
|
s.summary = %q{A library for reading and writing quicken QIF files.}
|
20
20
|
|
21
21
|
if s.respond_to? :specification_version then
|
22
|
-
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
23
22
|
s.specification_version = 3
|
24
23
|
|
25
24
|
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
data/spec/lib/reader_spec.rb
CHANGED
@@ -8,21 +8,21 @@ shared_examples_for "3 record files" do
|
|
8
8
|
it 'should have a debit of $10 on the 1st of January 2010' do
|
9
9
|
transaction = instance.transactions.detect{|t| t.date == Time.mktime(2010, 1, 1)}
|
10
10
|
transaction.should_not be_nil
|
11
|
-
transaction.
|
11
|
+
transaction.category.should == 'Debit'
|
12
12
|
transaction.amount.should == -10.0
|
13
13
|
end
|
14
14
|
|
15
15
|
it 'should have a debit of $20 on the 1st of June 1020' do
|
16
16
|
transaction = instance.transactions.detect{|t| t.date == Time.mktime(2010, 6, 1)}
|
17
17
|
transaction.should_not be_nil
|
18
|
-
transaction.
|
18
|
+
transaction.category.should == 'Debit'
|
19
19
|
transaction.amount.should == -20.0
|
20
20
|
end
|
21
21
|
|
22
22
|
it 'should have a credit of $30 on the 29th of December 2010' do
|
23
23
|
transaction = instance.transactions.detect{|t| t.date == Time.mktime(2010, 12, 29)}
|
24
24
|
transaction.should_not be_nil
|
25
|
-
transaction.
|
25
|
+
transaction.category.should == 'Credit'
|
26
26
|
transaction.amount.should == 30.0
|
27
27
|
end
|
28
28
|
|
@@ -52,6 +52,38 @@ describe Qif::Reader do
|
|
52
52
|
end
|
53
53
|
end
|
54
54
|
|
55
|
+
it 'should reject the wrong account type !Type:Invst and raise an UnknownAccountType exception' do
|
56
|
+
expect{ Qif::Reader.new(open('spec/fixtures/quicken_investment_account.qif')) }.to raise_error(Qif::Reader::UnknownAccountType)
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'should guess the date format dd/mm/yyyy' do
|
60
|
+
@instance = Qif::Reader.new(open('spec/fixtures/3_records_ddmmyyyy.qif'))
|
61
|
+
@instance.guess_date_format.should == 'dd/mm/yyyy'
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'should guess the date format mm/dd/yy' do
|
65
|
+
@instance = Qif::Reader.new(open('spec/fixtures/3_records_mmddyy.qif'))
|
66
|
+
@instance.guess_date_format.should == 'mm/dd/yy'
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'shouldn\t guess the date format because transactions are ambiguious, fall back on default dd/mm/yyyy and fail' do
|
70
|
+
@instance = Qif::Reader.new(open('spec/fixtures/quicken_non_investement_account.qif'))
|
71
|
+
@instance.guess_date_format.should == nil
|
72
|
+
@instance.size.should == 0
|
73
|
+
end
|
74
|
+
|
75
|
+
# TODO Date parser should be more flexible and efficient, probably using Date.strptime(str, format)
|
76
|
+
# it 'should initialize if leading zeros are missing too' do
|
77
|
+
# @instance = Qif::Reader.new(open('spec/fixtures/3_records_dmyy.qif'))
|
78
|
+
# @instance.size.should == 3
|
79
|
+
# end
|
80
|
+
|
81
|
+
it 'should should parse amounts with comma separator too' do
|
82
|
+
@instance = Qif::Reader.new(open('spec/fixtures/3_records_separator.qif'))
|
83
|
+
@instance.size.should == 3
|
84
|
+
@instance.collect(&:amount).should == [-1010.0, -30020.0, 30.0]
|
85
|
+
end
|
86
|
+
|
55
87
|
it 'should initialize with an io object' do
|
56
88
|
@instance = Qif::Reader.new(open('spec/fixtures/3_records_ddmmyyyy.qif'))
|
57
89
|
@instance.size.should == 3
|
@@ -2,21 +2,38 @@ require 'spec/spec_helper'
|
|
2
2
|
|
3
3
|
describe Qif::Transaction do
|
4
4
|
describe '::read' do
|
5
|
-
it 'should return a new transaction' do
|
6
|
-
date = Time.now
|
5
|
+
it 'should return a new transaction with all attributes set' do
|
7
6
|
t = Qif::Transaction.read(
|
8
|
-
'D' =>
|
9
|
-
'T' => '
|
10
|
-
'
|
11
|
-
'
|
12
|
-
'P' => '
|
7
|
+
'D' => Time.parse('06/ 1/94'),
|
8
|
+
'T' => '-1000.00'.to_f,
|
9
|
+
'C' => 'X',
|
10
|
+
'N' => '1005',
|
11
|
+
'P' => 'Bank Of Mortgage',
|
12
|
+
'M' => 'aMemo',
|
13
|
+
'L' => 'aCategory',
|
14
|
+
# TODO Support correctly splits with an array of hash
|
15
|
+
# 'S' => '[linda]
|
16
|
+
#Mort Int',
|
17
|
+
# 'E' => 'Cash',
|
18
|
+
# '$' => '-253.64
|
19
|
+
#=746.36',
|
20
|
+
'A' => 'P.O. Box 27027
|
21
|
+
Tucson, AZ
|
22
|
+
85726',
|
23
|
+
'^' => nil
|
13
24
|
)
|
25
|
+
|
14
26
|
t.should be_a(Qif::Transaction)
|
15
|
-
t.date.should ==
|
16
|
-
t.amount.should ==
|
17
|
-
t.
|
18
|
-
t.
|
19
|
-
t.
|
27
|
+
t.date.should == Time.mktime(1994,6,1)
|
28
|
+
t.amount.should == -1000.00
|
29
|
+
t.status.should == 'X'
|
30
|
+
t.number.should == '1005'
|
31
|
+
t.payee.should == 'Bank Of Mortgage'
|
32
|
+
t.memo.should == 'aMemo'
|
33
|
+
t.category.should == 'aCategory'
|
34
|
+
t.adress.should == 'P.O. Box 27027
|
35
|
+
Tucson, AZ
|
36
|
+
85726'
|
20
37
|
end
|
21
38
|
|
22
39
|
it 'should return nil if the date does not respond to strftime' do
|
@@ -29,9 +46,9 @@ describe Qif::Transaction do
|
|
29
46
|
@instance = Qif::Transaction.new(
|
30
47
|
:date => Time.mktime(2010, 1, 2),
|
31
48
|
:amount => -10.0,
|
32
|
-
:
|
33
|
-
:
|
34
|
-
:
|
49
|
+
:category => 'Debit',
|
50
|
+
:memo => 'Supermarket',
|
51
|
+
:payee => 'abcde'
|
35
52
|
)
|
36
53
|
end
|
37
54
|
|
@@ -44,7 +61,7 @@ describe Qif::Transaction do
|
|
44
61
|
@instance.to_s.should include('T-10.00')
|
45
62
|
end
|
46
63
|
|
47
|
-
it 'should put the
|
64
|
+
it 'should put the category in L' do
|
48
65
|
@instance.to_s.should include('LDebit')
|
49
66
|
end
|
50
67
|
|
@@ -56,4 +73,4 @@ describe Qif::Transaction do
|
|
56
73
|
@instance.to_s.should include('Pabcde')
|
57
74
|
end
|
58
75
|
end
|
59
|
-
end
|
76
|
+
end
|
data/spec/lib/writer_spec.rb
CHANGED
@@ -27,7 +27,7 @@ describe Qif::Writer do
|
|
27
27
|
date = Time.now
|
28
28
|
|
29
29
|
Qif::Writer.open(@path) do |writer|
|
30
|
-
writer << Qif::Transaction.new(:date => date, :amount => 10.0, :
|
30
|
+
writer << Qif::Transaction.new(:date => date, :amount => 10.0, :category => 'Credit')
|
31
31
|
end
|
32
32
|
|
33
33
|
@buffer.should include('D%s' % date.strftime('%d/%m/%Y'))
|
@@ -50,7 +50,7 @@ describe Qif::Writer do
|
|
50
50
|
|
51
51
|
it 'should write any pending transactions' do
|
52
52
|
date = Time.now
|
53
|
-
@instance << Qif::Transaction.new(:date => date, :amount => 10.0, :
|
53
|
+
@instance << Qif::Transaction.new(:date => date, :amount => 10.0, :category => 'Credit')
|
54
54
|
|
55
55
|
@buffer.should_not include('D%s' % date.strftime('%d/%m/%Y'))
|
56
56
|
@instance.write
|
metadata
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: qif
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
5
|
-
prerelease:
|
4
|
+
hash: 15
|
5
|
+
prerelease:
|
6
6
|
segments:
|
7
|
+
- 1
|
7
8
|
- 0
|
8
|
-
|
9
|
-
version: "0.5"
|
9
|
+
version: "1.0"
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Jeremy Wells
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date:
|
17
|
+
date: 2011-02-19 00:00:00 +13:00
|
18
18
|
default_executable:
|
19
19
|
dependencies: []
|
20
20
|
|
@@ -89,7 +89,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
89
89
|
requirements: []
|
90
90
|
|
91
91
|
rubyforge_project: qif
|
92
|
-
rubygems_version: 1.
|
92
|
+
rubygems_version: 1.5.0
|
93
93
|
signing_key:
|
94
94
|
specification_version: 3
|
95
95
|
summary: A library for reading and writing quicken QIF files.
|