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__ |