#!/usr/bin/env perl # The MIT License (MIT) # # Copyright (c) 2011-2015 Ismaël Bouya http://www.normalesup.org/~bouya/ # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # Feel like tipping/donating? https://www.immae.eu/licenses_and_tipping use Switch; use strict; my $file = "comptes.data"; my @actions=('Ajouter un participant'); my %participants = (); my @transactions = (); my @descriptions = (); my $evenement_def; my @remboursements = (); my %initiales = (); if($#ARGV == 0) { open FILE, $ARGV[0] || die "Impossible de lire le fichier"; $evenement_def = ; chomp($evenement_def); while(my $noms = ) { if($noms eq "\n") { last; } my ($numero,$nom) = split(/ /,$noms,2); chomp($nom); $participants{$nom} = $numero; } while(my $transaction = ) { if($transaction eq "\n") { last; } chomp($transaction); push(@transactions,$transaction); my $description = ; chomp($description); push(@descriptions,$description); } while(my $remboursement = ) { if($remboursement eq "\n") { last; } chomp($remboursement); my @remb = split(/ /, $remboursement); push(@remboursements,\@remb); } $file = $ARGV[0]; } sub choose_action { my @actions = ('Ajouter un participant'); my $action; if (keys(%participants) > 0) { $actions[1] = "Ajouter une dépense"; $actions[2] = "Enregistrer"; } if(scalar(@transactions) > 0) { $actions[2] = "Enregistrer"; $actions[3] = "Voir les frais de quelqu'un (dépenses - dettes)"; $actions[4] = "Voir les frais de tout le monde"; $actions[5] = "Equilibrage"; $actions[6] = "Voir une transaction"; } if(scalar(@remboursements) > 0) { $actions[7] = "Afficher les remboursements"; } while(1) { my $i = 1; my $var; my @vart = ( @actions, 'Sortir' ); my $message = ''; foreach $var ( @vart ) { $message .= $i++.') '.$var."\n"; } print $message; $var = ; chop($var); if(/\D/ || $var < 1 || $var > $i) { print "Mauvaise entrée\n"; next; } $action = $vart[$var-1]; last; } return $action; } sub pp { my ($price, $noteur) = @_; if (!$noteur) { return sprintf("%.2f", $price)." EUR"; } else { return sprintf("%.2f", $price); } } sub compute_initiales { %initiales = (); } sub add_participant { print "Nom du participant ?\n"; my $participant = ; chomp($participant); $participant =~ s/\s/_/g; $participants{$participant} = keys(%participants)+1; compute_initiales(); } sub select_menu { my (@vart) = @_; while(1) { my $i = 1; my $var; foreach $var ( @vart ) { print $i++.') '.$var."\n"; } $var = ; chop($var); if(/\D/ || $var < 1 || $var > scalar @vart) { print "Mauvaise entrée\n"; next; } return $vart[$var-1]; } } sub select_participant { my ($annuler, @supplementaires) = @_; while(1) { my $i = 1; my $var; my @vart = ( keys(%participants), @supplementaires, ($annuler) ); foreach $var ( @vart ) { print $i++.') '.$var."\n"; } $var = ; chop($var); if(/\D/ || $var < 1 || $var > scalar @vart) { print "Mauvaise entrée\n"; next; } if ($var != scalar @vart) { return $vart[$var-1]; } else { return; } } } sub select_transaction { my $transaction; while(1) { my $i = 1; my $var; my @vart = ( @descriptions, "Annuler" ); foreach $var ( @vart ) { print $i++.') '.$var."\n"; } $var = ; chop($var); if(/\D/ || $var < 1 || $var > $i) { print "Mauvaise entrée\n"; next; } $transaction = $var-1; last; } my ($payeur, $prix, $total_parts, $split, $parts) = &parse_transaction($transactions[$transaction]); return ($descriptions[$transaction], $payeur, $prix, $total_parts, $split, $parts); } sub add_transaction { print "Qui a payé ?\n"; my $paye = select_participant("Annuler"); if(!$paye) { print "Annulé"; return; } print "Quoi ?\n"; my $description = ; chomp($description); print "Combien ?\n"; my $prix = ; chomp($prix); print "Pour qui ?\n"; my $repartition = ""; my $total_parts = 0; DESTS: while(1) { my $dest = select_participant("Fini", "Ajouter à tous"); switch($dest) { case undef { if($repartition eq "") { print "Annulé\n"; return; } elsif($total_parts == 0) { print "Le nombre total de parts est nul, il faut en ajouter !\n"; next DESTS; } else { push(@transactions, $participants{$paye}.' '.$prix.$repartition); push(@descriptions, $description); return; } } case "Ajouter à tous" { print "Nombre de parts ?\n"; my $parts = ; chomp($parts); if(!($parts == 0)) { foreach my $participant (keys(%participants)) { $repartition .= ' '.$participants{$participant}.':'.scalar($parts); $total_parts += scalar($parts); } } } else { print "Nombre de parts ?\n"; my $parts = ; chomp($parts); if(!($parts == 0)) { $repartition .= ' '.$participants{$dest}.':'.scalar($parts); $total_parts += scalar($parts); } } } } } sub find_participant { my ($participant_number) = @_; foreach my $name (keys(%participants)) { if ($participant_number == $participants{$name}) { return $name; } } } sub parse_transaction { my ($transaction) = @_; my @table_trans = split(/ /,$transaction); my $payeur = shift(@table_trans); $payeur = find_participant($payeur); my $prix = shift(@table_trans); my %parts; my %split; my $total_parts = 0; foreach my $repartition (@table_trans) { my ($participant, $part) = split(/:/, $repartition); $participant = find_participant($participant); $parts{$participant} ||= 0; $parts{$participant} += $part; $total_parts += $part; } if (!$parts{$payeur}) { $parts{$payeur} = 0; } foreach my $participant (keys(%parts)) { $split{$participant} = - ($parts{$participant} * $prix) / $total_parts; if ($participant eq $payeur) { $split{$participant} += $prix; } } return ($payeur, $prix, $total_parts, \%split, \%parts); } sub max_length { my (@strings) = @_; my @string_lengths = sort { length($a) <=> length($b) } @strings; return length($string_lengths[-1]); } sub list_transactions_for_participants { my (@participants) = @_; my $one_participant = 0; if (scalar @participants == 0) { @participants = keys(%participants); } if (scalar @participants == 1) { $one_participant = 1; } my %depenses; my %solde; my @zipped = map {($transactions[$_], $descriptions[$_])} (0 .. $#descriptions); while (scalar(@zipped) > 0) { my $transaction = shift @zipped; my $description = shift @zipped; my ($payeur, $prix, $total_parts, $parsed_transaction, $parts) = &parse_transaction($transaction); $depenses{$payeur} ||= 0; $depenses{$payeur} += $prix; my @intersection = grep( $parsed_transaction->{$_}, @participants); if (scalar @intersection) { my $phantom = " " x length($description.":"); my $max_participant_length = max_length(@intersection); print $description.":"; foreach my $participant (@intersection) { $solde{$participant} ||= 0; $solde{$participant} += $parsed_transaction->{$participant}; if ($one_participant) { print " ".pp($parsed_transaction->{$participant}); if ($payeur eq $participant) { print " (a payé, ".pp($prix).")"; } print "\n"; } else { my $participant_padding = " " x ($max_participant_length - length($participant)); print " ".$participant.$participant_padding." ".pp($parsed_transaction->{$participant}); if ($payeur eq $participant) { print " (a payé, ".pp($prix).")"; } print "\n".$phantom; } } if (!$one_participant) { print "\n"; } } } foreach my $personne (@participants) { my $d = $depenses{$personne} || 0; my $s = $solde{$personne} || 0; if ($d) { print $personne." a payé ".pp($d); } else { print $personne." n'a rien payé"; } if ($s) { print " et a pour ".pp($d - $s)." de frais au total.\n"; } else { print " et n'a pas eu de frais.\n"; } print $personne." a donc un solde de ".pp($s)."\n"; } } sub compute_equity { my %depenses; my %frais; my %soldes; foreach my $transaction (@transactions) { my ($payeur, $prix, $total_parts, $split, $parts) = &parse_transaction($transaction); $depenses{$payeur} += $prix; foreach my $personne (keys(%{$split})) { $soldes{$personne} += $split->{$personne}; } } foreach my $personne (keys(%soldes)) { $frais{$personne} = $depenses{$personne} - $soldes{$personne}; } return (\%depenses, \%frais, \%soldes); } sub delete_zeros { my ($soldes, @participants) = @_; foreach my $participant (@participants) { delete $soldes->{$participant} if pp($soldes->{$participant}, 1) == 0; } } sub compute_reimbursment { my ($a, $b, $soldes) = @_; my $s_a = $soldes->{$a}; my $s_b = $soldes->{$b}; return if (pp($s_a, 1) * pp($s_b, 1) >= 0); my $debtor = pp($s_a, 1) < 0 ? $a : $b; my $creditor = pp($s_a, 1) > 0 ? $a : $b; my $exchange = (abs($s_a) < abs($s_b)) ? abs($s_a) : abs($s_b); my @remboursement = ($debtor, $creditor, $exchange); push @remboursements, \@remboursement; $soldes->{$creditor} -= $exchange; $soldes->{$debtor} += $exchange; delete_zeros($soldes, $a, $b); } sub select_exchange_couple { my ($soldes) = @_; my $bd; # biggest debtor my $sc; # smallest creditor foreach my $p (keys(%{$soldes})) { $bd = $p if $soldes->{$p} < 0 && (!$bd || $soldes->{$p} < $soldes->{$bd}); $sc = $p if $soldes->{$p} > 0 && (!$sc || $soldes->{$p} < $soldes->{$sc}); } return ($bd, $sc); } sub print_remboursements { foreach my $remboursement (@remboursements) { my ($debtor, $creditor, $exchange) = (@{$remboursement}); print $debtor." doit ".pp($exchange)." à ".$creditor."\n"; } my $pause = ; } GLOBALE: while(1) { my $action = choose_action(); switch ($action) { case "Ajouter un participant" { add_participant(); } case "Ajouter une dépense" { add_transaction(); } case "Voir les frais de quelqu'un (dépenses - dettes)" { my $personne = select_participant("Annuler"); if(!$personne) { next GLOBALE; } list_transactions_for_participants(($personne)); my $pause = ; } case "Voir les frais de tout le monde" { list_transactions_for_participants(keys(%participants)); my $pause = ; } case "Voir une transaction" { my ($description, $payeur, $prix, $total_parts, $split, $parts) = select_transaction(); print $description." (payé par ".$payeur."), ".pp($prix)."\n"; foreach my $pers (keys(%{$split})) { print $pers.", ".$parts->{$pers}." part(s), soit ". pp($split->{$pers}); if ($pers eq $payeur) { print " (".pp($prix)." - ".pp($prix - $split->{$pers}).")"; } print "\n"; } } case "Enregistrer" { print "Nom de l'événement ?"; if(!($evenement_def eq "")) { print " [".$evenement_def."]"; } print "\n"; my $evenement = ; chomp($evenement); if($evenement eq "") { $evenement = $evenement_def; } open FILE, ">".$file; print FILE $evenement."\n"; my $key; foreach $key (keys(%participants)) { print FILE $participants{$key}." ".$key."\n"; } print FILE "\n"; for(my $i=0;$i{$personne})." (+" . pp($depenses->{$personne}). " / -". pp($frais->{$personne}) . ")\n"; } my $pause = ; print "Y-a-t-il des remboursements plus aisés ?\n"; my $ouinon = select_menu("Oui", "Non"); AISE: while($ouinon eq "Oui") { print "Qui ?\n"; my $personnea = select_participant("Fini"); if(!$personnea) { last AISE; } print "Avec qui ?\n"; my $personneb = select_participant("Annuler"); if(!$personneb) { next AISE; } compute_reimbursment($personnea, $personneb, $soldes); } # Une boucle pour détecter si on a des couples qui s'annulent mutuellement delete_zeros($soldes, keys(%{$soldes})); COUPLE: foreach my $personnea (keys(%participants)) { next if(pp($soldes->{$personnea}, 1) == 0); foreach my $personneb (keys(%participants)) { next if (pp($soldes->{$personneb} + $soldes->{$personnea}, 1) != 0); compute_reimbursment($personnea, $personneb, $soldes); next COUPLE; } } REMB: while(1) { last REMB if(keys(%{$soldes}) == 0); my ($biggest_debtor, $smallest_creditor) = select_exchange_couple($soldes); compute_reimbursment($biggest_debtor, $smallest_creditor, $soldes); } print_remboursements(); } case "Afficher les remboursements" { print_remboursements(); } case "Sortir" { last GLOBALE; } } }