use Exporter qw(import);
use Scalar::Util qw(refaddr);
use Time::Piece;
+use XML::Entities;
use XML::Parser::Lite;
our $VERSION = '9999.999'; # VERSION
our @EXPORT_OK = qw(parse_string parse_file);
-sub _croak { require Carp; Carp::croak(@_) }
-sub _usage { _croak("Usage: @_\n") }
-
my %ACCOUNT_TYPES = (
0 => 'none',
1 => 'bank',
11 => 'directdebit',
);
+sub _croak { require Carp; Carp::croak(@_) }
+sub _usage { _croak("Usage: @_\n") }
+
=method new
$homebank = File::HomeBank->new(string => $str);
shift->{file};
}
+=method file_version
+
+ $version = $homebank->file_version;
+
+Get the file format version.
+
+=cut
+
+sub file_version {
+ shift->{homebank}{version};
+}
+
=method title
$title = $homebank->title;
$account = $homebank->find_account_by_key($key);
-Find a account with the given key.
+Find an account with the given key.
=cut
my $self = shift;
my $transaction = shift;
- return if $transaction->{paymode} ne 'internaltransfer';
+ return if !$transaction->{dst_account};
my $transfer_key = $transaction->{transfer_key};
my @candidates;
for my $t (@{$self->transactions}) {
- next if $t->{paymode} ne 'internaltransfer';
+ next if !$t->{dst_account};
next if $t->{account} != $transaction->{dst_account};
next if $t->{dst_account} != $transaction->{account};
next if $t->{amount} != -$transaction->{amount};
# sort the candidates so we can pick the nearest one by date
my @ordered_candidates =
- map { $_->[1] }
+ map { $_->[1] }
sort { $a->[0] <=> $b->[0] }
- map { [abs($transaction_day - _ymd_to_julian($_->{date})), $_] } @candidates;
+ map { [abs($transaction_day - _ymd_to_julian($_->{date})), $_] } @candidates;
if (my $winner = $ordered_candidates[0]) {
my $key1 = $transfer_key || '[no key]';
$transations = $homebank->sorted_transactions;
-Get an arrayref of transactions sorted by date (earliest first).
+Get an arrayref of transactions sorted by date (oldest first).
=cut
sub parse_string {
my $str = shift or die _usage(q{parse_string($str)});
+ my %homebank;
my %properties;
my @accounts;
my @payees;
shift;
my $node = shift;
my %attr = @_;
- if ($node eq 'properties') {
+
+ # decode all attribute values
+ for my $key (keys %attr) {
+ $attr{$key} = _decode_xml_entities($attr{$key});
+ }
+
+ if ($node eq 'homebank') {
+ $attr{version} = delete $attr{v} if $attr{v};
+ %homebank = %attr;
+ }
+ elsif ($node eq 'properties') {
$attr{currency} = delete $attr{curr} if $attr{curr};
%properties = %attr;
}
$attr{flags}{$name} = $flags & (1 << $shift) ? 1 : 0;
}
+ for my $bnum (0 .. 12) {
+ $attr{budget_amounts}[$bnum] = delete $attr{"b$bnum"} if $attr{"b$bnum"};
+ }
+
push @categories, \%attr;
}
elsif ($node eq 'ope') { # transaction
- $attr{paymode} = $TRANSACTION_PAYMODES{$attr{paymode} || ''} || 'unknown';
- $attr{status} = $TRANSACTION_STATUSES{delete $attr{st}} || 'unknown';
+ $attr{paymode} = $TRANSACTION_PAYMODES{$attr{paymode} || ''} || 'unknown';
+ $attr{status} = $TRANSACTION_STATUSES{delete $attr{st} || ''} || 'unknown';
$attr{transfer_key} = delete $attr{kxfer} if $attr{kxfer};
$attr{split_amount} = delete $attr{samt} if $attr{samt};
$xml_parser->parse($str);
return {
+ homebank => \%homebank,
properties => \%properties,
accounts => \@accounts,
payees => \@payees,
};
}
+sub _decode_xml_entities {
+ my $str = shift;
+ # decoding entities can be extremely slow, so don't bother if it doesn't look like there are any
+ # entities to decode
+ return $str if $str !~ /&(?:#\d+)|[A-Za-z0-9]+;/;
+ return XML::Entities::decode('all', $str);
+}
+
sub _rdn_to_unix_epoch {
my $rdn = shift;
my $jan01_1970 = 719163;