From: Ismaël Bouya Date: Mon, 31 Aug 2015 17:39:25 +0000 (+0200) Subject: Amélioration script comptes X-Git-Url: https://git.immae.eu/?p=perso%2FImmae%2FProjets%2FScripts%2FPublic.git;a=commitdiff_plain;h=c2590696efd0b91972c238ab892555f739751bc9 Amélioration script comptes --- diff --git a/comptes b/comptes index 0fbacf2..853615a 100755 --- a/comptes +++ b/comptes @@ -1,4 +1,4 @@ -#!/usr/bin/en perl +#!/usr/bin/env perl # The MIT License (MIT) # # Copyright (c) 2011-2015 Ismaël Bouya http://www.normalesup.org/~bouya/ @@ -31,416 +31,505 @@ my %participants = (); my @transactions = (); my @descriptions = (); my $evenement_def; -my $remboursements = ""; - -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; +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; } - while(my $transaction = ) { - if($transaction eq "\n") { last; } - chomp($transaction); - push(@transactions,$transaction); - my $description = ; - chomp($description); - push(@descriptions,$description); + 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"; } - $file = $ARGV[0]; + $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) { - if(keys(%participants)>0) { - $actions[1]="Ajouter une dépense"; - $actions[2]="Enregistrer"; + 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 = ; } - if(scalar(@transactions)>0) { - $actions[3]="Voir les dépenses de quelqu'un"; - $actions[2]="Enregistrer"; - $actions[4]="Equilibrage"; - $actions[5]="Voir une transaction"; + 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"); - my $action; - 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; + 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(); } - switch ($action) { - case "Ajouter un participant" { - print "Nom du participant\n"; - my $participant = ; - chomp($participant); - $participants{$participant} = keys(%participants)+1; - } - case "Ajouter une dépense" { - print "Qui a payé ?\n"; - my $paye; - while(1) { - my $i = 1; - my $var; - my @vart = ( keys(%participants), "Annuler" ); - foreach $var ( @vart ) { - print $i++.') '.$var."\n"; - } - $var = ; - chop($var); - if(/\D/ || $var < 1 || $var > $i) { - print "Mauvaise entrée\n"; - next; - } - $paye = $vart[$var-1]; - last; - } - if($paye eq "Annuler") { - next GLOBALE; - } - 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; - while(1) { - my $i = 1; - my $var; - my @vart = ( keys(%participants), "Ajouter à tous", "Fini" ); - foreach $var ( @vart ) { - print $i++.') '.$var."\n"; - } - $var = ; - chop($var); - if(/\D/ || $var < 1 || $var > $i) { - print "Mauvaise entrée\n"; - next; - } - $dest = $vart[$var-1]; - last; - } - switch($dest) { - case "Fini" { - if($repartition eq "") { print "Annulé\n"; } - 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); - } - last DESTS; - } - 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); - } - } - } - } - } - case "Voir les dépenses de quelqu'un" { - my $personne; - while(1) { - my $i = 1; - my $var; - my @vart = ( keys(%participants), "Annuler" ); - foreach $var ( @vart ) { - print $i++.') '.$var."\n"; - } - $var = ; - chop($var); - if(/\D/ || $var < 1 || $var > $i) { - print "Mauvaise entrée\n"; - next; - } - $personne = $vart[$var-1]; - last; - } - if($personne eq "Annuler") { - next GLOBALE; - } - my $depenses = 0; - my $frais = 0; - foreach my $transaction (@transactions) { - my @table_trans = split(/ /,$transaction); - my $payeur = shift(@table_trans); - my $prix = shift(@table_trans); - if($payeur == $participants{$personne}) { - $depenses += $prix; - } - my $parts = 0; - my $frac = 0; - my $repartition; - foreach $repartition (@table_trans) { - my ($pers,$part) = split(/:/,$repartition); - if($pers == $participants{$personne}) { - $frac += $part; - } - $parts += $part; - } - $frais += $prix*$frac/$parts; - } - print $personne." a payé ".$depenses." EUR\n"; - print "et a pour ".sprintf("%.2f",$frais)." EUR de frais au total.\n"; - print "Il a donc un solde de ".sprintf("%.2f",$depenses-$frais)." EUR\n"; - my $pause = ; - } - case "Voir une 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 @table_trans = split(/ /,$transactions[$transaction]); - my $payeur = shift(@table_trans); - my $prix = shift(@table_trans); - my $Tparts = 0; - my $repartition; - my %parts; - foreach my $pers (keys(%participants)) { - if($payeur == $participants{$pers}) { - print $descriptions[$transaction]." (payé par ".$pers."), ".$prix." EUR\n"; - last - } - } - foreach $repartition (@table_trans) { - my ($pers,$part) = split(/:/,$repartition); - $Tparts += $part; - $parts{$pers} += $part; - } - foreach my $pers (keys(%participants)) { - if(!exists $parts{$participants{$pers}}) { - next; - } - print $pers.", ".$parts{$participants{$pers}}." part(s), soit ". $prix*$parts{$participants{$pers}}/$Tparts." EUR\n"; - } - my $pause = ; - } - 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; - print "Y-a-t-il des remboursements plus aisés ?\n"; - my $ouinon; - while(1) { - my $i = 1; - my $var; - my @vart = ( "Oui", "Non" ); - foreach $var ( @vart ) { - print $i++.') '.$var."\n"; - } - $var = ; - chop($var); - if(/\D/ || $var < 1 || $var > $i) { - print "Mauvaise entrée\n"; - next; - } - $ouinon = $vart[$var-1]; - last; - } - AISE: while($ouinon eq "Oui") { - print "Qui ?\n"; - my $personne; - while(1) { - my $i = 1; - my $var; - my @vart = ( keys(%participants), "Fini" ); - foreach $var ( @vart ) { - print $i++.') '.$var."\n"; - } - $var = ; - chop($var); - if(/\D/ || $var < 1 || $var > $i) { - print "Mauvaise entrée\n"; - next; - } - $personne = $vart[$var-1]; - last; - } - if($personne eq "Fini") { last AISE; } - print "Avec qui ?\n"; - my $personneb; - while(1) { - my $i = 1; - my $var; - my @vart = ( keys(%participants), "Annuler" ); - foreach $var ( @vart ) { - print $i++.') '.$var."\n"; - } - $var = ; - chop($var); - if(/\D/ || $var < 1 || $var > $i) { - print "Mauvaise entrée\n"; - next; - } - $personneb = $vart[$var-1]; - last; - } - if($personneb eq "Annuler") { next AISE; } - if($Tsolde{$personne}*$Tsolde{$personneb}<0) { - my $pers_inf = ($Tsolde{$personne}<0)?$personne:$personneb; - my $pers_sup = ($Tsolde{$personne}>0)?$personne:$personneb; - my $inf = (abs($Tsolde{$personne})0)?$personne:$personneb; - my $val = abs($Tsolde{$personne}); - $remboursements .= $pers_inf." doit ".$val." EUR à ".$pers_sup."\n"; - $Tsolde{$pers_inf} += $val; - $Tsolde{$pers_sup} -= $val; - next COUPLE; - } - } - REMB: while(1) { - # On supprime les 0, et on fait deux hashs - my %Tneg = (); - my %Tpos = (); - foreach my $personne (keys(%Tsolde)) { - if($Tsolde{$personne} == 0) { delete $Tsolde{$personne}; } - elsif($Tsolde{$personne} < 0) { $Tneg{$personne} = $Tsolde{$personne}; } - elsif($Tsolde{$personne} > 0) { $Tpos{$personne} = $Tsolde{$personne}; } - } - if(keys(%Tneg) == 0) { - last REMB; - } - foreach my $neg (sort { $Tneg{$a} cmp $Tneg{$b} } keys %Tneg) { - my $max = 0; - my $positif = 0; - foreach my $pos (sort { $Tpos{$a} cmp $Tpos{$b} } keys %Tpos) { - if($Tpos{$pos} + $Tneg{$neg}>0) { - $positif = $pos; - last; - } - $max = $pos; - } - if(!($max eq 0)) { - $remboursements .= $neg." doit ".$Tsolde{$max}." EUR à ".$max."\n"; - $Tsolde{$neg} += $Tsolde{$max}; - delete $Tsolde{$max}; - } - else { - $remboursements .= $neg." doit ".-$Tsolde{$neg}." EUR à ".$positif."\n"; - $Tsolde{$positif} += $Tsolde{$neg}; - delete $Tsolde{$neg}; - } - next REMB; - } - } - print $remboursements; - my $pause = ; - } - case "Sortir" { - last GLOBALE; - } + case "Sortir" { + last GLOBALE; } + } }