GnuCash 2.4.99
CBBlib.pl
Go to the documentation of this file.
00001 #!/usr/bin/perl -w
00002 #use Exporter();
00003 
00004 package CBBlib;
00005 
00006 use strict;
00007 use English;
00008 use IO;
00009 ##  @file
00010 # @brief Belongs to package CBBlib
00011 #
00012 #### To do ######################
00013 #
00014 # Check remove transactions.
00015 # Move everything to Cbb package.
00016 #
00017 # put warnings into add_txns and remove_txns if attempted when inside
00018 # begin/end_txn_modifications
00019 #
00020 # Check to see that set_db is OK in the face of modifications...
00021 #
00022 # update_ledger has to return a sorted list of txn indices for modified txns
00023 #
00024 # ledger_add/modify/remove_txns?
00025 #
00026 # note_txn_modification
00027 #
00028 # Dirty should be set whenever a modification is made.
00029 # Dirty should be cleared whenever the client tells us we're clear...
00030 #
00031 # What about clones and begin/end_modify_txns and no db?
00032 #
00033 # check copy_obj, and automate?
00034 #
00035 # Need to clone account balances too?
00036 #
00037 # Should have list positions in txns?
00038 #
00039 # Need to create "Unitemized" category by default.
00040 #
00041 # Don't sort modifications by serial number.  Order is irrelevant.
00042 # Just use dates
00043 # @cond Perl
00044 
00045 STDOUT->autoflush(1);
00046 STDERR->autoflush(1);
00047 
00048 #@ISA       = qw(Exporter);
00049 #@EXPORT    = qw(func1 func2);
00050 #@EXPORT_OK = qw($sally @listabob %harry func3);
00051 
00052 ## private #################################################################
00053 
00054 sub BEGIN {}
00055 
00056 sub END {
00057   my $old_status = $?;
00058   
00059   $? = $old_status;
00060 }
00061 
00062 ## public ##################################################################
00063 
00064 my $pref_debug = 0;
00065 my $pref_verbose = 0;
00066 
00067 sub set_debug {
00068   my($value) = @_;
00069   $pref_debug = 1;
00070 }
00071 
00072 sub set_verbose {
00073   my($value) = @_;
00074   $pref_verbose = 1;
00075 }
00076 
00077 sub debug {
00078   my($message) = @_;
00079   print STDERR $message if $pref_debug;
00080 }
00081 
00082 sub verbose {
00083   my($message) = @_;
00084   print STDERR $message if ($pref_verbose || $pref_debug);
00085 }
00086 
00087 my $elapsed_offset = 0;
00088 
00089 sub elapsed_reset {
00090   my $prefix = shift;
00091   my($user,$system) = times;
00092   $elapsed_offset = $user + $system;
00093   print STDERR $prefix . " ... " if $prefix;
00094 }
00095 sub elapsed {
00096   my $prefix = shift;
00097   print STDERR $prefix if $prefix;
00098   my($user,$system) = times;
00099   print STDERR '(elapsed time: ' . ($user + $system - $elapsed_offset) . ")\n";
00100 }
00101 
00102 package CBBlib::Sink;
00103 use strict;
00104 use English;
00105 use IO;
00106 
00107 # Abstract class -- no "new" defined.
00108 
00109 # A destination for money.  Parent class for Acct and Cat.
00110 
00111 # It's really possible that ledgers should be a separate class, but
00112 # I'm tired and that's too big a change right now.
00113 
00114 
00115 sub acquire_ledger {
00116   my $self = shift;
00117   my $ledger = $self->get_ledger_();
00118   if(!$ledger) {
00119     $self->build_ledger_();
00120     print STDERR "Building ledger\n";
00121   }
00122   $self->set_ledger_usage_count_($self->get_ledger_usage_count_() + 1);
00123   return $self->get_ledger_();
00124 }
00125 
00126 sub release_ledger {
00127   my $self = shift;
00128   my $ledger = $self->get_ledger_();
00129   if(!$ledger) {
00130     print STERR "Big problem.  Released ledger that didn't exist!\n";
00131     exit 1;
00132   }
00133 
00134   my $usage_count = $self->get_ledger_usage_count_();
00135   if(!$usage_count) {
00136     print STERR
00137         "Big problem.  Released ledger that no-one could be holding!\n";
00138     exit 1;
00139   }
00140 
00141   $self->set_ledger_usage_count_($usage_count - 1);
00142   if($usage_count == 1) {
00143     print STDERR "Killing ledger\n";
00144     $self->set_ledger_(undef);
00145   }
00146 }
00147 
00148 sub build_ledger_ {
00149   my($self) = @_;
00150   my $db = $self->get_db();
00151   my $transactions = $db->get_txns();
00152   my $ledger = $self->get_ledger_();
00153   if(!$ledger) {
00154     $self->set_ledger_([]);
00155     $ledger = $self->get_ledger_();
00156   }
00157   @$ledger = ();
00158   
00159   my $cleared_balance = 0;
00160   my $total = 0;
00161   my $txn;
00162   foreach $txn (@$transactions) {    
00163     my($debit, $credit, $applicable_txn) = $txn->totals_wrt($self);
00164     if($applicable_txn) {
00165       $total += ($credit - $debit);
00166       if($txn->cleared_wrt_p($self)) {
00167         $cleared_balance += ($credit - $debit);
00168       }
00169       push @$ledger, [ $txn, $total ];
00170     }
00171   }
00172   $self->set_cleared_balance_($cleared_balance);
00173   $self->set_final_balance_($total);
00174 }
00175 
00176 sub compare_ledger_and_txn_ {
00177   my($ledger, $txn) = @_;
00178   return($$ledger[0]->get_date() cmp $txn->get_date());
00179 }
00180 
00181 sub match_ledger_and_txn_ {
00182   my($ledger, $txn) = @_;
00183   return($$ledger[0] == $txn);
00184 }
00185 
00186 my $started_mods_p;
00187 my $mods_acct;
00188 my $cleared_balance_diff_tmp;
00189 
00190 sub handle_ledger_entry_merge_ {
00191   my($ledger, $index, $new_p) = @_;
00192 
00193   if($new_p || $started_mods_p) {
00194     if($new_p) {
00195       # convert txn to ledger pair.
00196       my $txn = $$ledger[$index];
00197       $$ledger[$index] = [$txn, 0.0];
00198       $started_mods_p = 1;
00199     }
00200     
00201     my $entry = $$ledger[$index];
00202     my $txn = $$entry[0];
00203     my($debit, $credit, $applicable_txn) = $txn->totals_wrt($mods_acct);
00204     
00205     if($new_p) {
00206       if($txn->cleared_wrt_p($mods_acct)) {
00207         $cleared_balance_diff_tmp += ($credit - $debit);
00208       }
00209     }
00210     
00211     my $prev_total = 0;
00212     my $prev_entry = $$ledger[$index - 1];
00213     if($prev_entry) {
00214       $prev_total = $$prev_entry[1];
00215     }
00216     $$entry[1] = $prev_total + ($credit - $debit);
00217   }
00218 }
00219 
00220 my $ledger_removal_diff;
00221 
00222 sub handle_ledger_entry_removal_ {
00223   my($killed_p, $ledger, $index, $old_item) = @_;
00224 
00225   if($killed_p || $started_mods_p) {
00226     my $txn = $$ledger[$index]; $txn = $$txn[0];
00227     if($killed_p) {
00228       my($debit, $credit, $applicable_txn)  = $txn->totals_wrt($mods_acct);
00229       $ledger_removal_diff += ($credit - $debit);
00230       if($txn->cleared_wrt_p($mods_acct)) {
00231         $cleared_balance_diff_tmp -= ($credit - $debit);
00232       }      
00233       $started_mods_p = 1;
00234     } else {
00235       my $prev_total = 0;
00236       my $prev_entry = $$ledger[$index - 1];
00237       if($prev_entry) { $prev_total = $$prev_entry[1]; }
00238       my $entry = $$ledger[$index];
00239       $$entry[1] = $prev_total - $ledger_removal_diff;
00240     }
00241   }
00242 }
00243 
00244 sub ledger_add_txns_ {
00245   my($self, $txns) = @_;
00246   
00247   my $ledger = $self->get_ledger_();
00248   if(!$ledger) {
00249     return [];
00250   }
00251   
00252   # Globals, yuck...
00253   $cleared_balance_diff_tmp = 0;
00254   $started_mods_p = 0;
00255   $mods_acct = $self;
00256  CBBlib::debug("There are " . scalar(@$ledger) . " ledger entries\n");
00257  CBBlib::debug("  adding " . scalar(@$txns) . " ledger entries\n");
00258   $$txns[0]->print(\*STDERR);
00259   my $added_indices =
00260     main::destructive_merge_mangle($ledger, $txns, 
00261                                    \&compare_ledger_and_txn_,
00262                                    \&handle_ledger_entry_merge_);
00263   $self->set_cleared_balance_($self->get_cleared_balance() + 
00264                               $cleared_balance_diff_tmp);  
00265   my $final = $$ledger[$#$ledger];
00266   $self->set_final_balance_($$final[1]);  
00267   return $added_indices;
00268 }
00269 
00270 sub ledger_modify_txns_ {
00271   my($self, $txn_date_changed, $txns) = @_;
00272   # presumes @$txns is sorted on date to match the database order.
00273   
00274   my $ledger = $self->get_ledger_();
00275   if(!$ledger) {
00276     return ([], []);
00277   }
00278   
00279   # Brute force date change handling.  We should do better later...
00280   my @txns_w_date_changes = grep {
00281     $$txn_date_changed{$_};
00282   } @$txns;
00283   
00284   my @moves = ();
00285   
00286   if(@txns_w_date_changes) {
00287     print STDERR "Handling ledger mods for date changes\n";
00288 
00289     # Get the current positions of the changed $txns
00290     my %move_from_txn;
00291     my @candidates = @txns_w_date_changes;
00292     my $candidate = shift @candidates;
00293     # Unfortunately, these txns won't be in the order relative to the
00294     # old ledger since their dates have changed, so we have to loop.
00295     # This may be marginally faster than just traversing once per txn.
00296     while($candidate) {
00297       print STDERR "Looking for candidate $candidate\n";
00298       my $initial_value = $candidate;
00299       my $i;
00300       for($i=0; $candidate && $i < scalar(@$ledger); $i++) {
00301         my $entry = $$ledger[$i];
00302         my $txn = $$entry[0];
00303         print STDERR "[$txn] [$candidate]\n";
00304         if($txn == $candidate) {          
00305           my $move = [$i];
00306           push @moves, $move;
00307           $move_from_txn{$txn} = $move;
00308           $candidate = shift @candidates;
00309         }
00310       }
00311       if($initial_value == $candidate) {
00312         die "Couldn't find transaction in ledger during ledger modify";
00313       }
00314     }
00315 
00316     # Just trash the existing one to get the new one with these
00317     # transactions in the proper positions.
00318     $self->build_ledger_();
00319 
00320     # Now things should be properly sorted to match the order of
00321     # @txns_w_date_changes.  We'll leave the extra loop just in case,
00322     # but this should go off in one pass.
00323     @candidates = @txns_w_date_changes;
00324     $candidate = shift @candidates;
00325     while($candidate) {
00326       my $initial_value = $candidate;
00327       my $i;
00328       for($i=0; $candidate && $i < scalar(@$ledger); $i++) {
00329         my $entry = $$ledger[$i];
00330         my $txn = $$entry[0];
00331         if($txn == $candidate) {
00332           my $move = $move_from_txn{$txn};
00333           die "FATAL: Couldn't find move in ledger search" if ! $move;
00334           push @$move, $i;
00335           $candidate = shift @candidates;
00336         }
00337       }
00338       if($initial_value == $candidate) {
00339         die "Couldn't find transaction in ledger during ledger modify";
00340       }
00341     }
00342   }
00343 
00344   my @modified_indices = ();
00345   my $current_index = 0;
00346 
00347   if(@$txns) {
00348     my @mod_txns = @$txns;
00349     my $next_mod_txn = shift @mod_txns; 
00350     my $prev_ledger_value = 0;
00351     my $started_mods = 0;
00352     my $cleared_balance_diff = 0;
00353     my $diff = 0;
00354     my $entry;
00355     foreach $entry (@$ledger) {
00356       my $txn = $$entry[0];
00357       if(defined($next_mod_txn) && $next_mod_txn == $txn) {
00358         $started_mods = 1;
00359         my $prev_value = $$entry[1];
00360         my($debit, $credit, $applicable_txn) = $txn->totals_wrt($self);
00361         my $new_value = $prev_ledger_value + ($credit - $debit);
00362         my $local_change = $new_value - $prev_value;
00363         if($txn->cleared_wrt_p($self)) {
00364           $cleared_balance_diff -= ($credit - $debit);
00365         }      
00366         $diff +=  $local_change;
00367         $$entry[1] = $new_value;
00368         $next_mod_txn = shift @mod_txns;
00369         push @modified_indices, $current_index;
00370       } elsif ($started_mods) {
00371         $$entry[1] += $diff;
00372       }
00373       $prev_ledger_value = $$entry[1];
00374       $current_index++;
00375     }
00376     $self->set_cleared_balance_($self->get_cleared_balance() + 
00377                                 $cleared_balance_diff);  
00378   }
00379   my $final = $$ledger[$#$ledger];
00380   $self->set_final_balance_($$final[1]);      
00381   return (\@modified_indices, \@moves);
00382 }
00383 
00384 sub ledger_remove_txns_ {
00385   my($self, $txns) = @_;
00386   # presumes @$txns is sorted on date to match the database order.
00387   # returns a list where each item is [$txn, $old_index].
00388 
00389   my $ledger = $self->get_ledger_();
00390   if(!$ledger) {
00391     return;
00392   }
00393 
00394   # Globals, yuck...
00395   $ledger_removal_diff = 0;
00396   $cleared_balance_diff_tmp = 0;
00397   $started_mods_p = 0;
00398   $mods_acct = $self;
00399   
00400   my $removed_indices = 
00401     main::destructive_remove_mangle($ledger,
00402                                     $txns,
00403                                     \&match_ledger_and_txn_,
00404                                     \&handle_ledger_entry_removal_);
00405 
00406   print STDERR
00407       "Finished removing ledger items (" . join(" ", @$removed_indices) .
00408           ")\n";
00409 
00410   $self->set_cleared_balance_($self->get_cleared_balance() + 
00411                               $cleared_balance_diff_tmp);  
00412   my $final = $$ledger[$#$ledger];
00413   $self->set_final_balance_($$final[1]);  
00414   
00415   my @txns_tmp = @$txns;
00416   my @result = map { 
00417     my $txn = shift @txns_tmp;
00418     [$_, $txn];
00419   } @$removed_indices;
00420   return(\@result);
00421 }
00422 
00423 
00424 package CBBlib::Acct;
00425 use strict;
00426 use English;
00427 use IO;
00428 
00429 use vars qw(@ISA);
00430 unshift @ISA, qw(CBBlib::Sink);
00431 
00432 sub new {
00433   my $class = shift;
00434   my ($db, $name, $notes) = @_;  
00435   my $self = make_internals_();
00436   bless $self, $class;
00437   
00438   $self->set_db_($db);
00439   $self->set_name_($name);
00440   $self->set_notes_($notes);
00441   
00442   return $self;
00443 }
00444 
00445 sub print {
00446   my($self, $fh, $prefix, $id_map) = @_;
00447   my $name = $self->get_name();
00448   my $notes = $self->get_notes();
00449   $notes = "" if ! $notes;
00450   $prefix  = "" unless $prefix;
00451   if($id_map) {
00452     print $fh $prefix . $$id_map{$self} . "\t$name\t$notes\n"; 
00453   } else {
00454     print $fh $prefix . "$self\t$name\t$notes\n"; 
00455   }
00456 }
00457 
00458 
00459 package CBBlib::Cat;
00460 use strict;
00461 use English;
00462 use IO;
00463 
00464 use vars qw(@ISA);
00465 unshift @ISA, qw(CBBlib::Sink);
00466 
00467 sub new {
00468   my $class = shift;
00469   my ($db, $name, $notes) = @_;  
00470   my $self = make_internals_();
00471   bless $self, $class;
00472   
00473   $self->set_db_($db);
00474   $self->set_name_($name);
00475   $self->set_notes_($notes);
00476   
00477   return $self;
00478 }
00479 
00480 sub print {
00481   my($self, $fh, $prefix, $id_map) = @_;
00482   my $name = $self->get_name();
00483   my $notes = $self->get_notes();
00484   $notes = "" if ! $notes;
00485   $prefix  = "" unless $prefix;
00486   if($id_map) {
00487     print $fh $prefix . $$id_map{$self} . "\t$name\t$notes\n"; 
00488   } else {
00489     print $fh $prefix . "$self\t$name\t$notes\n"; 
00490   }
00491 }
00492 
00493 package CBBlib::Txn;
00494 use strict;
00495 use English;
00496 use IO;
00497 
00498 sub new {
00499   my $class = shift;
00500   my ($date, $source, $checkno, $desc, $status) = @_;
00501   $status = "" if !$status;
00502   
00503   my $self = make_internals_();
00504   bless $self, $class;
00505   
00506   $self->set_date_($date);
00507   
00508   die "CBBlib::Txn new: source must be a CBBlib::Acct."
00509       unless (ref($source) eq 'CBBlib::Acct');
00510   
00511   $self->set_source_($source);
00512   
00513   $self->set_checkno_($checkno);
00514   $self->set_desc_($desc);
00515   $self->set_status_($status);
00516   
00517   return $self;
00518 }
00519 
00520 
00521 sub make_clone_ {
00522   my ($self) = @_;
00523   
00524   my $clone = $self->get_clone_();
00525   if(!$clone) {
00526     $self->set_clone_($self->copy_obj_());
00527     $clone = $self->get_clone_();
00528   }
00529   return $clone;
00530 }  
00531 
00532 
00533 
00534 
00535 # All the set/get slot functions are in CBBlib-auto.pl.
00536 
00537 # ""     = irrelevant (for split line with category destination)
00538 # " "    = new and untouched
00539 # "*"    = selected from the balance window to tentatively be
00540 #          cleared (stage one of the balance process)
00541 # "x"    = cleared (stage two of the balance process)
00542 #
00543 # "?"    = a tentative future (recurring) transaction
00544 # "!"    = a past (recurring) transaction
00545 
00546 sub get_status_wrt {
00547   my($self, $sink) = @_;
00548   my $result;
00549   if($self->get_source() == $sink) {
00550     $result = $self->get_status();
00551   } else {
00552     $result = $self->get_transfer_status($sink);
00553   }
00554   if(!defined($result)) {
00555     print STDERR "Undefined status wrt $sink for\n";
00556     $self->print(\*STDERR, "  ");
00557   }
00558   return $result;
00559 }
00560 
00561 sub set_status_wrt_ {
00562   my($self, $sink, $val) = @_;
00563   if($self->get_source() == $sink) {
00564     $self->set_status($val);
00565   } else {
00566     $self->set_transfer_status_($sink, $val);
00567   }
00568 }
00569 
00570 sub copy_obj_ {
00571   my($self) = shift;
00572   
00573   my @copy = @$self;
00574   my $copy_ref = \@copy;
00575   bless $copy_ref, ref($self);
00576 
00577   my $splits = $copy_ref->get_splits_();
00578 
00579   # Copy only the spine of the list (so we can do a "diff" later).
00580   my @splits_copy = @$splits;
00581 
00582   $copy_ref->set_splits_(\@splits_copy);
00583   return $copy_ref;
00584 }
00585 
00586 
00587 sub get_transfer_status {
00588   my($self, $sink) = @_;
00589   my $splits = $self->get_splits_();
00590   my $result;
00591   my $split;
00592   foreach $split (@$splits) {
00593     if($split->get_dest() == $sink) {
00594       $result = $split;
00595       last;
00596     }
00597   }
00598   undef $split;
00599 
00600   if(!$result) {
00601     return undef;
00602   } else {
00603     return $result->get_status();
00604   }
00605 }
00606 
00607 
00608 sub set_transfer_status_ {
00609   my($self, $acct, $new_status) = @_;
00610   my $splits = $self->get_splits_();
00611   my $result;
00612   my $split;
00613   foreach $split (@$splits) {
00614     my $dest = $split->get_dest();
00615     if($split->get_dest() == $acct) {
00616       $split->set_status($new_status);
00617     }
00618   }
00619 }
00620 
00621 sub cleared_p_ { 
00622   my($self) = @_;  
00623   return $self->get_status() eq 'x';
00624 }
00625 
00626 sub cleared_wrt_p { 
00627   my($self, $acct) = @_;  
00628   return $self->get_status_wrt($acct) eq 'x';
00629 }
00630 
00631 sub clear_wrt { 
00632   my($self, $acct) = @_;  
00633   $self->set_status_wrt_($acct, 'x');
00634 }
00635 
00636 sub clear_pending_wrt_p { 
00637   my($self, $acct) = @_;  
00638   return $self->get_status_wrt($acct) eq '*';
00639 }
00640 
00641 sub clear_pending_wrt { 
00642   my($self, $acct) = @_;  
00643   $self->set_status_wrt_($acct, '*');
00644 }
00645 
00646 sub uncleared_wrt_p { 
00647   my($self, $acct) = @_;  
00648   return $self->get_status_wrt($acct) eq ' ';
00649 }
00650 
00651 sub unclear_wrt { 
00652   my($self, $acct) = @_;  
00653   $self->set_status_wrt_($acct, ' ');
00654 }
00655 
00656 sub void {
00657   # ???
00658 }
00659 
00660 sub add_split {
00661   my($self, $split, $insert_position) = @_;
00662 
00663   # Don't forget to change modify functions in CBBlib-auto.plp too
00664   # whenever you make changes here.
00665 
00666   my $splits;
00667 
00668   $split->set_txn_($self);
00669 
00670   my $db = $self->get_db();
00671   if($db) {
00672     
00673     $db->begin_txn_modifications();
00674     
00675     my $clone = $self->make_clone_();
00676     $splits = $clone->get_splits_();
00677 
00678     if($insert_position) {
00679       splice @$splits, $insert_position, 0, ($split);
00680     } else {
00681       push @$splits, $split;
00682       $insert_position = $#$splits;
00683     }
00684   } else {
00685     $splits = $self->get_splits_();
00686 
00687     if($insert_position) {
00688       splice @$splits, $insert_position, 0, ($split);
00689     } else {
00690       push @$splits, $split;
00691       $insert_position = $#$splits;
00692     }    
00693   }
00694 
00695   my $i;
00696   for($i = $insert_position; $i < scalar(@$splits); $i++) {
00697     $$splits[$i]->set_pos_($i);
00698   }
00699   
00700   if($db) {
00701     $db->record_txn_modification_($self);
00702     $db->end_txn_modifications();
00703   }
00704 }
00705 
00706 sub remove_split {
00707   my($self, $split) = @_;
00708 
00709   # Don't forget to change modify functions in CBBlib-auto.plp too
00710   # whenever you make changes here.
00711 
00712   my $db = $self->get_db();
00713   my $splits;
00714   if($db) {
00715     $db->begin_txn_modifications();
00716     
00717     my $clone = $self->make_clone_();
00718     $splits = $clone->get_splits_();
00719   } else {
00720     $splits = $self->get_splits_();
00721   }
00722   
00723   my $old_index = 0;
00724   my $split_found = 0;
00725   my $candidate;
00726   foreach $candidate (@$splits) {
00727     if($candidate == $split) {
00728       $split_found = 1;
00729       last;
00730     }
00731     $old_index++;
00732   }  
00733   if(!$split_found) {
00734     die "Failed to find split in Txn::remove_split";
00735   }  
00736   splice @$splits, $old_index, 0;
00737   
00738   my $i;
00739   for($i = $old_index; $i < scalar(@$splits); $i++) {
00740     $$splits[$i]->set_pos_($i);
00741   }
00742 
00743   if($db) {
00744     my $txn = $split->get_txn_();
00745     $split->set_txn_(undef);
00746     $db->record_txn_modification_($txn);
00747     $db->end_txn_modifications();
00748   }
00749 }
00750 
00751 sub totals_wrt {
00752   my($self, $sink) = @_;
00753   # O(n)
00754   # Returns (total_debit, total_credit, applicable)
00755 
00756   my($total_debit, $total_credit, $applicability) = (0, 0, 0); 
00757   my $splits = $self->get_splits_();
00758   
00759   my $split;
00760   foreach $split (@$splits) {
00761     my $dest = $split->get_dest();
00762     
00763     if($self->get_source() == $sink) {
00764       $total_debit += $split->get_debit();
00765       $total_credit += $split->get_credit();
00766       $applicability = 1;
00767     } elsif($dest == $sink) {
00768       #} elsif((ref($dest) eq 'CBBlib::Acct') && ($dest == $sink)) {
00769       $total_debit += $split->get_credit();
00770       $total_credit += $split->get_debit();
00771       $applicability = 1;
00772     }
00773   }
00774   return($total_debit, $total_credit, $applicability);
00775 }
00776 
00777 
00778 sub affected_sinks {
00779   my($self) = @_;
00780   # O(n)
00781   # Returns list of affected sinks
00782   
00783   my $source = $self->get_source();
00784   my %result = ($source => $source);
00785   my $splits = $self->get_splits_();  
00786 
00787   map {
00788     my $dest = $_->get_dest();
00789     if(!$dest) {
00790       die "\nNo dest in $_\n";
00791     }
00792     $result{$dest} = $dest;
00793   } @$splits;
00794   return(values(%result));
00795 }
00796 
00797 
00798 sub print {
00799   my($self, $fh, $prefix, $id_map) = @_;
00800   $prefix = "" if ! $prefix;
00801   
00802   print $fh $prefix . $self->get_date() . "\t";
00803   if($id_map) {
00804     print $fh $$id_map{$self->get_source()} . "\t";
00805   } else {
00806     print $fh $self->get_source() . "\t";
00807   }
00808   print $fh $self->get_checkno() . "\t";
00809   print $fh $self->get_desc() . "\t";
00810   print $fh $self->get_status() . "\n";
00811   my $splits = $self->get_splits_();
00812   my $split;
00813   foreach $split (@$splits) {
00814     $split->print($fh, $prefix . ' ', $id_map);
00815   }
00816 }
00817 
00818 sub print_pretty {
00819   my($self, $fh, $prefix) = @_;
00820   $prefix = "" if ! $prefix;
00821 
00822   print $fh $prefix . $self->get_date() . ":";
00823   print $fh ($self->get_source())->get_name() . ":";
00824   print $fh $self->get_checkno() . ":";
00825   print $fh $self->get_desc() . ":";
00826   print $fh $self->get_status() . "\n";
00827   my $splits = $self->get_splits_();
00828   my $split;
00829   foreach $split (@$splits) {
00830     $split->print_pretty($fh, $prefix . "  ");
00831   }
00832 }
00833 
00834 package CBBlib::Split;
00835 use strict;
00836 use English;
00837 use IO;
00838 
00839 sub new {
00840   my $class = shift;
00841   my ($dest, $notes, $debit, $credit, $status) = @_;
00842   my $self = make_internals_();
00843   bless $self, $class;
00844 
00845   $status = '' if ! $status;
00846 
00847   $self->set_dest_($dest);
00848   $self->set_notes_($notes);
00849   $self->set_debit_($debit);
00850   $self->set_credit_($credit);
00851   $self->set_status_($status);
00852 
00853   return $self;
00854 }
00855 
00856 sub copy_obj_ {
00857   my($self) = shift;
00858   
00859   my @copy = @$self;
00860   my $copy_ref = \@copy;
00861   bless $copy_ref, ref($self);
00862   return $copy_ref;
00863 }
00864 
00865 sub make_clone_ {
00866   my ($self) = @_;
00867   
00868   my $clone = $self->get_clone_();
00869   if(!$clone) {
00870     my $txn = $self->get_txn_();
00871     if($txn) {
00872       $txn->make_clone_();
00873     }
00874     $self->set_clone_($self->copy_obj_());
00875     $clone = $self->get_clone_();
00876   }
00877   return $clone;
00878 }  
00879 
00880 sub get_db {
00881   my $self = shift;
00882   my $txn = $self->get_txn();
00883   if($txn) {
00884     return $txn->get_db();
00885   } else {
00886     return undef;
00887   }
00888 }
00889 
00890 sub cleared_p_ { 
00891   my($self) = @_;  
00892   my $status = $self->get_status();
00893   if($status) {
00894     return $status eq 'x';
00895   } else {
00896     return undef;
00897   }
00898 }
00899 
00900 
00901 sub print {
00902   my($self, $fh, $prefix, $id_map) = @_;
00903   $prefix = "" if ! $prefix;
00904 
00905   print $fh $prefix;
00906   if($id_map) {
00907     print $fh $$id_map{$self->get_dest()} . "\t";
00908   } else {
00909     print $fh $self->get_dest() . "\t";
00910   }
00911   print $fh $self->get_notes() . "\t";
00912   print $fh $self->get_debit() . "\t";
00913   print $fh $self->get_credit() . "\t";
00914   print $fh $self->get_status() . "\n";
00915 }
00916 
00917 
00918 package CBBlib::Db;
00919 use strict;
00920 use English;
00921 use IO;
00922 
00923 sub new {
00924   my($class, $default_sink) = @_;
00925   my $self = make_internals_();
00926   bless $self, $class;
00927   
00928   if($default_sink) {
00929     $self->add_sinks([$default_sink]);
00930   } else {
00931     $default_sink = new CBBlib::Cat($self, '<<unitemized>>', '');
00932     $self->add_sinks([$default_sink]);
00933   }
00934   $self->set_default_sink($default_sink);
00935   
00936   return $self;
00937 }
00938 
00939 sub clean_p {
00940   # Should eventually know the truth, but this is safe at the moment.
00941   return 0;
00942 }
00943 
00944 sub add_sinks {
00945   my($self, $sinks) = @_;
00946   
00947   my $accts = $self->get_accts_();
00948   my $cats = $self->get_cats_();
00949   map {
00950     $_->set_db_($self);
00951     if(ref($_) eq 'CBBlib::Acct') {
00952       push @$accts, $_;
00953     } elsif(ref($_) eq 'CBBlib::Cat') {
00954       push @$cats, $_;
00955     } else {
00956       die "Unknown sink type in CBBlib::Db::add_sinks()";
00957     }
00958   } @$sinks;
00959 }
00960 
00961 sub record_txn_modification_ {
00962   my($self, $txn) = @_;
00963   
00964   my $mod_level = $self->get_modified_txns_level_();
00965   if(!$mod_level) {
00966     die "Tried to record_txn_modification_ when not in update region.";
00967   }
00968   my $modified_txns = $self->get_modified_txns_();
00969   #my $serial_number = $self->get_modified_txns_serial_num_();
00970   #$self->set_modified_txns_serial_num_($serial_number + 1);
00971   
00972   if(ref($txn) eq 'CBBlib::Split') {
00973     $txn = $txn->get_txn_();
00974   }
00975   $$modified_txns{$txn} = $txn;
00976 }
00977 
00978 sub update_dirty_txns_hash_ {
00979   my($self, $dirty_hash, $txn) = @_;
00980 
00981   my @affected_sinks = $txn->affected_sinks();
00982   
00983   map {
00984     my $sink = $_;
00985     if(!$$dirty_hash{$sink}) {
00986       $$dirty_hash{$sink} = [$sink, [$txn]];
00987     } else {
00988       my $list = $$dirty_hash{$sink};
00989       $list = $$list[1];
00990       push @$list, $txn;
00991     }
00992   } @affected_sinks;
00993 }
00994 
00995 
00996 sub debug_txns_modified_data {
00997   my($self, $modifications, $dirty_sinks) = @_;
00998  CBBlib::debug("CBBlib CALLBACK: txns-modified\n");
00999 
01000  CBBlib::debug("  Modifications:\n");
01001   map {
01002     my $txn = $$_[0];
01003     my $mods = $$_[1];
01004   CBBlib::debug("    [$txn");
01005     map {
01006     CBBlib::debug("\n     [");
01007       my $first = 1;
01008       map {
01009         if($first) {
01010           $first = 0;
01011         } else {
01012           print " ";
01013         }
01014         if(!defined($_)) {
01015           print "<<undefined>>";
01016         } else {
01017           print $_;
01018         }
01019       } @$_;
01020     CBBlib::debug("]");
01021     } @$mods;
01022   CBBlib::debug("]\n");
01023   } @$modifications;
01024 
01025  CBBlib::debug("  Affected sinks:\n");
01026   map {
01027     my ($sink, $mod_txns, $indices) = @$_;
01028     $sink->print(\*STDERR, "    ");
01029   } values(%$dirty_sinks);
01030 }
01031 
01032 
01033 sub post_modification_notices_ {
01034   my $self = shift;
01035  CBBlib::debug("CBBlib::post_modification_notices_: checking for changes.\n");
01036 
01037   my $modified_txns_hash = $self->get_modified_txns_();
01038   my @sorted_txns = sort { 
01039     $a->get_date <=> $b->get_date(); 
01040   } values(%$modified_txns_hash);
01041   my @modifications = ();
01042 
01043   # Have to treat credit/debit mods the same as other field mods since
01044   # in the end we need the ledger indices of all the txns.
01045   my %dirty_sinks;
01046 
01047   # This is not the most efficient way to go about this, but it's
01048   # easy, and I'm in a hurry.  Someone can make it do the merge/remove
01049   # mangle thing later.  Be careful, though, you don't want to cause
01050   # spurious add/remove events.
01051   my $date_changed;
01052   my %txn_date_changed;
01053 
01054   # generate modified events for each transaction
01055   my $txn;
01056   foreach $txn (@sorted_txns) {
01057     my $new_txn = $txn->get_clone_();
01058     my $old_splits = $txn->get_splits_();
01059     my $new_splits = $new_txn->get_splits_();
01060     my @txn_mods = ();
01061 
01062     my $i;
01063     for($i=0; $i < scalar(@$old_splits); $i++) {
01064       my $old_item = $$old_splits[$i];
01065       if(! grep { $_ == $old_item } @$new_splits) {
01066         my $old_index = $i;
01067         push @txn_mods, ['split-removed', $old_item, $old_index];
01068       }
01069     }
01070 
01071     for($i=0; $i < scalar(@$new_splits); $i++) {
01072       my $new_item = $$new_splits[$i];
01073       if(! grep { $_ == $new_item } @$old_splits) {
01074         my $new_index = $i;
01075         push @txn_mods, ['split-added', $new_item, $new_index];
01076       }
01077     }
01078 
01079     # This could be much faster with a better algorithm.
01080     my @modified_splits = grep { $_->get_clone_() } @$new_splits;
01081     
01082     if($new_txn->get_date_ ne $txn->get_date_()) {
01083       push @txn_mods, ['date', $txn->get_date_()];
01084       $txn->set_date_($new_txn->get_date_());
01085       $date_changed = 1;
01086       $txn_date_changed{$txn} = $txn;
01087     }
01088     if($new_txn->get_checkno_ ne $txn->get_checkno_()) {
01089       push @txn_mods, ['checkno', $txn->get_checkno_()];
01090       $txn->set_checkno_($new_txn->get_checkno_());
01091     }
01092     if($new_txn->get_desc_ ne $txn->get_desc_()) {
01093       push @txn_mods, ['desc', $txn->get_desc_()];
01094       $txn->set_desc_($new_txn->get_desc_());
01095     }
01096     if($new_txn->get_status_ ne $txn->get_status_()) {
01097       push @txn_mods, ['status', $txn->get_status_()];
01098       $txn->set_status_($new_txn->get_status_());
01099     }
01100     
01101     my $split;
01102     foreach $split (@modified_splits) {
01103       my $new_split = $split->get_clone_();
01104       
01105       my $old_txn = $split->get_txn();
01106       if(defined($old_txn) && ($old_txn == $new_split->get_txn())) {
01107         # This is really a modification to an existing split (is that
01108         # what this test should be doing)?
01109 
01110         my $old_pos = $split->get_pos__();
01111         my $new_pos = $new_split->get_pos__();
01112         if((defined($new_pos) != defined($old_pos)) ||
01113            ($new_pos != $old_pos)) {
01114           push @txn_mods, ['split-modified', $split, 'pos',
01115                            $split->get_pos__()];
01116           $split->set_pos__($new_split->get_pos__());
01117         }
01118         if($new_split->get_dest_ != $split->get_dest_()) {
01119           push @txn_mods, ['split-modified', $split, 'dest',
01120                            $split->get_dest_()];
01121           $split->set_dest_($new_split->get_dest_());
01122         }
01123         if($new_split->get_notes_ ne $split->get_notes_()) {
01124           push @txn_mods, ['split-modified', $split, 'notes',
01125                            $split->get_notes_()];
01126           $split->set_notes_($new_split->get_notes_());
01127         }
01128         if($new_split->get_debit_ != $split->get_debit_()) {
01129           push @txn_mods, ['split-modified', $split, 'debit',
01130                            $split->get_debit_()];
01131           $split->set_debit_($new_split->get_debit_());
01132         }
01133         if($new_split->get_credit_ != $split->get_credit_()) {
01134           push @txn_mods, ['split-modified', $split, 'credit',
01135                            $split->get_credit_()];
01136           $split->set_credit_($new_split->get_credit_());
01137         }
01138         if($new_split->get_status_ ne $split->get_status_()) {
01139           push @txn_mods, ['split-modified', $split, 'status',
01140                            $split->get_status_()];
01141           $split->set_status_($new_split->get_status_());
01142         }
01143       }
01144       $split->set_clone_(undef);
01145     }
01146     $txn->set_splits_($new_splits);
01147     $txn->set_clone_(undef);
01148     
01149     if(@txn_mods) {
01150       $self->update_dirty_txns_hash_(\%dirty_sinks, $txn);
01151       push @modifications, [ $txn, \@txn_mods ];
01152     }
01153   }
01154 
01155   # Re-sort if needed, so the positions will be right for the next
01156   # steps.
01157   if($date_changed) {
01158     print STDERR "Re-sorting txns\n";
01159     my $txns = $self->get_txns();
01160     @$txns = sort {
01161       $a->get_date <=> $b->get_date(); 
01162     } @$txns;
01163   }
01164 
01165   # Act on dirty ledgers hash.
01166   my $acct_data;
01167   foreach $acct_data (values %dirty_sinks) {
01168     my $acct = $$acct_data[0];
01169     my $acct_txns = $$acct_data[1];
01170     my ($txn_indices, $moves) = 
01171         $acct->ledger_modify_txns_(\%txn_date_changed, 
01172                                    $acct_txns);
01173     push @$acct_data, $txn_indices, $moves;
01174   }
01175   
01176   if(@modifications) {
01177     
01178     debug_txns_modified_data($self, \@modifications, \%dirty_sinks);
01179     
01180     # txns-modified callback
01181     my $callbacks_hash = $self->get_callbacks_hash_();
01182     my $callbacks = $$callbacks_hash{'txns-modified'};
01183     my $callback;
01184     foreach $callback (@$callbacks) {
01185       my $func = $$callback[0];
01186       my $args = $$callback[1];
01187 
01188       # @modifications elements are of the form
01189       # [ $txn, [ mod, mod, mod, ...]]
01190 
01191       # %dirty_sinks
01192       # Key: $sink
01193       # Value: [ $sink, @$txns, @$current_indices, @$moves]
01194       #        where @$moves is a ref to a list of pairs
01195       #        of the form [$prev_pos, $new_pos] sorted on $prev_pos
01196 
01197       &$func($self, \@modifications, \%dirty_sinks, $args);
01198       
01199     }
01200   }
01201 }
01202 
01203 
01204 sub begin_txn_modifications {
01205   my ($self) = shift;
01206   my $mod_level = $self->get_modified_txns_level_();
01207   if(!$mod_level) {
01208     $self->set_modified_txns_({});
01209     #$self->set_modified_txns_serial_num_(0);
01210   }
01211   $self->set_modified_txns_level_($mod_level + 1);
01212 }
01213 
01214 sub end_txn_modifications {
01215   my ($self) = shift;
01216   my $mod_level = $self->get_modified_txns_level_();
01217   if($mod_level == 1) {
01218     $self->post_modification_notices_();
01219   } elsif(!$mod_level) {
01220     die
01221         "Big problem.  Db::end_txn_modifications called when not modifying.\n";
01222   }
01223   $self->set_modified_txns_level_($mod_level - 1);
01224 }
01225 
01226 sub get_accts_by_name {
01227   my($self, $name) = @_;
01228   my $accts = $self->get_accts();
01229   my @matches = grep { 
01230     if($_) {
01231       $_->get_name() eq $name;
01232     } else {
01233       0;
01234     }
01235   } @$accts;
01236   return \@matches;
01237 }
01238 
01239 sub get_cats_by_name {
01240   my($self, $name) = @_;
01241   my $cats = $self->get_cats();
01242   my @matches = grep { 
01243     if($_) {
01244       $_->get_name() eq $name;
01245     } else {
01246       0;
01247     }
01248   } @$cats;
01249   return \@matches;
01250 }
01251 
01252 sub extract_accounts_ {
01253   my($text, $hash) = @_;
01254   my @accounts = split("\n", $text);
01255   return map {
01256     my $acct = $_;
01257     my @fields = split("\t", $acct);
01258     (scalar(@fields) < 4) or die "Wrong number of fields in account."; 
01259     
01260     my $name = $fields[1];
01261     my $notes = $fields[2];
01262     $acct = new CBBlib::Acct(undef, $name, $notes);
01263     $$hash{$fields[0]} = $acct;
01264     $acct;
01265   } @accounts;
01266 }
01267 
01268 sub extract_categories_ {
01269   my($text, $hash) = @_;
01270   my @categories = split("\n", $text);
01271   return map {
01272     my $cat = $_;
01273     my @fields = split("\t", $cat);
01274     (scalar(@fields) < 4) or die "Wrong number of fields in category."; 
01275 
01276     my $name = $fields[1];
01277     my $notes = $fields[2];
01278     $cat = new CBBlib::Cat(undef, $name, $notes);
01279     $$hash{$fields[0]} = $cat;
01280     $cat;
01281   } @categories;
01282 }
01283 
01284 sub calc_account_totals_only_ {
01285   my($self) = @_;
01286   my $transactions = $self->get_txns();
01287   my $accts = $self->get_accts();
01288   map {
01289     if($_) {
01290       $_->set_cleared_balance_(0);
01291       $_->set_final_balance_(0);
01292     }
01293   } @$accts;
01294 
01295   #my $cleared_balance = 0;
01296   #my $final_balance = 0;
01297 
01298   my $txn;
01299   foreach $txn (@$transactions) {
01300     my $splits = $txn->get_splits_();
01301     
01302     my $split;
01303     foreach $split (@$splits) {
01304       
01305       my $source = $txn->get_source();
01306       my $dest = $split->get_dest();
01307       my $debit = $split->get_debit();
01308       my $credit = $split->get_credit();
01309       
01310       my $cleared_bal;
01311       my $final_bal;
01312       my $diff = $credit - $debit;
01313       
01314       if($txn->cleared_p_()) {
01315         $cleared_bal = $source->get_cleared_balance() + $diff;
01316         $source->set_cleared_balance_($cleared_bal);
01317       }
01318       $final_bal = $source->get_final_balance() + $diff;
01319       $source->set_final_balance_($final_bal);
01320       
01321       if(ref($dest) eq 'CBBlib::Acct' && ($source != $dest)) {
01322         if($split->cleared_p_()) {
01323           $cleared_bal = $dest->get_cleared_balance();
01324           $dest->set_cleared_balance_($cleared_bal - $diff);
01325         }
01326         $final_bal = $dest->get_final_balance() - $diff;
01327         $dest->set_final_balance_($final_bal);
01328       }
01329     }
01330   }
01331 }
01332 
01333 
01334 sub add_callback_ {
01335   my($self, $name, $callback, $user_data) = @_;
01336   my $data = [$callback, $user_data];
01337   my $callbacks_hash = $self->get_callbacks_hash_();
01338   my $txn_callbacks = $$callbacks_hash{$name};
01339   if(!$txn_callbacks) { 
01340     $$callbacks_hash{$name} = [];
01341     $txn_callbacks = $$callbacks_hash{$name};
01342   }
01343   push @$txn_callbacks, $data;
01344   return $data;
01345 }
01346 
01347 sub remove_callback_ {
01348   my($self, $name, $callback_id) = @_;
01349   my $callbacks_hash = $self->get_callbacks_hash_();
01350   my $callbacks = $$callbacks_hash{$name};
01351   if(scalar(@$callbacks)) {    
01352     @$callbacks = grep { !($_ == $callback_id) } @$callbacks;
01353   }
01354 }
01355 
01356 # add_txn_callback
01357 # Called whenever new transactions are added
01358 # Called with args ($db, $new_txns, $user_data)
01359 
01360 sub add_txns_added_callback {
01361   my($self, $callback, $user_data) = @_;
01362   return $self->add_callback_('txns-added', $callback, $user_data);
01363 }
01364 
01365 
01366 sub remove_txns_added_callback {
01367   my($self, $callback_id) = @_;
01368   $self->remove_callback_('txns-added', $callback_id);
01369 }
01370 
01371 
01372 # add_txn_callback
01373 # Called whenever new transactions are added
01374 # Called with args ($db, $dead_txns, $user_data)
01375 
01376 sub add_txns_removed_callback {
01377   my($self, $callback, $user_data) = @_;
01378   return $self->add_callback_('txns-removed', $callback, $user_data);
01379 }
01380 
01381 
01382 sub remove_txns_removed_callback {
01383   my($self, $callback_id) = @_;
01384   $self->remove_callback_('txns-removed', $callback_id);
01385 }
01386 
01387 sub add_txns_modified_callback {
01388   my($self, $callback, $user_data) = @_;
01389   return $self->add_callback_('txns-modified', $callback, $user_data);
01390 }
01391 
01392 sub remove_txns_modified_callback {
01393   my($self, $callback_id) = @_;
01394   $self->remove_callback_('txns-modified', $callback_id);
01395 }
01396 
01397 sub merge_new_txns_into_main_list_ {
01398   my($self, $new_txns) = @_;
01399   my $txns = $self->get_txns();
01400   
01401   my $added_indices =
01402     main::destructive_merge_mangle($txns, $new_txns, sub {
01403       return $_[0]->get_date() cmp $_[1]->get_date();
01404     });
01405   
01406   map { $_->set_db_($self); } @$new_txns;
01407 
01408   my %affected_sinks;
01409   my $txn;
01410   foreach $txn (@$new_txns) {
01411     my @accts = $txn->affected_sinks();
01412     my $acct;
01413     foreach $acct (@accts) {
01414       my $data = $affected_sinks{$acct};
01415       if(!$data) { $data = $affected_sinks{$acct} = [$acct, []]; }
01416       my $list = $$data[1];
01417       push @$list, $txn;
01418     }
01419   }
01420   
01421   return (\%affected_sinks, $added_indices);
01422 }  
01423 
01424 sub merge_new_txns_into_ledger_lists_ {
01425   my($self, $new_txns, $affected_accts) = @_;
01426 
01427   # $affected accts is a hash mapping accounts to [acct,
01428   # relevant_txns] acct is a ref to the account, and relevant txns is
01429   # a ref to a list of the relevant transactions.  The transactions
01430   # must be ordered in each list like they are in the global DB.
01431 
01432   # We're going to add the resulting new ledger indices to the hash
01433   # values so we have: [acct, relevant_txns, indices]
01434 
01435   my $data;
01436   foreach $data (values(%$affected_accts)) {
01437     my $acct_ref = $$data[0];
01438     my $txns = $$data[1];
01439     my $added_indices = $acct_ref->ledger_add_txns_($txns);
01440     push @$data, $added_indices;
01441   }
01442 }
01443 
01444 sub add_txns {
01445   my($self, $new_txns) = @_;
01446 
01447   @$new_txns = sort {
01448     $a->get_date() <=> $b->get_date();
01449   } @$new_txns;
01450 
01451   my ($affected_accts, $added_indices) =
01452       $self->merge_new_txns_into_main_list_($new_txns);
01453   
01454   $self->merge_new_txns_into_ledger_lists_($new_txns, $affected_accts);
01455   
01456   my $callbacks_hash = $self->get_callbacks_hash_();
01457   my $txn_callbacks = $$callbacks_hash{'txns-added'};
01458   my $callback;
01459   foreach $callback (@$txn_callbacks) {
01460     my $func = $$callback[0];
01461     my $args = $$callback[1];
01462     
01463     if($main::pref_debug) {
01464       print STDERR
01465           "(txns-added\n" .
01466               "   db: $self\n" . 
01467                   '   added-indices: (' . join("\n" . 
01468                                                '                   ', @$added_indices) . ")\n" .
01469                                                    "   (affected-accts\n";
01470       my $acct_data;
01471       foreach $acct_data (values(%$affected_accts)) {
01472         my $acct = $$acct_data[0];
01473         my $txns = $$acct_data[1];
01474         my $indices = $$acct_data[2];
01475         print STDERR
01476             "      acct: $acct\n" . 
01477                 '      txns: (' . join("\n" . 
01478                                        '             ', @$txns) . ")\n" .
01479                                            '      indices: (' . join("\n" . 
01480                                                                      '                ', @$indices) . ")\n";
01481       }
01482     CBBlib::debug('   args: ' . $args . "))\n");
01483     }
01484     &$func($self, $added_indices, $affected_accts, $args);
01485   } 
01486 }
01487 
01488 sub remove_txns_from_ledger_lists_ {
01489   my($self, $dead_txns, $affected_accts) = @_;
01490   # $affected accts is a hash mapping accounts to refs to lists
01491   # of relevant transactions.  The transactions must be ordered
01492   # in each list like they are in the global DB.
01493 
01494   # returns a hash from $sink to a listref of [$sink, @$txn_info] where
01495   # @$txn_info is a lists of [$txn, $prev_ledger_index] pairs
01496 
01497   my %result;
01498 
01499   my $acct;
01500   foreach $acct (keys(%$affected_accts)) {
01501     my $data = $$affected_accts{$acct};
01502     my $acct_ref = $$data[0];
01503     my $txns = $$data[1];
01504     my $removal_info = $acct_ref->ledger_remove_txns_($txns);
01505     $result{$acct} = [$acct, $removal_info];
01506   }
01507   return \%result;
01508 }  
01509 
01510 sub remove_txns_from_main_list_ {
01511   my($self, $dead_txns) = @_;
01512   my $txns = $self->get_txns();
01513 
01514   my $removed_indices = 
01515     main::destructive_remove_mangle($txns, $dead_txns, sub {
01516       return $_[0] == $_[1];
01517     });
01518 
01519   map { $_->set_db_($self); } @$dead_txns;
01520 
01521   my %affected_accts;
01522   my $txn;
01523   foreach $txn (@$dead_txns) {
01524     my @accts = $txn->affected_sinks();
01525     my $acct;
01526     foreach $acct (@accts) {
01527       my $data = $affected_accts{$acct};
01528       if(!$data) { $data = $affected_accts{$acct} = [$acct, []]; }
01529       my $list = $$data[1];
01530       push @$list, $txn;
01531     }
01532   }
01533   
01534   return (\%affected_accts, $removed_indices);
01535 }  
01536 
01537 sub remove_txns {
01538   my($self, $dead_txns) = @_;
01539   
01540   @$dead_txns = sort {
01541     $a->get_date() cmp $b->get_date();
01542   } @$dead_txns;
01543 
01544   my ($affected_accts, $removed_db_indices) =
01545       $self->remove_txns_from_main_list_($dead_txns);
01546 
01547   my $ledger_removal_info = 
01548       $self->remove_txns_from_ledger_lists_($dead_txns, $affected_accts);
01549   
01550   my $callbacks_hash = $self->get_callbacks_hash_();
01551   my $txn_callbacks = $$callbacks_hash{'txns-removed'};
01552   my $callback;
01553   foreach $callback (@$txn_callbacks) {
01554     my $func = $$callback[0];
01555     my $args = $$callback[1];
01556     &$func($self, $dead_txns,
01557            $removed_db_indices,
01558            $ledger_removal_info,
01559            $args);
01560   } 
01561 }
01562 
01563 sub print_sinks {
01564   my ($self, $fh, $id_map) = @_;
01565 
01566   print $fh "#### Accounts ####\n";
01567       my $accts = $self->get_accts();
01568   map { $_->print($fh, '', $id_map); } @$accts;
01569   undef $accts;
01570   print $fh "\n";
01571 
01572   print $fh "#### Categories ####\n";
01573       my $cats = $self->get_cats();
01574   map { $_->print($fh, '', $id_map); } @$cats;
01575   undef $cats;
01576   print $fh "\n";
01577 }
01578 
01579 sub print_txns {
01580   my ($self, $fh, $id_map) = @_;
01581   print $fh "#### Transactions ####\n\n";
01582       my $txns = $self->get_txns();
01583   my $txn;
01584   foreach $txn (@$txns) {
01585     $txn->print($fh, '',$id_map);
01586     print $fh "\n";
01587   }
01588 }
01589 
01590 sub print {
01591   my($self, $fh) = @_;  
01592   
01593   my %id_map;
01594   my $i = 0;
01595   my $accts = $self->get_accts();
01596   map {
01597     $id_map{$_} = "a$i";
01598     $i++;
01599   } @$accts;
01600   $i = 0;
01601   my $cats = $self->get_cats();
01602   map {
01603     $id_map{$_} = "c$i";
01604     $i++;
01605   } @$cats;
01606 
01607   print $fh "# CBB data file\n";
01608   print $fh "Version: 1.0\n";
01609   print $fh 'Default-sink: ' . $id_map{$self->get_default_sink()} . "\n";
01610   print $fh "\n";
01611 
01612   $self->print_sinks($fh, \%id_map);
01613   $self->print_txns($fh, \%id_map);
01614 }
01615 
01616 package CBBlib;
01617 
01618 sub key_colon_value_to_hash {
01619   my($text) = @_;
01620   # Assumes comment lines have already been stripped.
01621   
01622   my @lines = split("\n", $text);
01623   my %data;
01624   
01625   map {
01626     if($_ =~ m/\s*([^:]+):\s*(.*)$/o) {
01627       $data{$1} = $2;
01628     } else {
01629       die 'Bad line in database file, first "key: value" section.';
01630     }
01631   } @lines;
01632   return \%data;        
01633 }
01634 
01635 sub load_file {
01636   # Args (filename:<string>)
01637 
01638   elapsed_reset("Starting load");
01639   my $name = shift;
01640   my $categories;
01641   my $accounts;
01642   my @transactions = ();
01643   my $fh = new IO::File;
01644   my $file;
01645   
01646   $fh->open($name) or die "Can't open input data file $file.";
01647   $fh->input_record_separator('');
01648   
01649   # Get the initial key/value pairs.
01650   my $text = <$fh>;
01651   $text =~ s/#.*//mgo;  # Kill comment lines.
01652   $text =~ s/^\n//mgo;  # Kill blank lines.
01653   my $file_data = key_colon_value_to_hash($text);
01654 
01655   die "Couldn't determine data file version"
01656       unless $$file_data{'Version'};
01657   die "Couldn't find default sink in $text." 
01658       unless $$file_data{'Default-sink'};
01659 
01660   my %sink_map = ();
01661 
01662   # Get accounts
01663   $text = <$fh>;
01664   $text =~ s/#.*\n//mgo;  # Kill comment lines.
01665   my @sinks = CBBlib::Db::extract_accounts_($text, \%sink_map);
01666   
01667   # Get categories
01668   $text = <$fh>;
01669   $text =~ s/#.*\n//mgo;  # Kill comment lines.
01670   push @sinks, CBBlib::Db::extract_categories_($text, \%sink_map);
01671 
01672   my $default_sink = $sink_map{$$file_data{'Default-sink'}};
01673   die "Default sink " . $$file_data{'Default-sink'} . 
01674       " not in database file." unless $default_sink;
01675 
01676   my $self = new CBBlib::Db($default_sink);
01677 
01678   $self->add_sinks(\@sinks);
01679 
01680   
01681   my @new_txns = ();
01682 
01683   # Read the "Transactions" comment line.
01684   scalar(<$fh>);
01685 
01686   my $count = 1;
01687   while(<$fh>) {
01688     print "\rLoading record $count";
01689     $count++;
01690     
01691     my @lines = split('\n', $_);
01692     my $transaction_info = shift @lines;
01693     my($date, $source, $checknum, $description, $status) = 
01694         split("\t", $transaction_info);
01695     $source = $sink_map{$source};
01696     
01697     if(!defined($status) || $status eq '') { $status = ' '; }
01698 
01699     my $transaction =
01700         new CBBlib::Txn($date, $source, $checknum, $description, $status);
01701     
01702     # @lines is now just the split lines
01703     my $split_line;
01704     foreach $split_line (@lines) {
01705       $split_line =~ s/^\s*//o;
01706       my($destination, $note, $debit, $credit, $status) = 
01707           split("\t", $split_line);
01708       if($destination) {
01709         my $old_dest = $destination;
01710         $destination = $sink_map{$destination};
01711         if(!$destination) {
01712           die "No destination for key [$old_dest]\n";
01713         }
01714       } else {
01715         $destination = $self->get_default_sink();
01716       }
01717 
01718       $transaction->add_split(new CBBlib::Split($destination, $note,
01719                                                 $debit, $credit, $status));
01720     }
01721     push @new_txns, $transaction;
01722   }
01723   print "\n";
01724   elapsed();
01725   elapsed_reset("  Now adding transactions");
01726   $self->add_txns(\@new_txns);
01727   elapsed();
01728   elapsed_reset("  Calculating totals");
01729   $self->calc_account_totals_only_();
01730   elapsed();
01731   $fh->close() or die "Can't open close file $name.";
01732   print STDERR "finished load\n";
01733 
01734   return $self;
01735 }
01736 
01737 1;
01738 __END__
01739 ## @endcond Perl
 All Data Structures Files Functions Variables Typedefs Enumerations Enumerator Defines