qif 0.5 → 1.0
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.
- 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.
|