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

use strict;
use Exporter;
use vars qw($VERSION @ISA @EXPORT_OK @EXPORT);
@ISA=qw(Exporter);
@EXPORT=();
@EXPORT_OK=qw(add subtract multiply divide exponentiate
				  normalise sign negative invert);

$VERSION = '0.13';

=head1 NAME

Silly::StringMaths - Perl extension for doing maths with strings

=head1 SYNOPSIS

  use Silly::StringMaths qw(add subtract multiply divide exponentiate);

  # Add two positive numbers - returns ABFOOR
  print add("FOO", "BAR");

  # Add a generally positive number and a negative number
  # - returns ot
  print add("FNoRD", "yncft");

  # Subtract several numbers from a rather large one
  # - returns accdeiiiiloopssu
  print subtract("Supercalifragilisticepsialidocious",
					  "stupid", "made", "up", "word");

  # Multiply two negative numbers - returns AAACCCCCCEEELLLNNN
  print multiply("cancel", "out");

  # Divide two numbers - returns AAA
  print divide("EuropeanCommission", "France");

  # Confirm Pythagorus' theorum - returns nothing
  print subtract(exponentiate("FETLA", "PI"),
					  exponentiate("TLA", "PI"),
					  exponentiate("ETLA", "PI"));

=head1 DESCRIPTION

Silly::StringMaths provides support for basic integer mathematics, using
strings rather than numbers. Upper-case letters are positive,
lower-case letters are negative, so ABCDEF would be 6 (but
WOMBAT would also be 6), whereas C<positive> would actually be
-8. Mixed-case is also possible, so Compaq is actually -5.
Most methods return a canonicalised version of the string -
e.g. C<ampq> rather than C<Compaq> (mixed case removed,
the result sorted alphabetically).

The behaviour of other characters is as yet undefined, but be
warned that non-alphabetical characters may be reserved for
floating point or imaginary numbers.

Actual numbers (i.e. the characters 0 to 9) will I<never> be used
by this module.

=head1 BASIC METHODS

=head2 add

Takes an array of strings, returns the sum.

=cut

sub add {
	my $base=shift;
	# Go through our arguments in turn
	while (my $addition=shift) {
		# If there are any positive elements, add them on now
		while ($addition =~ s/([A-Z])//) {
			$base.=$1;
		}
		# If there are any negative elements, subtract:
		while ($addition =~ s/([a-z])//) {
			# First take away any positive letters
			if ($base =~ /[A-Z]/) {
				$base =~ s/[A-Z]//;
			} else {
				# Then add on negative ones
				$base.=$1;
			}
		}
	}
	# Return a normalised (i.e. sorted and condensed) version of this
	return Silly::StringMaths::normalise($base);
}

=head2 subtract

Takes a string, subtracts all other supplied strings from it and
returns the result.

=cut

sub subtract {
	my ($base, @others)=@_;
	# Find our base, subtract all other numbers by adding negative
	# versions of them
	foreach (@others) {
		Silly::StringMaths::invert(\$_);
	}
	return Silly::StringMaths::add($base, @others);
}

=head2 multiply

Takes a string and multiplies it by all the other strings,
returning the resulting product.

=cut

sub multiply {
	# Find our base number, normalise it
	my $base=Silly::StringMaths::normalise(shift);
	while (my $product=Silly::StringMaths::normalise(shift)) {
		# If the argument is negative, invert the base number
		if (Silly::StringMaths::negative($product)) {
			Silly::StringMaths::invert(\$base);
		}
		# Now add on the base number as many times as we have extra letters
		# (so remove one letter from the product first)
		$product =~ s%.%%;
		my $step=$base;
		while ($product =~ s%.%%) {
			$base=Silly::StringMaths::add($base, $step);
		}
	}
	return $base;
}

=head2 divide

Takes a string, and divides it by all the other strings,
returning the result. Results are rounded down.

=cut

sub divide {
	# Find our base number, normalise it
	my $base=Silly::StringMaths::normalise(shift);
	# Find the sign of this number, convert the base number to positive
	my $sign=Silly::StringMaths::sign($base);
	if (Silly::StringMaths::negative($sign)) {
		$base=Silly::StringMaths::multiply($base, $sign);
	}
	# Step through our divisors
	while (my $divisor=Silly::StringMaths::normalise(shift)) {
		# If this divisor is negative, invert our sign
		if (Silly::StringMaths::negative(Silly::StringMaths::sign($divisor))) {
			Silly::StringMaths::invert(\$sign);
			Silly::StringMaths::invert(\$divisor);
		}
		# Now find how many times we can remove our divisor
		# First convert our divisor to a regexp that can remove itself from
		# a number, then apply it as many times as possible, and insert
		# that many As into the return value
		$divisor =~ s%.%.%g;
		$base="A"x($base =~ s%$divisor%%g);
	}
	# Multiply our (positive) base number by the stored sign, return it
	return Silly::StringMaths::multiply($base, $sign);
}

=head2 exponentiate

Takes a number, raises it to the appropriate power, as specified by the
other arguments. Returns the result. (Note that some textual information
is lost here - the result will be either C<A>s or C<a>s).

=cut

sub exponentiate {
	my $base=Silly::StringMaths::normalise(shift);
	while (my $power=Silly::StringMaths::normalise(shift)) {
		# Don't allow negative powers
		if (Silly::StringMaths::negative($power)) {
			warn "Cannot use negative power $power";
			return undef;
		}
		# Find the number we multiply by (the original base number)
		my $multiply=$base;
		# Remove one 
		$power =~ s%.%%;
		# For every remaining digit, multiply base by its original value
		while ($power =~ s%.%%) {
			$base=Silly::StringMaths::multiply($base, $multiply);
		}
	}
	return $base;
}

=head1 USEFUL TOOLBOX METHODS

=head2 normalise

Takes a string with, potentially, a mix of upper-case and lower-case
letters, and returns a sorted string that is unmistakeably either
positive or negative.

=cut

sub normalise {
	my ($number)=@_;

	# If there's a mixture of upper and lower case, add this number to
	# a null string to make the negatives and positives cancel out
	if ($number =~ /[A-Z]/ && $number =~ /[a-z]/) {
		$number=Silly::StringMaths::add(undef, $number);
	}
	# Return this string in sorted form
	return join("", sort split("", $number));
}

=head2 sign

Returns the sign of a number as either 1, 0 or -1 (as a string,
obviously).

=cut

sub sign {
	# Take the sign of a number by normalising it, then removing all but
	# the first character, so we have 1 or -1
	my $number=Silly::StringMaths::normalise(shift);
	$number =~ s%^(.).*%$1%;
	return $number;
}

=head2 negative

Returns whether the supplied string is negative or not

=cut

sub negative {
	my ($number)=@_;
	return (Silly::StringMaths::normalise($number) =~ /[a-z]/);
}

=head2 invert

Takes a I<reference> to a number, inverts it.

=cut

sub invert {
	my ($number)=shift;
	$$number = Silly::StringMaths::normalise($$number);
	$$number =~ tr/A-Za-z/a-zA-Z/;
	return $$number;
}


=head1 AUTHOR

Sam Kington, sam@illuminated.co.uk

=head1 SEE ALSO

perl(1).

=cut


1;