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