The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Math::BaseArith;

use 5.006;
use strict;
use warnings;
use integer;
use Carp;

require Exporter;

our @ISA = qw(Exporter);

our($debug) = 0;

# Items to export into callers namespace by default. Note: do not export
# names by default without a very good reason. Use EXPORT_OK instead.
# Do not simply export all your public functions/methods/constants.

# This allows declaration	use Math::BaseArith ':all';
# If you do not need this, moving things directly into @EXPORT or @EXPORT_OK
# will save memory.
our %EXPORT_TAGS = ( 'all' => [ qw(
	encode
	decode
	$Math::BaseArith::debug
) ] );

our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );

our @EXPORT = qw(
	encode
	decode	
);
our $VERSION = '1.00';

#######################################################################

sub encode {
	croak "Function called in void context" unless defined wantarray;
	
	my $value = shift; # value to be encoded
	my $b_listRef = shift; # list of base values

	my @b_list = @$b_listRef; # copy the base value list
	my @r_list;
	my @radix_list = (1);
	
	print STDERR "encode($value ,[@$b_listRef])" 
		if $Math::BaseArith::debug >= 1;

	my $r = 0;	
	my $b = 1;

	# Compute the radix divisors from the base list, and put in reverse order
	# [1760,3,12] miles/yards/feet/inches becomes [63360,5280,1760]
	# [2,2,2,2] becomes [16,8,4,2]
	do {
		$b *= pop @b_list;
		unshift @radix_list, $b;
	} while @b_list;

	my $i = 0;
	foreach $b (@radix_list) {
		$i++;
		if ($b > $value) {
			printf STDERR "%10d%10d%10d%10d\n", $b,$value,$r,$value%$b 
				if $Math::BaseArith::debug >= 2;
			push @r_list, 0 if $i > 1;
			next;
		}
		my $r = $b ? int($value/$b) : 0;
		printf STDERR "%10d%10d%10d%10d\n", $b,$value,$r,$b?$value%$b:0 if $Math::BaseArith::debug >= 2;
		push @r_list, $r;
		$value %= $b if $b;
	}
	
	shift @r_list while (scalar(@r_list) > scalar(@$b_listRef));
	
	return wantarray ? @r_list : \@r_list;	
}	

#######################################################################

sub decode {
	my $r_listRef = shift; # list of representation values
	my $b_listRef = shift; # list of base values

	print STDERR "decode([@$r_listRef],[@$b_listRef])" 
		if $Math::BaseArith::debug >= 1;

	if ( scalar(@$r_listRef) > scalar(@$b_listRef) && 
		 scalar(@$b_listRef) != 1 )
	{
		carp "length error";
		return; 
	}
	
	my $value = 0;
	my $b = 1;
	my $base = 1;
	my $r;

	do {
		$r = pop @$r_listRef;
		$value += $r * $base;
		printf STDERR "%10d%10d%10d%10d\n", $r,$b,$base,$value 
			if $Math::BaseArith::debug >= 2;
		$b = pop @$b_listRef || $b;
		$base *= $b;
	} while @$r_listRef;
	$value;
}

1;
__END__

=head1 NAME

Math::BaseArith - Perl extension for mixed-base number representation (like APL encode/decode)

=head1 SYNOPSIS

  use Math::BaseArith;
  encode( value, base_list );
  decode( representation_list, base_list );

=head1 DESCRIPTION

The inspiration for this module is a pair of functions in the APL
programming language called encode (a.k.a. "representation") and decode
(a.k.a. base-value). Their principal use is to convert numbers from one
number base to another. Mixed number bases are permitted. 

In this perl implementation, the representation of a number in a
particular number base consists of a list whose elements are the digit
values in that base. For example, the decimal number 31 would be
expressed in binary as a list of five ones with any number of leading
zeros: [0, 0, 0, 1, 1, 1, 1, 1]. The same number expressed as three
hexadecimal (base 16) digits would be [0, 1, 15], while in base 10 it
would be [0, 3, 1]. Fifty-one inches would be expressed in yards, feet,
inches as [1, 1, 3], an example of a mixed number base. 

In the following description of encode and decode, Q will mean an
abstract value or quantity, R will be its representation and B will
define the number base. Q will be a perl scalar; R and B are perl lists.
The values in R correspond to the radix values in B. 

In the examples below, assume the output of B<print> has been altered by
setting $, = ' ' and that C<=E<gt>> is your shell prompt. 

=head1 &encode

Encode is used to represent a number in one or more number bases. The
first argument is the number to be converted, and the second argument
defines the base (or bases) to be used for the representation. Consider
first the representation of a scalar in a single uniform number base: 

    print encode( 2, [2, 2, 2, 2] )
    => 0 0 1 0

    print encode( 5, [2, 2, 2, 2] )
    => 0 1 0 1

    print encode( 13, [2, 2, 2, 2] )
    => 1 1 0 1

    print encode( 62, [16, 16, 16] )
    => 0 3 14

The second argument is called the base list. The length of the
base list determines the number of digits in the representation of
the first argument. No error occurs if the length is insufficient to
give a proper representation of the number. Exploring this situation
will suggest other uses of encode, and may clarify the use of encode
with mixed number bases. 

    # The representation of 75 in base 4
    print encode( 75, [4, 4, 4, 4] )
    => 1 0 2 3

    # At least four digits are needed for the full representation
    print encode( 75, [4, 4, 4] )
    => 0 2 3

    # If fewer elements are in the second argument,
    # leading digits do not appear in the representation.
    print encode( 75, [4, 4] )
    => 2 3

    # If the second argument is a one-element list, encode is identical
    # to modulus (%)
    print encode( 75, [4] )
    => 3
    print encode( 76, [4] )
    => 0

    # The expression encode( Q, [0] ) always yields Q as the result
    print encode ( 75, [0] )
    => 75

    # This usage returns quotient and remainder
    print encode( 75, [0, 4] )
    => 18 3

    # The first quotient (18) is again divided by 4,
    # yielding a second quotient and remainder
    print encode( 75, [0, 4, 4] )
    => 4 2 3

    # The process is repeated again. Since the last quotient
    # is less than 4, the result is the same as encode(75,[4,4,4,4])
    print encode( 75, [0, 4, 4, 4] )
    => 1 0 2 3

Now consider a mixed number base: convert 175 inches into yards, feet,
inches. 

    # 175 inches is 14 feet, 7 inches (quotient and remainder). 
    print encode( 175, [0, 12] )
    => 14 7

    # 14 feet is 4 yards, 2 feet,
    print encode( 14, [0, 3] )
    => 4 2
       
    # so 175 inches is 4 yards, 2 feet, 7 inches.
    print encode( 175, [0, 3, 12] )
    => 4 2 7

=head1 &decode

Decode (or base-value) is used to determine the value of the
representation of a quantity in some number base. If B<R> is a list
representation (perhaps produced by the encode function described above)
of some quantity B<Q> in a number base defined by the radix list B<B> (i.e.,
C<@R = encode($Q,@B)>, then the expression C<decode(@R,@B)> yields C<$Q>: 

    print decode( [0, 0, 1, 0], [2, 2, 2, 2] )
    => 2

    print decode( [0, 1, 0, 1], [2, 2, 2, 2] )
    => 5

    print decode( [0, 3, 14], [16, 16, 16]
    => 62

The length of the representation list must be less than or equal to
that of the base list.

    print decode( [1, 1, 1, 1], [2, 2, 2, 2] )
    => 15

    print decode( [1, 1, 1, 1], [2] )
    => 15

    print decode( [1], [2, 2, 2, 2] )
    => 15

    print decode( [1, 1, 1, 1], [2, 2, 2] )
    => (void) 
    raises a LENGTH ERROR

As with the encode function, mixed number bases can be used:

    # Convert 4 yards, 2 feet, 7 inches to inches.
    print decode( [4, 2, 7], [0, 3, 12] )
    => 175


    # Convert 2 days, 3 hours, 5 minutes, and 27 seconds to seconds 
    print decode( [2, 3, 5, 27], [0, 24, 60, 60] )
    => 183927

    # or to minutes.
    print decode( [2, 3, 5, 27], [0, 24, 60, 60] ) / 60
    => 3065.45

The first element of the radix list (second argument) is not used; it is
required only to make the lengths match and so can be any value. 

=head1 EXPORT

  use Math::BaseArith;
   &encode
   &decode

  use Math::BaseArith ':all';
   &encode
   &decode
   BaseArith::debug

=head1 DEBUGGING

Import the global $Math::BaseArith::debug to print debugging information to STDERR.

If set to 1, function names and parameters are printed.

If set to 2, intermediate results are also printed.

=head1 LIMITATIONS

The APL encode function allows both arguments to be a list, in which case the
function evaluates in dot-product fashion, generating a matrix whose columns
are the representation vectors for each value in the value list; i.e. a call
such as encode([15,31,32,33,75],[4,4,4,4]) would generate the following matrix;

	0 0 0 0 1
	0 1 2 2 0
	3 3 0 0 2
	3 3 0 1 3

This version of encode supports only a scalar value as the first argument.

The APL version of decode support non-integer values.  This version doesn't.

=head1 AUTHOR

Gary Puckering E<lt>gary.puckering@cognos.comE<gt>

=head1 SEE ALSO

L<http://www.acm.org/sigapl/encode.htm>.

=cut