http://qs321.pair.com?node_id=202914
Category: Text Processing
Author/Contact Info John Clyman (module-support@clyman.com)
Description: Create an object that will pretty-format numbers using SI prefixes, your preferred truncation behavior, and so on. For example, return a number like '12' as '12 bytes' and a number like 1048976 as '1 MB'.
package ScaleUnits;
$VERSION = '0.01';

use strict;
use warnings;
use Carp;

my @si_prefixes = qw(kilo mega giga tera peta exa zetta yotta);

sub new {
  my $class = shift;
  my %args = @_;
  croak "ScaleUnits::new requires named parameter 'unit'\n" 
    unless defined $args{unit};
  %args = (plural    => $args{unit} . 's',
           factor    => 1000,
           round     => 0,
           brief     => '',
           separator => ' ',
           %args);
  my $self = \%args;
  bless $self, $class;
  return $self;
}

sub scale {
  my ($self, $value) = @_;
  return scale_units($value, 
                     $self->{unit}, 
                     $self->{plural}, 
                     $self->{factor},
                     $self->{round},
                     $self->{brief},
                     $self->{separator});
}

sub scale_units {
  my ($value, $base_unit, $plural, $factor, $round, $brief, $separator
+) = @_;

  my $order = 0;
  while ($value >= $factor && $order <= $#si_prefixes) {
    $value /= $factor;
    $order++;
  }  
  $value = int($value*(10**$round))/(10**$round);

  # if $order is 0, the number doesn't need a prefix
  # otherwise it needs a prefix. by default ($brief is ''), we use the
  # full SI prefix. If $brief contains something, we instead use the s
+hort
  # and capitalized form of the SI prefix (e.g. 'mega' becomes 'M').
  my $si_prefix = ($order == 0 ? 
                   '' :
                   $brief eq '' ? 
                     $si_prefixes[$order-1] : 
                     uc substr($si_prefixes[$order-1],0,1));

  # if $brief is set and we do have a prefix ($order >0), then use $br
+ief
  # as the units. Otherwise we have to decide between singular and plu
+ral.
  my $units = ($brief ne '' && $order > 0 ? 
               $brief : 
               $value == 1 ? 
                 $base_unit : 
                 $plural);

  return "$value$separator$si_prefix$units";   
}

1;

__END__

=head1 NAME

ScaleUnits - Perl class that takes numbers and formats them nicely, wi
+th
SI prefixes, user-selectable rounding, and more

=head1 SYNOPSIS

  use ScaleUnits;
  $converter = ScaleUnits->new(
    unit   => 'byte',
    factor => 1024,
    round  => 2
  );
  print $converter->scale(123_456_789);
  # '117.73 megabytes'

=head1 DESCRIPTION

You want your program to generate nice readable output like this:

  Transferred 12 bytes
  Transferred 2.14 gigabytes

instead of this:

  Transferred 12 byte(s)
  Transferred 2243953 byte(s)

Specifically, C<ScaleUnits> can apply SI prefixes from 'kilo' (10**3, 
+or 2**10
if you wish) through 'yotta' (10**24). It can round numbers to the num
+ber of
decimal places that you specify, and apply singular and plural forms a
+s
appropriate. It can also use abbreviated forms such as 'MB' and 'GB'.

You define the desired behavior when you construct a C<ScaleUnits> obj
+ect, and 
then each time you call the C<scale> method with a specific number, th
+e
appropriate rules are applied and a nicely-formatted string is returne
+d.

=head1 PUBLIC METHODS

=head2 new(%params)

Create a new C<ScaleUnits> object. Pass parameters by name. A C<unit>
parameter is required; others are optional.

=over 4

=item unit

The name of the units you are scaling. Use the singular form:
'byte', 'minute', and so on. By default, C<ScaleUnits> will construct
a plural form by adding an 's' to this base unit, but you can
override this behavior by passing a C<plural> argument.

=item plural

The plural of the units you are scaling. By default, C<ScaleUnits>
creates the plural form of a unit by appending a single 's'.

=item factor

The factor you want to use between successive levels of the SI prefix
hierarchy. By default, this is 1,000. For scaling bytes and other 
base-2 numbers, you may want to make it 1,024:

  $s = ScaleUnits->new(unit => 'byte', factor => 1024);

Now 'kilo' will be used for a factor of 1,024, 'mega' for a factor of 
1,048,976, and so on.  

=item round

Round scaled numbers to this many decimal places (maximum). By default
+,
it's 0, and all results will be in whole numbers.

=item brief

Set C<brief> to the units you want to use when a number is large enoug
+h
to require an SI prefix. This will also cause the single-letter form o
+f
SI prefixes to be used. A typical use would be:

  ScaleUnits->new(unit => 'byte', brief => 'B');

This will result in output like this:

  35 bytes
  1 MB
  4 GB

By default, C<brief> is a null string, which means that the full SI pr
+efixes
are prepended onto the unit name (singular or plural as needed).

=item separator

Defines the separator that appears between the scaled number and its 
accompanying units. By default, it's a space, so results will read 
like '24 kilobytes'. By setting C<separator> to a null string you can
get results like '24KB' (no space). Setting C<separator> to a hyphen c
+ould
be useful for generating adjectival forms ('You uploaded a 24-kilobyte
+ file').

=back

=head2 scale($number)

Scale C<$number> using the settings defined when the object was constr
+ucted,
and return the results as a string.

=head1 SEE ALSO

If you want to do conversions between different units -- e.g., meters 
+to
inches -- you'll probably want to use a module like C<Math::Units> or
C<Convert::Units> instead.

If you want to handle singular and plural forms for arbitrary terms, t
+ry
C<Lingua::EN::Inflect>.

=head1 BUGS AND LIMITATIONS

Currently provides prefixes only for positive exponents -- there's no 
+provision
for converting a fractional number into milli-units, micro-units, and 
+so on.

Doesn't currently provide a provision for using the alternate SI prefi
+xes, such
as 'kibi', that I've read have been proposed for base-2 numbers.

=head1 RELEASE AND REVISION HISTORY

  0.01 - 4 October 2002 - first release

=head1 AUTHOR AND COPYRIGHT INFORMATION

(C) Copyright 2002 John Clyman (Z<>module-support@clyman.comZ<>, 
http://www.clyman.comZ<>). All Rights Reserved.

This program is free software and may be used and redistributed under 
+the
terms of the Artistic License.

=cut