File Coverage

blib/lib/Finance/QIF.pm
Criterion Covered Total %
statement 191 205 93.1
branch 90 104 86.5
condition 57 72 79.1
subroutine 18 19 94.7
pod 9 9 100.0
total 365 409 89.2


line stmt bran cond sub pod time code
1             package Finance::QIF;
2              
3 1     1   52810 use 5.006;
  1         3  
  1         42  
4 1     1   5 use strict;
  1         2  
  1         32  
5 1     1   5 use warnings;
  1         7  
  1         34  
6 1     1   6 use Carp;
  1         1  
  1         70  
7 1     1   1065 use IO::File;
  1         1095  
  1         4154  
8              
9             our $VERSION = '3.02';
10             $VERSION = eval $VERSION;
11              
12             my %noninvestment = (
13             "D" => "date",
14             "T" => "transaction",
15             "U" => "total", #Quicken 2005 added this which is usually the same
16             #as T but can sometimes be higher.
17             "C" => "status",
18             "N" => "number",
19             "P" => "payee",
20             "M" => "memo",
21             "A" => "address",
22             "L" => "category",
23             "S" => "splits"
24             );
25              
26             my %split = (
27             "S" => "category",
28             "E" => "memo",
29             '$' => "amount"
30             );
31              
32             my %investment = (
33             "D" => "date",
34             "N" => "action",
35             "Y" => "security",
36             "I" => "price",
37             "Q" => "quantity",
38             "T" => "transaction",
39             "U" => "total", #Quicken 2005 added this which is usually the same
40             #as T but can sometimes be higher.
41             "C" => "status",
42             "P" => "text",
43             "M" => "memo",
44             "O" => "commission",
45             "L" => "account",
46             '$' => "amount"
47             );
48              
49             my %account = (
50             "N" => "name",
51             "D" => "description",
52             "L" => "limit",
53             "X" => "tax",
54             "A" => "note",
55             "T" => "type",
56             "B" => "balance"
57             );
58              
59             my %category = (
60             "N" => "name",
61             "D" => "description",
62             "B" => "budget",
63             "E" => "expense",
64             "I" => "income",
65             "T" => "tax",
66             "R" => "schedule"
67             );
68              
69             my %class = (
70             "N" => "name",
71             "D" => "description"
72             );
73              
74             my %memorized = (
75             "K" => "type",
76             "T" => "transaction",
77             "U" => "total", #Quicken 2005 added this which is usually the same as
78             #as T but can sometimes be higher.
79             "C" => "status",
80             "P" => "payee",
81             "M" => "memo",
82             "A" => "address",
83             "L" => "category",
84             "S" => "splits",
85             "N" => "action", #Quicken 2006 added N, Y, I, Q, $ for investment
86             "Y" => "security",
87             "I" => "price",
88             "Q" => "quantity",
89             '$' => "amount",
90             "1" => "first",
91             "2" => "years",
92             "3" => "made",
93             "4" => "periods",
94             "5" => "interest",
95             "6" => "balance",
96             "7" => "loan"
97             );
98              
99             my %security = (
100             "N" => "security",
101             "S" => "symbol",
102             "T" => "type",
103             "G" => "goal",
104             );
105              
106             my %budget = (
107             "N" => "name",
108             "D" => "description",
109             "E" => "expense",
110             "I" => "income",
111             "T" => "tax",
112             "R" => "schedule",
113             "B" => "budget"
114             );
115              
116             my %payee = (
117             "P" => "name",
118             "A" => "address",
119             "C" => "city",
120             "S" => "state",
121             "Z" => "zip",
122             "Y" => "country",
123             "N" => "phone",
124             "#" => "account"
125             );
126              
127             my %prices = (
128             "S" => "symbol",
129             "P" => "price"
130             );
131              
132             my %price = (
133             "C" => "close",
134             "D" => "date",
135             "X" => "max",
136             "I" => "min",
137             "V" => "volume"
138             );
139              
140             my %nofields = ();
141              
142             my %header = (
143             "Type:Bank" => \%noninvestment,
144             "Type:Cash" => \%noninvestment,
145             "Type:CCard" => \%noninvestment,
146             "Type:Invst" => \%investment,
147             "Type:Oth A" => \%noninvestment,
148             "Type:Oth L" => \%noninvestment,
149             "Account" => \%account,
150             "Type:Cat" => \%category,
151             "Type:Class" => \%class,
152             "Type:Memorized" => \%memorized,
153             "Type:Security" => \%security,
154             "Type:Budget" => \%budget,
155             "Type:Payee" => \%payee,
156             "Type:Prices" => \%prices,
157             "Option:AutoSwitch" => \%nofields,
158             "Option:AllXfr" => \%nofields,
159             "Clear:AutoSwitch" => \%nofields
160             );
161              
162             sub new {
163 25     25 1 39418 my $class = shift;
164 25         91 my %opt = @_;
165 25         58 my $self = {};
166              
167 25   100     255 $self->{debug} = $opt{debug} || 0;
168 25   100     122 $self->{autodetect} = $opt{autodetect} || 0;
169 25   100     267 $self->{trim_white_space} = $opt{trim_white_space} || 0;
170 25   66     119 $self->{record_separator} = $opt{record_separator} || $/;
171              
172 25         60 bless( $self, $class );
173              
174 25 100       70 if ( $opt{file} ) {
175 15         54 $self->file( $opt{file} );
176 15         44 $self->open;
177             }
178 25         214 return $self;
179             }
180              
181             sub file {
182 55     55 1 701 my $self = shift;
183 55 100       121 if (@_) {
184 19 100       75 my @file = ( ref( $_[0] ) eq "ARRAY" ? @{ shift @_ } : (), @_ );
  1         4  
185 19         71 $self->{file} = [@file];
186             }
187 55 100       118 if ( $self->{file} ) {
188 53 100       155 return wantarray ? @{ $self->{file} } : $self->{file}->[0];
  17         68  
189             }
190             else {
191 2         10 return undef;
192             }
193             }
194              
195             sub record_separator {
196 7376     7376 1 20287 my $self = shift;
197 7376         33200 return $self->{record_separator};
198             }
199              
200             sub _filehandle {
201 18637     18637   27488 my $self = shift;
202 18637 100       61413 if (@_) {
203 16         32 my @args = @_;
204 16 50       119 $self->{_filehandle} = IO::File->new(@args)
205             or croak("Failed to open file '$args[0]': $!");
206 16         2876 binmode( $self->{_filehandle} );
207 16         46 $self->{_linecount} = 0;
208             }
209 18637 100       44334 if ( !$self->{_filehandle} ) {
210 5         940 croak("No filehandle available");
211             }
212 18632         273920 return $self->{_filehandle};
213             }
214              
215             sub open {
216 17     17 1 38 my $self = shift;
217 17 100       46 if (@_) {
218 1         5 $self->file(@_);
219             }
220 17 100       45 if ( $self->file ) {
221 16         33 $self->_filehandle( $self->file );
222 16 100       134 if ( $self->{autodetect} ) {
223 8 100       28 if ( $self->_filehandle->seek( -2, 2 ) ) {
224 7         445 my $buffer = "";
225 7         29 $self->_filehandle->read( $buffer, 2 );
226 7 100       183 if ( $buffer eq "\015\012" ) {
    100          
    50          
227 1         5 $self->{record_separator} = "\015\012";
228             }
229             elsif ( $buffer =~ /\012$/ ) {
230 5         15 $self->{record_separator} = "\012";
231             }
232             elsif ( $buffer =~ /\015$/ ) {
233 1         3 $self->{record_separator} = "\015";
234             }
235             }
236             }
237 16         70 $self->reset();
238             }
239             else {
240 1         111 croak("No file specified");
241             }
242             }
243              
244             sub next {
245 697     697 1 531389 my $self = shift;
246 697         1260 my %object;
247 697         2477 my $continue = 1;
248 697         844 my $csplit; # Need to keep track of current split for adding split values
249 697 100       1604 if ( $self->_filehandle->eof ) {
250 1         10 return undef;
251             }
252 695 50       8571 if ( exists( $self->{header} ) ) {
253 695         2268 $object{header} = $self->{header};
254             }
255 695   100     1739 while ( !$self->_filehandle->eof && $continue ) {
256 7368         77844 my $line = $self->_getline;
257 7368 100       24017 next if ( $line =~ /^\s*$/ );
258 7366         17863 my ( $field, $value ) = $self->_parseline($line);
259 7366 100       15020 if ( $field eq '!' ) {
260 75         360 $value =~ s/\s+$//; # Headers sometimes have trailing white space
261 75         134 $self->{header} = $value;
262 75         290 $object{header} = $value;
263 75 50       314 if ( !exists( $header{$value} ) ) {
264 0         0 $self->_warning("Unknown header format '$value'");
265             }
266             }
267             else {
268 7291 100       13835 if ( $field eq '^' ) {
269 695         2038 $continue = 0;
270             }
271             else {
272 6596 50 33     95084 if (
    100 100        
    100 100        
    100 100        
    100 100        
    100 100        
    50          
273             !exists( $header{ $object{header} } )
274             && !(
275             exists( $header{"split"} )
276             && ( $object{header} eq "noninvestment"
277             || $object{header} eq "memorized" )
278             )
279             )
280             {
281 0         0 $self->_warning(
282             "Unknown header '$object{header}' can't process line");
283             }
284             elsif ( $object{header} eq "Type:Prices" ) {
285 132         228 $object{"symbol"} = $field;
286 132         168 push( @{ $object{"prices"} }, $value );
  132         510  
287             }
288             elsif ($field eq 'A'
289             && $header{ $object{header} }{$field} eq "address" )
290             {
291 215 100       592 if ( $self->{header} eq "Type:Payee" ) {
292              
293             # The address fields are numbered for this record type
294 9 50       23 if ( length($value) == 0 ) {
295 0         0 $self->_warning( 'Improper address record for '
296             . 'this record type' );
297             }
298             else {
299 9         20 $value = substr( $value, 1 );
300             }
301             }
302 215 100 100     1175 if ( exists( $object{ $header{ $object{header} }{$field} } )
303             && $object{ $header{ $object{header} }{$field} } ne "" )
304             {
305 6         17 $object{ $header{ $object{header} }{$field} } .= "\n";
306             }
307 215         873 $object{ $header{ $object{header} }{$field} } .= $value;
308             }
309             elsif ($field eq 'S'
310             && $header{ $object{header} }{$field} eq "splits" )
311             {
312 24         39 my %mysplit; # We assume "S" always appears first
313 24         77 $mysplit{ $split{$field} } = $value;
314 24         34 push( @{ $object{splits} }, \%mysplit );
  24         104  
315 24         90 $csplit = \%mysplit;
316             }
317             elsif ( ( $field eq 'E' || $field eq '$' ) && $csplit ) {
318              
319             # this currently assumes the "S" was found first
320 46         198 $csplit->{ $split{$field} } = $value;
321             }
322             elsif ($field eq 'B'
323             && $header{ $object{header} }{$field} eq "budget" )
324             {
325 3492         4404 push( @{ $object{budget} }, $value );
  3492         14717  
326             }
327             elsif ( exists( $header{ $object{header} }{$field} ) ) {
328 2687         20005 $object{ $header{ $object{header} }{$field} } = $value;
329             }
330             else {
331 0         0 $self->_warning("Unknown field '$field'");
332             }
333             }
334             }
335             }
336              
337             # Must check that we have a valid record to return
338 695 50       8254 if ( scalar( keys %object ) > 1 ) {
339 695         2855 return \%object;
340             }
341             else {
342 0         0 return undef;
343             }
344             }
345              
346             sub _parseline {
347 7366     7366   9823 my $self = shift;
348 7366         10105 my $line = shift;
349 7366         16466 my @result;
350 7366 100 66     58316 if ( $line !~ /^!/
      100        
351             && exists( $self->{header} )
352             && $self->{header} eq "Type:Prices" )
353             {
354 141         181 my %price;
355 141         536 $line =~ s/\"//g;
356 141         670 my @data = split( ",", $line );
357 141         269 $result[0] = $data[0];
358 141         284 $price{"close"} = $data[1];
359 141         232 $price{"date"} = $data[2];
360 141 100       341 if ( scalar(@data) > 3 ) {
361 120         204 $price{"max"} = $data[3];
362 120         168 $price{"min"} = $data[4];
363 120         211 $price{"volume"} = $data[5];
364             }
365 141         370 $result[1] = \%price;
366             }
367             else {
368 7225         16791 $result[0] = substr( $line, 0, 1 );
369 7225         27134 $result[1] = substr( $line, 1 );
370 7225 100       16742 if ( $self->{trim_white_space} ) {
371 32         170 $result[1] =~ s/^\s*(.*?)\s*$/$1/;
372             }
373             }
374 7366         26883 return @result;
375             }
376              
377             sub _getline {
378 7369     7369   9496 my $self = shift;
379 7369         15726 local $/ = $self->record_separator;
380 7369         16568 my $line = $self->_filehandle->getline;
381 7368         274250 chomp($line);
382 7368         13591 $self->{_linecount}++;
383 7368         26241 return $line;
384             }
385              
386             sub _warning {
387 0     0   0 my $self = shift;
388 0         0 my $message = shift;
389 0         0 carp( $message
390             . " in file '"
391             . $self->file
392             . "' line "
393             . $self->{_linecount} );
394             }
395              
396             sub header {
397 23     23 1 170 my $self = shift;
398 23         39 my $header = shift;
399 23         49 my $fh = $self->_filehandle;
400 23         88 local $\ = $self->{record_separator};
401 23         70 print( $fh "!", $header );
402              
403             # used during write to validate passed record is appropriate for
404             # current header also generate reverse lookup for mapping record
405             # values to file key identifier.
406 23         44 $self->{currentheader} = $header;
407 23         34 foreach my $key ( keys %{ $header{$header} } ) {
  23         174  
408 187         466 $self->{reversemap}{ $header{$header}{$key} } = $key;
409             }
410 23 100 100     133 if ( exists( $header{$header}{S} ) && $header{$header}{S} eq "splits" ) {
411 6         20 foreach my $key ( keys %split ) {
412 18         49 $self->{reversesplitsmap}{ $split{$key} } = $key;
413             }
414             }
415              
416 23         41 $self->{_linecount}++;
417 23 50       105 if ( !exists( $header{$header} ) ) {
418 0         0 $self->_warning("Unsupported header '$header' written to file");
419             }
420             }
421              
422             sub write {
423 231     231 1 1307 my $self = shift;
424 231         302 my $record = shift;
425 231 50       691 if ( $record->{header} eq $self->{currentheader} ) {
426 231 100       451 if ( $record->{header} eq "Type:Prices" ) {
427 3 50 33     19 if ( exists( $record->{symbol} ) && exists( $record->{prices} ) ) {
428 3         4 foreach my $price ( @{ $record->{prices} } ) {
  3         8  
429 44 100 33     477 if ( exists( $price->{close} )
    50 66        
      66        
      66        
      33        
430             && exists( $price->{date} )
431             && exists( $price->{max} )
432             && exists( $price->{min} )
433             && exists( $price->{volume} ) )
434             {
435 40         174 $self->_writeline(
436             join( ",",
437             '"' . $record->{symbol} . '"',
438             $price->{close},
439             '"' . $price->{date} . '"',
440             $price->{max},
441             $price->{min},
442             $price->{volume} )
443             );
444             }
445             elsif (exists( $price->{close} )
446             && exists( $price->{date} ) )
447             {
448 4         23 $self->_writeline(
449             join( ",",
450             '"' . $record->{symbol} . '"',
451             $price->{close},
452             '"' . $price->{date} . '"' )
453             );
454             }
455             else {
456 0         0 $self->_warning("Prices missing a required field");
457             }
458             }
459 3         9 $self->_writeline("^");
460             }
461             else {
462 0         0 $self->_warning("Record missing 'symbol' or 'prices'");
463             }
464             }
465             else {
466 228         273 foreach my $value ( keys %{$record} ) {
  228         889  
467             next
468             if (
469 1234 100 100     11423 $value eq "header"
      100        
      66        
470             || $value eq "splits"
471             || ( $self->{currentheader} eq "Type:Memorized"
472             && $value eq "transaction" )
473             );
474 1000 50       2225 if ( exists( $self->{reversemap}{$value} ) ) {
475 1000 100       2449 if ( $value eq "address" ) {
    100          
476 13         45 my @lines = split( "\n", $record->{$value} );
477 13 100       33 if ( $self->{currentheader} eq "Type:Payee" ) {
478              
479             # The address fields are numbered for this record type
480 1         5 for ( my $count = 0 ; $count < 3 ; $count++ ) {
481 3 100       25 if ( $count <= $#lines ) {
482 1         5 $self->_writeline( "A", $count,
483             $lines[$count] );
484             }
485             else {
486 2         5 $self->_writeline( "A", $count );
487             }
488             }
489             }
490             else {
491 12         39 for ( my $count = 0 ; $count < 6 ; $count++ ) {
492 72 50       144 if ( $count <= $#lines ) {
493 0         0 $self->_writeline( "A", $lines[$count] );
494             }
495             else {
496 72         133 $self->_writeline("A");
497             }
498             }
499             }
500             }
501             elsif ( $value eq "budget" ) {
502 97         128 foreach my $amount ( @{ $record->{$value} } ) {
  97         253  
503 1164         2883 $self->_writeline( $self->{reversemap}{$value},
504             $amount );
505             }
506             }
507             else {
508 890         2256 $self->_writeline( $self->{reversemap}{$value},
509             $record->{$value} );
510             }
511             }
512             else {
513 0         0 $self->_warning( "Unsupported field '$value'"
514             . " found in record ignored" );
515             }
516             }
517 228 100       780 if ( exists( $record->{splits} ) ) {
518 3         6 foreach my $s ( @{ $record->{splits} } ) {
  3         8  
519 8         34 foreach my $key ( 'category', 'memo', 'amount' ) {
520 24 100       54 if ( exists( $s->{$key} ) ) {
521 23         63 $self->_writeline( $self->{reversesplitsmap}{$key},
522             $s->{$key} );
523             }
524             else {
525 1         4 $self->_writeline(
526             $self->{reversesplitsmap}{$key} );
527             }
528             }
529             }
530             }
531 228 100 66     644 if ( $self->{currentheader} eq "Type:Memorized"
532             && exists( $record->{transaction} ) )
533             {
534 3         10 $self->_writeline( $self->{reversemap}{"transaction"},
535             $record->{"transaction"} );
536             }
537 228         497 $self->_writeline("^");
538             }
539             }
540             else {
541 0         0 $self->_warning( "Record header type '"
542             . $record->{header}
543             . "' does not match current output header type "
544             . $self->{currentheader}
545             . "." );
546             }
547             }
548              
549             sub _writeline {
550 2431     2431   3340 my $self = shift;
551 2431         4222 my $fh = $self->_filehandle;
552 2431         7266 local $\ = $self->{record_separator};
553 2431         4346 print( $fh @_ );
554 2431         10581 $self->{_linecount}++;
555             }
556              
557             sub reset {
558 18     18 1 68 my $self = shift;
559 18         190 map( $self->{$_} = undef, # initialize internally used variables
560             qw(_linecount header currentheader reversemap reversesplitsmap) );
561 18         45 $self->_filehandle->seek( 0, 0 );
562             }
563              
564             sub close {
565 4     4 1 64 my $self = shift;
566 4         13 $self->_filehandle->close;
567             }
568              
569             1;
570              
571             __END__