Suffixation of decimal numbers

Pass in a number string, optionally a type, and optionally the number of digits to round to.

The types supported are B, M & G for binary, metric or gigantic. (At this point, the only gigantic unit is googol, so maybe it stands for googol. ¯\(ツ)\/¯ )

If no type is specified, M (metric) is assumed.

If you desire the number to be rounded, pass in a number representing the placed past the decimal to round to. If you pass in a negative number for rounding, it will round to a negative number of places past the decimal.

sub sufficate ($val is copy, $type is copy = 'M', $round is copy = Any) {
   if +$type ~~ Int { $round = $type; $type = 'M' }
   my $s = '';
   if $val.substr(0,1) eq '-' { $s = '-'; $val.=substr(1) }
   $val.=subst(',', '', :g);
   if $val ~~ m:i/'e'/ {
       my ($m,$e) = $val.split(/<[eE]>/);
       $val = ($e < 0)
           ?? $m * FatRat.new(1,10**-$e)
           !! $m * 10**$e;
   }
   my %s = do given $type {
       when 'M' { <K M G T P E Z Y X W V U> Z=> (1000, * * 1000 … *) }
       when 'B' { <Ki Mi Gi Ti Pi Ei Zi Yi Xi Wi Vi Ui> Z=> (1024, * * 1024 … *) }
       when 'G' { googol => 10**100 }
       default { return 'What we have here is a failure to communicate...' }
   }
   my $k = do given $val {
       when .abs < (my $m = min %s.values) { %s.first( *.value == $m ).key };
       when .abs > (my $x = max %s.values) { %s.first( *.value == $x ).key };
       default { %s.sort(*.value).first({$val.abs/%s{$_.key} < min %s.values}).key}
   }
   $round.defined
       ?? $s ~ comma(($val.abs/%s{$k}).round(10**-$round)) ~ $k
       !! $s ~ comma($val.abs/%s{$k}) ~ $k
}

sub comma ($i is copy) {
    my $s = $i < 0 ?? '-' !! '';
    my ($whole, $frac) = $i.split('.');
    $frac = $frac.defined ?? ".$frac" !! '';
    $s ~ $whole.abs.flip.comb(3).join(',').flip ~ $frac
}

## TESTING

my @tests =
   '87,654,321',
   '-998,877,665,544,332,211,000 3',
   '+112,233 0',
   '16,777,216 1',
   '456,789,100,000,000',
   '456,789,100,000,000 M 2',
   '456,789,100,000,000 B 5',
   '456,789,100,000.000e+00 M 0',
   '+16777216 B',
   '1.2e101 G',
   "{run('df', '/', :out).out.slurp.words[10] * 1024} B 2", # Linux df returns Kilobytes by default
   '347,344 M -2', # round to -2 past the decimal
   '1122334455 Q', # bad unit type example
;

printf "%33s : %s\n", $_, sufficate(|.words) for @tests;

Output:

                       87,654,321 : 87.654321M
   -998,877,665,544,332,211,000 3 : -998.878E
                       +112,233 0 : 112K
                     16,777,216 1 : 16.8M
              456,789,100,000,000 : 456.7891T
          456,789,100,000,000 M 2 : 456.79T
          456,789,100,000,000 B 5 : 415.44727Ti
      456,789,100,000.000e+00 M 0 : 457G
                      +16777216 B : 16Mi
                        1.2e101 G : 12googol
                 703674818560 B 2 : 655.35Gi
                     347,344 M -2 : 300K
                     1122334455 Q : What we have here is a failure to communicate...