=head1 NAME

XAO::DO::Web::Math - calculate and output a value

=head1 SYNOPSIS

 <%Math formula='{x}+{y}' value.x='2' value.y='3'%>

=head1 DESCRIPTION

Given a formula and some values calculates the result and displays it
optionally formatting according to the given format:

 <%Math formula='{x}+{y}' value.x='2' value.y='3'%>
    -- output '5'

 <%Math formula='1/{x}' value.x='3' format='%.3'%>
    -- output '0.333'

 <%Math formula='1 / ({a} - {b})' value.a='7' value.b='7' default='-'%>
    -- output '-'

Formulas should not be received from untrusted sources. They are
'eval'ed to calculate the result. Some care is taken to avoid illegal
formulas, but there are no guarantees.

When an operation cannot be performed (division by zero for instance)
the result is 'default' value or empty if not set. Illegal arguments,
such as non-numeric, produce the same result.

If a 'path' or 'template' is given then the result is shown in that
template with the following parameters:

  FORMULA   => formula with substituted values, as calculated
  RESULT    => calculation result
  ERROR     => error message if calculation could not be performed
  ERRCODE   => more concise error code (FORMULA, VALUE, FUNCTION, CALCULATE)

Some mathematical functions are also supported: min(), max(), sum(),
abs(), and sqrt(). The first three work on any number of arguments.

=head1 METHODS

=over

=cut

###############################################################################
package XAO::DO::Web::Math;
use strict;
use XAO::Utils;
use XAO::Objects;
use Error qw(:try);
use base XAO::Objects->load(objname => 'Web::Page');

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

# Some useful functions that are not a part of standard perl

sub min (@) {
    my $a=shift;
    foreach my $b (@_) {
        $a=$b if $a>$b;
    }
    return $a;
}

sub max (@) {
    my $a=shift;
    foreach my $b (@_) {
        $a=$b if $a<$b;
    }
    return $a;
}

sub sum (@) {
    my $a=0;
    foreach my $b (@_) {
        $a+=$b;
    }
    return $a;
}

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

my %functions=map { $_ => 1 } qw(
    min
    max
    sum
    abs
    sqrt
);

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

sub display ($%) {
    my $self=shift;
    my $args=get_args(\@_);

    my $formula=$args->{'formula'} ||
        throw $self "- need a formula";

    my $default=defined $args->{'default'} ? $args->{'default'} : '';

    my $format=$args->{'format'};

    my $result;
    my $error;
    my $errcode;

    try {
        my @fparts=split(/(\{\w+\})/,$formula);

        foreach my $part (@fparts) {
            if($part =~ /^\{(\w+)\}$/) {
                my $value=$args->{'value.'.$1} || 0;
                $value=~s/[\s\$\,_]//g;
                $value =~ /^([\d\.\+e-]+)$/ ||
                    throw $self "- {{VALUE: Illegal value for '$part'}}";
                $part=$value;
            }
            else {
                $part=~/^[\s\w\(\)\.\+\*\/,-]*$/ ||
                    throw $self "- {{FORMULA: Illegal formula part '$part'}}";

                if($part=~/(\w+)\s*\(/) {
                    $functions{$1} ||
                        throw $self "- {{FUNCTION: Illegal function '$1'}}";
                }
            }
        }

        ### dprint ".'$formula'";

        $formula=join('',@fparts);

        ### dprint "..->'$formula'";

        $result=eval '0.0+('.$formula.')';

        ### dprint "....=",$result;

        $@ && throw $self "- {{CALCULATE: Unable to calculate '$formula'}} ($@)";

        # Formatting if necessary
        #
        if($format) {
            $result=sprintf($format,$result);
            ### dprint "....=$result (formatted)";
        }
    }
    otherwise {
        my $e=shift;
        my $etext="$e";

        if($etext=~/\{\{(\w+):\s*(.*?)\s*\}\}/) {
            $errcode=$1;
            $error=$2;
        }
        else {
            $errcode='SYSTEM';
            $error=$etext;
        }

        $result=$default;

        dprint "Math error in '$formula': $error ($errcode)";
    };

    if($args->{'path'} || $args->{'template'}) {
        $self->object->display($args,{
            FORMULA     => $formula,
            RESULT      => $result,
            ERROR       => $error || '',
            ERRCODE     => $errcode || ($error ? 'UNKNOWN' : ''),
        });
    }
    else {
        $self->textout($result);
    }
}

###############################################################################
1;
__END__

=back

=head1 EXPORTS

Nothing.

=head1 AUTHOR

Copyright (c) 2012 Andrew Maltsev

<am@ejelta.com> -- http://ejelta.com/xao/

=head1 SEE ALSO

Recommended reading:
L<XAO::Web>,
L<XAO::DO::Web::Page>.