Annotation of pta/pta.pl, Revision 1.11
1.1 schwarze 1: #!/usr/bin/perl
2: #
1.11 ! schwarze 3: # Copyright (c) 2020, 2025 Ingo Schwarze <schwarze@openbsd.org>
1.1 schwarze 4: #
5: # Permission to use, copy, modify, and distribute this software for any
6: # purpose with or without fee is hereby granted, provided that the above
7: # copyright notice and this permission notice appear in all copies.
8: #
9: # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10: # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11: # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12: # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13: # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14: # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15: # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16:
17: use warnings;
18: use strict;
19:
20: use Getopt::Std qw(getopts);
1.7 schwarze 21: use Time::Local qw(timegm);
1.1 schwarze 22:
1.8 schwarze 23: our ($opt_a, $opt_b, $opt_c, $opt_D, $opt_L, $opt_n, $opt_p, $opt_s);
1.1 schwarze 24:
25: my %accounts; # {ano}{type, text}; from pta-accounts(5)
26: my %alist; # {ano}{subname, ''}[]; contains lists of entries
1.8 schwarze 27: my %cclist; # {cc}{ano}[]; contains lists of entries
28: my %entry; # {year, month, day, date, daynum, id, contra,
1.1 schwarze 29: # amount, rel, old, days, skip, cc, text, sum}
1.8 schwarze 30: my %prices; # {cc}{year, month, day, date, daynum, price}
31: my %profit; # {cc}[]; contains lists of profit entries
32: my %ptot; # {cc}{profit, percent, capital, pcpa, days}
33: my $startday; # Initialized by the first journal line.
34: my $endday = (timegm 0, 0, 0, 31, 11, 99999) / 86400;
1.1 schwarze 35:
36: my %atypes = (
37: A => 'Assets',
38: Q => 'Equity',
39: L => 'Liabilities',
40: R => 'Revenue',
41: S => 'Statistical accounts',
42: X => 'Expenses',
43: );
44:
45: my $translations = {
1.4 schwarze 46: en => {},
1.1 schwarze 47: de => {
48: 'Account list' => 'Kontenblatt',
49: 'Assets' => 'Aktiva',
50: 'Balance sheet' => 'Bilanz',
51: 'change in price' => 'Kursaenderung',
52: 'Cost center' => 'Kostenstelle',
53: 'current period' => 'aktuelle Periode',
54: 'Equity' => 'Eigenkapital',
55: 'Expenses' => 'Aufwand',
56: 'Liabilities' => 'Fremdkapital',
57: 'loss' => 'Verlust',
58: 'mismatch' => 'Diskrepanz',
59: 'Partial balance sheet' => 'Teilbilanz',
60: 'previous years' => 'Vorjahre',
61: 'profit' => 'Gewinn',
62: 'Profits and losses' => 'Gewinne und Verluste',
63: 'Revenue' => 'Ertrag',
64: 'since' => 'seit',
65: 'Statistical accounts' => 'Statistische Konten',
66: 'Subaccount list' => 'Unterkontenblatt',
67: 'total' => 'Summe',
68: 'total assets' => 'Bilanzsumme',
69: 'total loss' => 'Gesamtverlust',
70: 'total profit' => 'Gesamtgewinn',
71: }
72: };
73:
74: # === SUBROUTINES =====================================================
75:
76: sub translate ($) {
77: my $en = shift;
1.4 schwarze 78: return $translations->{$en} || $en;
1.1 schwarze 79: }
80:
81: sub translate_type ($) {
82: my $type = shift;
1.4 schwarze 83: my $en = $atypes{substr $type, 0, 1};
84: return $en ? translate $en : $type;
1.1 schwarze 85: }
86:
87: # Handles account entries (not journal entries) with respect to
88: # subaccounts, running totals, cost centers,
89: # and realized profits and losses in the profit table,
90: # but does not handle unrealized profits and losses.
91: sub make_entry (\%$) {
92: my ($entry, $ano) = @_;
1.8 schwarze 93: return if $entry->{daynum} > $endday;
1.1 schwarze 94: my $sub = $accounts{$ano}{type} =~ /[RX]/ &&
95: $entry->{text} =~ s/\((.*?)\) *// ? $1 : '';
96: my $old = $alist{$ano}{$sub} ? $alist{$ano}{$sub}[-1]{sum} : 0;
97: push @{$alist{$ano}{$sub}}, {%$entry, sum => $old + $entry->{amount}};
98: my $cc = $entry->{cc} or return;
99: $old = $cclist{$cc}{$ano} ? $cclist{$cc}{$ano}[-1]{sum} : 0;
100: push @{$cclist{$cc}{$ano}}, {%$entry, sum => $old + $entry->{amount}};
101: push @{$profit{$cc}}, {
102: %$entry,
103: old => $prices{$cc}{price},
104: amount => -$entry->{amount},
105: rel => -$entry->{amount} / $prices{$cc}{price},
106: } if $accounts{$ano}{type} =~ /p/;
107: }
108:
109: # For account lists, not for balance sheets.
110: sub print_amount ($) {
111: my $amount = shift;
112: if ($amount < 0) {
113: printf "%9s %9.2f", '', -$amount;
114: } else {
115: printf "%9.2f %9s", $amount, '';
116: }
117: }
118:
119: sub print_amount_text ($$) {
120: my ($entry, $account) = @_;
121: print_amount $entry->{amount};
122: printf " %10.2f %s\n",
123: $entry->{sum} * ($account->{type} =~ /[ASX]/ ? 1 : -1),
124: $entry->{text};
125: }
126:
127: # For balance sheets, not for account lists.
128: sub print_sum ($$$$$) {
129: my ($list, $ano, $indent, $debit, $credit) = @_;
130: my $type = $accounts{$ano}{type};
131: my $amount = $list->[-1]{sum};
132: printf "%*s%05u ", $indent, '', $ano;
133: if ($type =~ /[AX]/ || ($type eq 'S' && $amount >= 0)) {
134: printf "%9.2f %9s", $amount, '';
135: $$debit += $amount;
136: } else {
137: printf "%9s %9.2f", '', -$amount;
138: $$credit -= $amount;
139: }
140: printf " %s\n", $accounts{$ano}{text};
141: }
142:
143: sub usage () {
144: printf STDERR "usage: %s [-abcnps] [-L de]\n", $0;
145: exit 1;
146: }
147:
148: # === MAIN PROGRAM =====================================================
149:
1.8 schwarze 150: getopts 'abcD:L:nps' or usage;
1.1 schwarze 151: $opt_a = $opt_b = $opt_c = $opt_p = $opt_s = 1
152: unless $opt_a || $opt_b || $opt_c || $opt_n || $opt_p || $opt_s;
1.8 schwarze 153: if ($opt_D) {
154: $opt_D =~ /^(?:(\d{4})(\d{2})(\d{2}):)?(?:(\d{4})(\d{2})(\d{2}))?$/
155: or die "-D parse error: $opt_D";
156: $startday = (timegm 0, 0, 0, $3, $2 - 1, $1) / 86400 if $1;
157: $endday = (timegm 0, 0, 0, $6, $5 - 1, $4) / 86400 if $4;
158: }
1.4 schwarze 159: unless ($translations = $translations->{$opt_L || 'en'}) {
160: printf STDERR "unsupported language: -L %s\n", $opt_L;
1.1 schwarze 161: usage;
162: }
163:
164: my $fn = 'accounts.txt';
165: open my $in, $fn or die "$fn: $!";
166: while (<$in>) {
167: chomp;
168: next if /^(?:#|$)/;
169: my $line = $_;
170: s/^(\d+) +// or die "$fn account number parse error: $line";
171: my $account = {};
172: $accounts{$1} = $account;
173: s/^([ALQRSX]p?) +// or die "$fn account type parse error: $line";
174: $account->{type} = $1;
175: $account->{text} = $_;
176: }
177: close $in;
178:
179: # === JOURNAL PARSER ===================================================
180:
1.10 schwarze 181: $fn = shift // 'journal.txt';
1.1 schwarze 182: open $in, $fn or die "$fn: $!";
183: while (<$in>) {
184: chomp;
185: next if /^(?:#|$)/;
186: my $line = $_;
187:
188: # --- Subsequent line of a split entry. ------------------------
189:
190: if (%entry) {
191: s/^ *(\d+) +// or die "$fn split account parse error: $line";
192: my $ano = $1;
193: /^(-?\d+\.\d+) +(.*)/
194: or die "$fn split amount parse error: $line";
195: my ($amount, $text) = ($1, $2);
196: my $cc = $1 if $text =~ s/\[(.*?)\] *//;
197: $accounts{$ano} or die "unknown account $ano: $line";
1.5 schwarze 198: ($accounts{$ano}{type} =~ /S/) ==
199: ($accounts{$entry{contra}}{type} =~ /S/)
200: or die "statistical vs. non-statistical account: " .
201: "$entry{contra} split $line";
1.11 ! schwarze 202: $amount *= -1 if $entry{split} eq 'credit';
1.1 schwarze 203:
1.8 schwarze 204: if ($entry{daynum} <= $endday) {
205: # Combine the text on the split side.
206: my $newentry = {
207: %entry,
208: amount => $amount,
209: text => "$entry{text} $text",
210: };
211: if ($cc) {
212: $newentry->{cc} = $cc;
213: $newentry->{text} = "[$cc] $newentry->{text}";
214: }
215: make_entry %$newentry, $ano;
216:
217: # Append split account numbers on the combined side.
218: my $contra = $entry{contra};
219: $alist{$contra}{''}[-1]{text} .= " $ano"
220: unless $alist{$contra}{''}[-1]{text} =~ / $ano/;
221:
222: # If the split side specifies a cost center,
223: # manually create the cost center entry
224: # on the combined side because make_entry()
225: # was only called once there.
226: if ($cc) {
227: my $old = $cclist{$cc}{$contra} ?
228: $cclist{$cc}{$contra}[-1]{sum} : 0;
229: $newentry->{contra} = $ano;
230: $newentry->{amount} *= -1;
231: $newentry->{sum} = $old - $amount;
232: push @{$cclist{$cc}{$contra}}, $newentry;
233: }
1.1 schwarze 234: }
235:
236: # Keep track of the remaining amount.
237: $entry{amount} -= $amount;
238: %entry = () if abs($entry{amount}) < 0.005;
239: next;
240: }
241:
242: # --- Parse a normal journal entry or a price line. ------------
243:
244: s/^(\d{4})(\d{2})(\d{2}) +// or die "$fn date parse error: $line";
245: my ($year, $month, $day) = ($1, $2, $3);
1.8 schwarze 246: my $daynum = (timegm 0, 0, 0, $day, $month-1, $year) / 86400;
247: $startday //= $daynum;
1.6 schwarze 248: s/^(\S+) +// or die "$fn ID parse error: $line";
1.1 schwarze 249: my $id = $1;
250: s/^(\d+) +// or die "$fn debit account number parse error: $line";
251: my $debit = $1;
252: my ($credit, $oldpc, $newpc);
253: if (s/^(\d+)#(\d+) +//) {
254: $oldpc = $1;
255: $newpc = $2;
256: } elsif (s/^(\d+) +//) {
257: $credit = $1;
258: } else {
259: die "$fn credit account number parse error: $line";
260: }
261: /^(\d+\.\d+) +(.*)/ or die "$fn amount parse error: $line";
262: my ($amount, $text) = ($1, $2);
263: my $cc = $1 if $text =~ /\[(.*?)\]/;
264:
265: # --- Handle a price line. -------------------------------------
266:
267: if ($oldpc || $newpc) {
268: defined $cc or die "$fn price without cost center: $line";
269: my $old = $prices{$cc};
270: my $new = {
271: year => $year,
272: month => $month,
273: day => $day,
274: date => "$year-$month-$day",
1.8 schwarze 275: daynum => $daynum,
1.1 schwarze 276: price => $newpc * $amount,
277: };
1.8 schwarze 278: next if $new->{daynum} > $endday;
1.1 schwarze 279: $prices{$cc} = $new unless $prices{$cc} && $oldpc == $newpc;
280: next unless $oldpc;
281:
282: # --- Some units were already held. --------------------
283:
284: my $oldval = $old ? $old->{price} :
285: $cclist{$cc}{$debit}[-1]{sum};
286: my $diff = $oldpc * $amount - $oldval;
287: my $newprofit = {
288: year => $year,
289: month => $month,
290: day => $day,
291: date => $new->{date},
292: id => $id,
293: amount => $diff,
294: old => $oldval,
295: rel => $diff / $oldval,
296: text => (sprintf "[%s] %s", $cc,
297: (translate 'change in price')),
298: };
299: if ($old) {
300: # Record a gain or loss in this period.
301: $newprofit->{olddate} = $old->{date};
1.8 schwarze 302: $newprofit->{days} = $new->{daynum} - $old->{daynum};
1.1 schwarze 303: $newprofit->{text} .= sprintf " %s %s (%dd)",
304: (translate 'since'), $old->{date},
305: $newprofit->{days};
306: } else {
307: # Record a gain or loss before this period.
308: $newprofit->{skip} = 1;
309: $newprofit->{text} .= sprintf " (%s)",
310: (translate 'previous years');
311: }
312: push @{$profit{$cc}}, $newprofit;
313:
314: # --- Obsolete one previous line, if needed. -----------
315:
316: for (my $i = $#{$profit{$cc}} - 1; $i >= 0; $i--) {
317: my $oldprofit = $profit{$cc}[$i];
318: next unless $oldprofit->{olddate};
319: $oldprofit->{skip} = 1
320: if $oldprofit->{olddate} eq $old->{date};
321: last;
322: }
323: next;
324: }
325:
326: # --- Handle a normal journal entry. ---------------------------
327:
328: %entry = (
329: year => $year,
330: month => $month,
331: day => $day,
1.8 schwarze 332: date => "$year-$month-$day",
333: daynum => (timegm 0, 0, 0, $day, $month - 1, $year) / 86400,
1.1 schwarze 334: id => $id,
335: text => $text,
336: cc => $cc,
337: );
338: if ($debit) {
339: $accounts{$debit} or die "unknown debit account $debit: $line";
1.5 schwarze 340: # The credit side may or may not be split.
1.1 schwarze 341: my %newentry = (%entry, contra => $credit, amount => $amount);
342: make_entry %newentry, $debit;
343: } else {
344: $credit or die "splitting both sides: $line";
1.5 schwarze 345: # The debit side is split, remember the entry.
1.1 schwarze 346: $entry{contra} = $credit;
347: $entry{amount} = $amount;
1.11 ! schwarze 348: $entry{split} = 'debit';
1.1 schwarze 349: }
350: if ($credit) {
351: $accounts{$credit}
352: or die "unknown credit account $credit: $line";
1.5 schwarze 353: $debit && ($accounts{$debit}{type} =~ /S/) !=
354: ($accounts{$credit}{type} =~ /S/)
355: and die "statistical vs. non-statistical account: $line";
356: # The debit side may or may not be split.
1.1 schwarze 357: my %newentry = (%entry, contra => $debit, amount => -$amount);
358: make_entry %newentry, $credit;
359: # This entry is not split: clear it after processing.
360: %entry = () if $debit;
361: } else {
1.5 schwarze 362: # The credit side is split, remember the entry.
1.1 schwarze 363: $entry{contra} = $debit;
364: $entry{amount} = -$amount;
1.11 ! schwarze 365: $entry{split} = 'credit';
1.1 schwarze 366: }
367: }
368: # The last journal entry is an incomplete split.
369: die "$fn split parse error: EOF" if %entry;
370: close $in;
371:
372: # === OUTPUT ===========================================================
373:
374: for my $ano (sort keys %accounts) {
375: next unless $alist{$ano};
376:
377: # --- Subaccount lists. ----------------------------------------
378:
379: if ($opt_s) {
380: for my $sub (sort keys %{$alist{$ano}}) {
381: next if $sub eq '';
382: printf "\n%s %s %s (%s) %s\n",
383: (translate 'Subaccount list'),
384: $ano, $accounts{$ano}{text},
385: (translate_type $accounts{$ano}{type}), $sub;
386: for my $entry (@{$alist{$ano}{$sub}}) {
387: printf "%10s %6s %5s ", $entry->{date},
388: $entry->{id}, $entry->{contra};
389: print_amount_text $entry, $accounts{$ano};
390: }
391: }
392: }
393:
394: # --- Account lists. -------------------------------------------
395:
396: my ($sum, $hassub);
397: if ($alist{$ano}{''}) {
398: $sum = $alist{$ano}{''}[-1]{sum};
399: } else {
400: $alist{$ano}{''} = [];
401: $sum = 0;
402: }
403:
404: # Entries outside any subaccount.
405: if ($opt_a) {
406: printf "\n%s %s %s (%s)\n", (translate 'Account list'),
407: $ano, $accounts{$ano}{text},
408: (translate_type $accounts{$ano}{type});
409: for my $entry (@{$alist{$ano}{''}}) {
410: printf "%10s %6s %5s ",
411: $entry->{date}, $entry->{id}, $entry->{contra};
412: print_amount_text $entry, $accounts{$ano};
413: }
414: }
415:
416: # Subaccount balances.
417: for my $sub (sort {
418: $alist{$ano}{$b}[-1]{sum} <=> $alist{$ano}{$a}[-1]{sum}
419: } grep { $_ ne '' } keys %{$alist{$ano}}) {
420: $hassub = 1;
421: $sum += $alist{$ano}{$sub}[-1]{sum};
422: if ($opt_a) {
423: printf "%24s", '';
424: print_amount $alist{$ano}{$sub}[-1]{sum};
425: printf " %10.2f %s\n",
426: $sum * ($accounts{$ano}{type} =~ /[ASX]/ ? 1 : -1),
427: $sub;
428: }
429: }
430: push @{$alist{$ano}{''}}, {sum => $sum} if $hassub;
431: }
432:
433: # --- Balance sheet. ---------------------------------------------------
434:
435: if ($opt_b) {
436: my $debit = 0;
437: my $credit = 0;
438: my $stat;
439: printf "\n%s\n", (translate 'Balance sheet');;
440: for my $ano (sort keys %accounts) {
441: $alist{$ano} or next;
442: if ($accounts{$ano}{type} =~ /S/) {
443: $stat = 1;
444: } else {
445: print_sum $alist{$ano}{''},
446: $ano, 18, \$debit, \$credit;
447: }
448: }
449: printf "%23s %9.2f %9.2f %s\n", '', $debit, $credit,
450: (translate 'total assets');
451: printf "%33s %9.2f %s\n", '', $credit - $debit, (translate 'mismatch')
452: if abs($credit - $debit) > 0.005;
453:
454: # --- Statistical accounts. ------------------------------------
455:
456: if ($stat) {
457: $debit = $credit = 0;
458: printf "\n%s\n", (translate 'Statistical accounts');;
459: for my $ano (sort keys %accounts) {
460: $alist{$ano} && $accounts{$ano}{type} =~ /S/ or next;
461: print_sum $alist{$ano}{''},
462: $ano, 18, \$debit, \$credit;
463: }
464: printf "%23s %9.2f %9.2f %s\n", '', $debit, $credit,
465: (translate 'total');
466: printf "%33s %9.2f %s\n", '',
467: $credit - $debit, (translate 'mismatch')
468: if abs($credit - $debit) > 0.005;
469: }
470: }
471:
472: # --- Cost centers. ----------------------------------------------------
473:
474: for my $cc (sort keys %cclist) {
475: if ($opt_c) {
476:
477: # --- Cost center account lists. -----------------------
1.9 schwarze 478:
1.1 schwarze 479: printf "\n%s [%s] %s\n", (translate 'Cost center'), $cc,
480: (translate 'Account list');
481: for my $ano (sort keys %accounts) {
482: next unless $cclist{$cc}{$ano};
483: printf "%19s %5s %30s *** %s (%s)\n",
484: '', $ano, '', $accounts{$ano}{text},
485: (translate_type $accounts{$ano}{type});
486: for my $entry (@{$cclist{$cc}{$ano}}) {
487: printf " %10s %6s %5s ", $entry->{date},
488: $entry->{id}, $entry->{contra};
489: print_amount_text $entry, $accounts{$ano};
490: }
491: }
492:
493: # --- Partial balance sheet. ---------------------------
494:
495: my $debit = 0;
496: my $credit = 0;
497: my $stat;
498: printf "\n%s [%s] %s\n", (translate 'Cost center'), $cc,
499: (translate 'Partial balance sheet');
500: for my $ano (sort keys %accounts) {
501: $cclist{$cc}{$ano} or next;
502: if ($accounts{$ano}{type} =~ /S/) {
503: $stat = 1;
504: } else {
505: print_sum $cclist{$cc}{$ano},
506: $ano, 20, \$debit, \$credit;
507: }
508: }
509: printf "%25s %9.2f %9.2f %s\n", '', $debit, $credit,
510: (translate 'total assets');
511:
512: # --- Cost center statistical accounts. ----------------
513:
514: if ($stat) {
515: $debit = $credit = 0;
516: printf "\n%s [%s] %s\n",
517: (translate 'Cost center'), $cc,
518: (translate 'Statistical accounts');
519: for my $ano (sort keys %accounts) {
520: $cclist{$cc}{$ano} &&
521: $accounts{$ano}{type} =~ /S/ or next;
522: print_sum $cclist{$cc}{$ano},
523: $ano, 20, \$debit, \$credit;
524: }
525: printf "%25s %9.2f %9.2f %s\n", '',
526: $debit, $credit, (translate 'total');
527: }
528: }
529:
530: # --- Cost center profits and losses. --------------------------
531:
1.3 schwarze 532: if ($opt_p && $profit{$cc}) {
1.1 schwarze 533: printf "\n%s [%s] %s\n", (translate 'Cost center'), $cc,
534: (translate 'Profits and losses');
535: my $pr = 0;
536: my $days = 0;
537: my $capital = 0;
538: for my $i (0 .. $#{$profit{$cc}}) {
539: my $entry = $profit{$cc}[$i];
540: printf " %s %6s %8.2f %5.1f%% of %8.2f ",
541: $entry->{date}, $entry->{id}, $entry->{amount},
542: 100.0 * $entry->{rel}, $entry->{old};
543: if ($entry->{days}) {
544: printf "%5.1f%% p.a.",
1.7 schwarze 545: 36524.5 * $entry->{rel} / $entry->{days};
1.1 schwarze 546: } else {
547: printf "%11s", '';
548: }
549: printf " %s", $entry->{text};
550: if ($entry->{skip}) {
551: print " --\n";
552: next;
553: } else {
554: print "\n";
555: }
556: $pr += $entry->{amount};
557: next unless $entry->{days};
558: $days += $entry->{days};
559: $capital += $entry->{old} * $entry->{days};
560: }
1.8 schwarze 561: next unless $days;
1.1 schwarze 562: my $entry = {
563: profit => $pr,
564: percent => 100.0 * $pr / $capital * $days,
565: capital => $capital / $days,
566: pcpa => 36000.0 * $pr / $capital,
567: days => $days,
568: };
569: printf "%19s %8.2f %5.1f%% of %8.2f %5.1f%% p.a. " .
570: "[%s] %s (%s, %dd)\n", '', $pr, $entry->{percent},
571: $entry->{capital}, $entry->{pcpa}, $cc,
572: translate($pr < 0 ? 'total loss' : 'total profit'),
573: (translate 'current period'), $days;
574: $ptot{$cc} = $entry;
575: }
576: }
577:
578: # --- Global list of profits and losses. -------------------------------
579:
1.2 schwarze 580: if ($opt_p && %ptot) {
1.1 schwarze 581: my $pr = 0;
582: my $capital = 0;
583: my $maxd = 0;
584: printf "\n%s\n", (translate 'Profits and losses');
585: for my $cc (sort keys %ptot) {
586: printf "%9.2f %5.1f%% of %9.2f %5.1f%% p.a. %3dd [%s]\n",
587: $ptot{$cc}{profit}, $ptot{$cc}{percent},
588: $ptot{$cc}{capital}, $ptot{$cc}{pcpa},
589: $ptot{$cc}{days}, $cc;
590: $pr += $ptot{$cc}{profit};
1.8 schwarze 591: $capital += $ptot{$cc}{capital} * $ptot{$cc}{days} / 365.245;
1.1 schwarze 592: $maxd = $ptot{$cc}{days} if $maxd < $ptot{$cc}{days};
593: }
594: printf "%9.2f %5.1f%% of %9.2f %5.1f%% p.a. %3dd %s\n",
595: $pr, 100.0 * $pr / $capital * $maxd / 360.0,
596: 360.0 * $capital / $maxd, 100.0 * $pr / $capital, $maxd,
597: translate($pr < 0 ? 'total loss' : 'total profit');
598: }
599: exit 0;
CVSweb