qif 0.5 → 1.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,3 +1,5 @@
1
+ v1.0 Added account types and fields from QIF specification. [elmatou]
2
+
1
3
  v0.5 Writes qif files without a record separator after the header. [jscipione]
2
4
 
3
5
  v0.4 Follow QIF specification that an end of entry should not appear under a
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2010 Jeremy Wells
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
@@ -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. This
23
- # defaults to 'dd/mm/yyyy', but also accepts 'mm/dd/yyyy'.
24
- def initialize(data, format = 'dd/mm/yyyy')
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
- rescue EOFError => e
136
- @data.close
137
- nil
138
- rescue Exception => e
139
- nil
158
+ rescue EOFError => e
159
+ @data.close
160
+ nil
161
+ rescue Exception => e
162
+ nil
140
163
  end
141
164
  end
142
165
  end
@@ -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
- attr_accessor :date, :amount, :name, :description, :reference
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
- Transaction.new(
12
- :date => record['D'],
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
- @date = attributes[:date]
22
- @amount = attributes[:amount]
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
- 'D' => DateFormat.new(format).format(date),
33
- 'T' => '%.2f' % amount,
34
- 'L' => name,
35
- 'M' => description,
36
- 'P' => reference
37
- }.map{|k,v| "#{k}#{v}" }.join("\n")
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
@@ -2,11 +2,11 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = %q{qif}
5
- s.version = "0.5"
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{2010-11-24}
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.3.7}
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
@@ -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.name.should == 'Debit'
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.name.should == 'Debit'
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.name.should == 'Credit'
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' => date,
9
- 'T' => '10.0',
10
- 'L' => 'Credit',
11
- 'M' => 'Supermarket',
12
- 'P' => 'abcde'
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 == date
16
- t.amount.should == 10.0
17
- t.name.should == 'Credit'
18
- t.description.should == 'Supermarket'
19
- t.reference.should == 'abcde'
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
- :name => 'Debit',
33
- :description => 'Supermarket',
34
- :reference => 'abcde'
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 name in L' do
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
@@ -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, :name => 'Credit')
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, :name => 'Credit')
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: 1
5
- prerelease: false
4
+ hash: 15
5
+ prerelease:
6
6
  segments:
7
+ - 1
7
8
  - 0
8
- - 5
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: 2010-11-24 00:00:00 +13:00
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.3.7
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.