On Stackoverflow, there’s the question How can I convert a number to its multiple form in Perl?. The best answer for the particular problem is Number::Bytes::Human. Beyond, that, many of the answers take the usual if-elsif-else
, such as the one suggested by Michael Cramer:
sub magnitudeformat { my $val = shift; my $expstr; my $exp = log($val) / log(10); if ($exp < 3) { return $val; } elsif ($exp < 6) { $exp = 3; $expstr = "K"; } elsif ($exp < 9) { $exp = 6; $expstr = "M"; } elsif ($exp < 12) { $exp = 9; $expstr = "G"; } # Or "B". else { $exp = 12; $expstr = "T"; } return sprintf("%0.1f%s", $val/(10**$exp), $expstr); }
A variation on that uses the conditional operator suggested by spaulson:
sub BytesToReadableString($) { my $c = shift; $c >= 1073741824 ? sprintf("%0.2fGB", $c/1073741824) : $c >= 1048576 ? sprintf("%0.2fMB", $c/1048576) : $c >= 1024 ? sprintf("%0.2fKB", $c/1024) : scalar($c) . "bytes"; } print BytesToReadableString(225939) . "/s\n";
The Effective Perler looks at those solutions and sees that they are mostly repeated code. He might also notice that they assume, as hard-coded values, a limit of the size of the input, and that they don't handle negative numbers.
Michael Cramer almost has it right: you can figure out the magnitude through logarithms, but he takes a bit longer to get there. He divides by log(10)
and then has to juggle powers of three. There might be a bug there—the sort that makes hard drives seem bigger than they really are. Instead, get the exponent with the step size as the base, which you can change between, say, 1,000 or 1,024 depending on how you feel that day:
my $step_size = 1024; my $exponent = log( abs $number ) / log( $step_size );
If $exponent
is 0, the number needs no suffix. If it is 1, the number needs the suffix for the first step size, If 2, the second step size, and so on. You don't care what the step size is or how many orders of 10 (or 2 or e or whatever) are between the steps. The Effective Perler always tries to reduce the number of assumptions in the code, and when he can't, moves those assumptions outside the immediate area so he can change them independently.
Notice the use of abs
: if someone gives you a negative number, you still have to work. The Effective Perler guards against potentially fatal operations, such as taking the logarithm of a negative number. For the meat of this algorithm, you don't care which side of 0 the number is so the sign doesn't really matter. It's just decoration on the front.
Although you might not recognize the list in both of the solutions, it's there. Those Perlers just didn't put it into a list, but you can do that yourself. Define an array to hold the possible suffixes. Once you have them in array, you can easily change any of them or add additional suffixes:
my @suffixes = ( '', qw(K M G T) );
Now, the value of $exponent
is also the index of the suffix we want, unless you don't have enough elements in @suffixes
. You could add many suffixes, or you can limit the value of $exponent
to the highest index in @suffixes
:
$exponent = $#suffices if $exponent > $#suffices;
Putting all of that together gives you a subroutine that knows nothing about the step size or the suffixes, has no loops, and more importantly, no repeated code. It traps garbage input that doesn't look like numbers. It also handles the special cases of negative number and numbers below the step size with the same, unbranching logic path. The result is about the same size as the other solutions too:
use Scalar::Util qw(looks_like_number); BEGIN { my @suffices = ( '', qw(K M G T) ); my $step_size = 1024; sub number_to_kmgt { no warnings 'uninitialized'; my( $number ) = @_; return unless looks_like_number( $number ); my $exponent = eval { int( log( abs $number ) / log $step_size ) }; $exponent = $#suffices if $exponent > $#suffices; sprintf $exponent ? '%s%0.2f%s' : '%s%d%s', # format with special case $number >= 0 ? '' : '-', # sign abs $number / ($step_size ** $exponent), $suffices[ $exponent ]; } }
You can see more about some of the features of this subroutine in the book Item 49: Create closures to lock-in data in the book.
The Effective Perler also creates tests, so you have those too:
use Test::More 'no_plan'; my @tests = ( [ '', undef ], [ 0, '0' ], [ 123, '123' ], [ -123, '-123' ], [ 1234, '1.21K' ], [ -1234, '-1.21K' ], [ 1234567, '1.18M' ], [ 12345678, '11.77M' ], [ 123456789123, '114.98G' ], [ 12345678912345, '11497.81G' ], ); foreach my $test ( @tests ) { is( number_to_kmgt( $test->[0] ), $test->[1], "$test->[0] turns into $test->[1]" ); }