Files: f1521af2d73e96acaa4e96dfc60dbfc50373845a / ledger-combine
2779 bytesRaw
1 | #!/usr/bin/env perl |
2 | |
3 | use strict; |
4 | use warnings; |
5 | use Time::Piece; |
6 | use Time::Seconds 'ONE_DAY'; |
7 | |
8 | $| = 1; |
9 | |
10 | my @files; |
11 | my @buffer; |
12 | |
13 | # acceptable time difference to consider transactions matching |
14 | my $time_difference = 7 * ONE_DAY; |
15 | |
16 | my $dateformat = "%Y/%m/%d"; |
17 | |
18 | if ($#ARGV == -1) { |
19 | print STDERR "Usage: $0 [journal.ledger]...\n"; |
20 | exit 1; |
21 | } |
22 | |
23 | my @in_filenames = @ARGV; |
24 | my $out_filename; |
25 | |
26 | if ($out_filename) { |
27 | open OUTFILE, '>', $out_filename; |
28 | OUTFILE->autoflush(1); |
29 | } else { |
30 | open OUTFILE, '>&STDOUT'; |
31 | } |
32 | |
33 | sub read_entry { |
34 | my $fh = shift; |
35 | return if !defined $fh or eof $fh; |
36 | |
37 | my $amounts = {}; |
38 | my $lines = []; |
39 | my $entry = { |
40 | lines => $lines, |
41 | amounts => $amounts, |
42 | fh => $fh, |
43 | }; |
44 | |
45 | # Read lines until found entry date |
46 | while(1) { |
47 | my $line = <$fh>; |
48 | unless (defined $line) { |
49 | # fh is closed |
50 | return; |
51 | } |
52 | push @$lines, $line; |
53 | if ($line =~ /^([0-9]{4}[\/-][0-9]{1,2}[\/-][0-9]{1,2})/) { |
54 | # Make the date string lexically comparable |
55 | $entry->{date} = Time::Piece->strptime($1, $dateformat) or die("$!"); |
56 | last; |
57 | } |
58 | } |
59 | |
60 | # Read until reaching a blank line |
61 | while(1) { |
62 | my $line = <$fh>; |
63 | unless (defined $line) { |
64 | # fh is closed |
65 | push @$lines, "\n"; |
66 | return $entry; |
67 | } |
68 | push @$lines, $line; |
69 | return $entry if $line =~ /^$/; |
70 | # Record item amounts |
71 | if ($line =~ /^\s*[^;].*?(?:\s\s|\t)\s*[^@ ]*? ?[^ 0-9]?-?([0-9.,]+)0*[^@]*?\s+(?:;.*|@.*)?$/) { |
72 | # Account 35.0 BTC @ $15.19 |
73 | $amounts->{+$1} = 1; |
74 | } |
75 | } |
76 | |
77 | return $entry; |
78 | } |
79 | |
80 | sub print_entry { |
81 | my ($stream, $entry) = @_; |
82 | my $date = $entry->{date}; |
83 | print $stream $_ for @{ $entry->{lines} }; |
84 | } |
85 | |
86 | sub pop_oldest_entry { |
87 | return unless scalar @buffer; |
88 | |
89 | # pick the oldest entry |
90 | my $oldest = $buffer[0]; |
91 | for my $entry (@buffer[1..$#buffer]) { |
92 | if ($entry->{date} < $oldest->{date}) { |
93 | $oldest = $entry; |
94 | } |
95 | } |
96 | |
97 | if ($oldest) { |
98 | # remove the entry from the buffer |
99 | @buffer = grep { $_ != $oldest } @buffer; |
100 | # read a new entry from the same stream and add it to the buffer |
101 | my $fh = $oldest->{fh}; |
102 | if (my $new_entry = read_entry $fh) { |
103 | push @buffer, $new_entry; |
104 | } |
105 | } |
106 | |
107 | return $oldest; |
108 | } |
109 | |
110 | # Open the ledger journals |
111 | for my $filename (@in_filenames) { |
112 | open my $fh, '<', $filename or die "Unable to open file $filename: $!"; |
113 | push @files, $fh; |
114 | } |
115 | |
116 | # Read first from each journal |
117 | for my $fh (@files) { |
118 | push @buffer, read_entry($fh); |
119 | } |
120 | |
121 | while (my $entry = pop_oldest_entry) { |
122 | print_entry \*OUTFILE, $entry; |
123 | } |
124 |
Built with git-ssb-web