use
POSIX
qw/ceil floor/
;
our
$VERSION
=
'0.011'
;
our
@ISA
=
'Exporter'
;
our
@EXPORT_OK
=
'calc'
;
our
$ERROR
;
use
constant
ROUND_HALF
=> 0.50000000000008;
{
my
%operators
= (
'<<'
=> {
assoc
=>
'left'
},
'>>'
=> {
assoc
=>
'left'
},
'+'
=> {
assoc
=>
'left'
},
'-'
=> {
assoc
=>
'left'
},
'*'
=> {
assoc
=>
'left'
},
'/'
=> {
assoc
=>
'left'
},
'%'
=> {
assoc
=>
'left'
},
'^'
=> {
assoc
=>
'right'
},
'u-'
=> {
assoc
=>
'right'
},
'u+'
=> {
assoc
=>
'right'
},
);
my
@op_precedence
= (
[
'<<'
,
'>>'
],
[
'+'
,
'-'
],
[
'*'
,
'/'
,
'%'
],
[
'u-'
,
'u+'
],
[
'^'
],
);
my
(
%lower_prec
,
%higher_prec
);
$higher_prec
{
$_
} = 1
for
keys
%operators
;
foreach
my
$set
(
@op_precedence
) {
delete
$higher_prec
{
$_
}
for
@$set
;
foreach
my
$op
(
@$set
) {
$operators
{
$op
}{equal_to}{
$_
} = 1
for
@$set
;
$operators
{
$op
}{lower_than}{
$_
} = 1
for
keys
%higher_prec
;
$operators
{
$op
}{higher_than}{
$_
} = 1
for
keys
%lower_prec
;
}
$lower_prec
{
$_
} = 1
for
@$set
;
}
sub
_operator {
my
$oper
=
shift
;
croak
'No operator passed'
unless
defined
$oper
;
return
undef
unless
exists
$operators
{
$oper
};
return
$operators
{
$oper
};
}
sub
_real { blessed
$_
[0] ?
$_
[0]->Re :
$_
[0] }
my
%functions
= (
'<<'
=> {
args
=> 2,
code
=>
sub
{ _real(
$_
[0]) << _real(
$_
[1]) } },
'>>'
=> {
args
=> 2,
code
=>
sub
{ _real(
$_
[0]) >> _real(
$_
[1]) } },
'+'
=> {
args
=> 2,
code
=>
sub
{
$_
[0] +
$_
[1] } },
'-'
=> {
args
=> 2,
code
=>
sub
{
$_
[0] -
$_
[1] } },
'*'
=> {
args
=> 2,
code
=>
sub
{
$_
[0] *
$_
[1] } },
'/'
=> {
args
=> 2,
code
=>
sub
{
$_
[0] /
$_
[1] } },
'%'
=> {
args
=> 2,
code
=>
sub
{ _real(
$_
[0]) % _real(
$_
[1]) } },
'^'
=> {
args
=> 2,
code
=>
sub
{
$_
[0] **
$_
[1] } },
'u-'
=> {
args
=> 1,
code
=>
sub
{ -
$_
[0] } },
'u+'
=> {
args
=> 1,
code
=>
sub
{ +
$_
[0] } },
sqrt
=> {
args
=> 1,
code
=>
sub
{
sqrt
$_
[0] } },
pi
=> {
args
=> 0,
code
=>
sub
{ pi } },
i
=> {
args
=> 0,
code
=>
sub
{ i } },
e
=> {
args
=> 0,
code
=>
sub
{
exp
1 } },
ln
=> {
args
=> 1,
code
=>
sub
{
log
$_
[0] } },
log
=> {
args
=> 1,
code
=>
sub
{
log
(
$_
[0])/
log
(10) } },
logn
=> {
args
=> 2,
code
=>
sub
{
log
(
$_
[0])/
log
(
$_
[1]) } },
sin
=> {
args
=> 1,
code
=>
sub
{
sin
$_
[0] } },
cos
=> {
args
=> 1,
code
=>
sub
{
cos
$_
[0] } },
tan
=> {
args
=> 1,
code
=>
sub
{ tan
$_
[0] } },
asin
=> {
args
=> 1,
code
=>
sub
{ asin
$_
[0] } },
acos
=> {
args
=> 1,
code
=>
sub
{ acos
$_
[0] } },
atan
=> {
args
=> 1,
code
=>
sub
{ atan
$_
[0] } },
abs
=> {
args
=> 1,
code
=>
sub
{
abs
$_
[0] } },
int
=> {
args
=> 1,
code
=>
sub
{
int
_real(
$_
[0]) } },
floor
=> {
args
=> 1,
code
=>
sub
{ floor _real(
$_
[0]) } },
ceil
=> {
args
=> 1,
code
=>
sub
{ ceil _real(
$_
[0]) } },
rand
=> {
args
=> 0,
code
=>
sub
{
rand
} },
round
=> {
args
=> 1,
code
=>
sub
{ _real(
$_
[0]) >= 0
? floor(_real(
$_
[0]) + ROUND_HALF)
: ceil(_real(
$_
[0]) - ROUND_HALF) } },
);
sub
_default_functions { +{
%functions
} }
my
$singleton
;
sub
_instance { blessed
$_
[0] ?
$_
[0] : (
$singleton
//=
$_
[0]->new) }
}
sub
new {
my
$class
=
shift
;
my
$self
= {};
bless
$self
,
$class
;
return
$self
;
}
sub
_functions {
my
$self
=
shift
;
$self
->{_functions} = _default_functions()
unless
defined
$self
->{_functions};
return
$self
->{_functions};
}
sub
error {
my
$self
=
shift
;
return
exists
$self
->{error} ?
$self
->{error} :
undef
;
}
sub
_set_error {
my
(
$self
,
$error
) =
@_
;
$self
->{error} =
$error
;
return
$self
;
}
sub
clear_error {
my
$self
=
shift
;
delete
$self
->{error};
}
sub
add_functions {
my
(
$self
,
%functions
) =
@_
;
foreach
my
$name
(
keys
%functions
) {
croak
"Function \"$name\" has invalid name"
unless
$name
=~ m/\A[a-z]\w*\z/i;
my
$definition
=
$functions
{
$name
};
$definition
= {
args
=> 0,
code
=>
$definition
}
if
ref
$definition
eq
'CODE'
;
croak
"No argument count for function \"$name\""
unless
defined
(
my
$args
=
$definition
->{args});
croak
"Invalid argument count for function \"$name\""
unless
$args
=~ m/\A\d+\z/ and
$args
>= 0;
croak
"No coderef for function \"$name\""
unless
defined
(
my
$code
=
$definition
->{code});
croak
"Invalid coderef for function \"$name\""
unless
ref
$code
eq
'CODE'
;
$self
->_functions->{
$name
} = {
args
=>
$args
,
code
=>
$code
};
}
return
$self
;
}
sub
remove_functions {
my
(
$self
,
@functions
) =
@_
;
foreach
my
$name
(
grep
{
defined
}
@functions
) {
next
unless
exists
$self
->_functions->{
$name
};
next
if
defined
_operator(
$name
);
delete
$self
->_functions->{
$name
};
}
return
$self
;
}
my
$token_re
=
qr{(
( 0x[0-9a-f]+ | 0b[01]+ | 0[0-7]+ ) # Octal/hex/binary numbers
| (?: \d*\. )? \d+ (?: e[-+]?\d+ )? # Decimal numbers
| [(),] # Parentheses and commas
| \w+ # Functions
| (?: [-+*/^%] | << | >> ) # Operators
)}
ix;
sub
parse {
my
(
$self
,
$expr
) =
@_
;
$self
= _instance(
$self
);
my
(
@expr_queue
,
@oper_stack
,
$binop_possible
);
while
(
$expr
=~ /
$token_re
/g) {
my
(
$token
,
$octal
) = ($1, $2);
$token
=
oct
$octal
if
defined
$octal
and
length
$octal
;
if
(
$binop_possible
and
$token
ne
')'
and
$token
ne
','
and !
defined
_operator(
$token
)) {
_shunt_operator(\
@expr_queue
, \
@oper_stack
,
'*'
);
}
if
(
defined
_operator(
$token
)) {
if
(!
$binop_possible
and (
$token
eq
'-'
or
$token
eq
'+'
)) {
$token
=
"u$token"
;
}
_shunt_operator(\
@expr_queue
, \
@oper_stack
,
$token
);
$binop_possible
= 0;
}
elsif
(
$token
eq
'('
) {
_shunt_left_paren(\
@expr_queue
, \
@oper_stack
);
$binop_possible
= 0;
}
elsif
(
$token
eq
')'
) {
_shunt_right_paren(\
@expr_queue
, \
@oper_stack
)
or
die
"Mismatched parentheses\n"
;
$binop_possible
= 1;
}
elsif
(
$token
eq
','
) {
_shunt_comma(\
@expr_queue
, \
@oper_stack
)
or
die
"Misplaced comma or mismatched parentheses\n"
;
$binop_possible
= 0;
}
elsif
(looks_like_number
$token
) {
_shunt_number(\
@expr_queue
, \
@oper_stack
,
$token
);
$binop_possible
= 1;
}
elsif
(
$token
=~ m/\A\w+\z/) {
die
"Invalid function \"$token\"\n"
unless
exists
$self
->_functions->{
$token
};
if
(
$self
->_functions->{
$token
}{args} > 0) {
_shunt_function_with_args(\
@expr_queue
, \
@oper_stack
,
$token
);
$binop_possible
= 0;
}
else
{
_shunt_function_no_args(\
@expr_queue
, \
@oper_stack
,
$token
);
$binop_possible
= 1;
}
}
else
{
die
"Unknown token \"$token\"\n"
;
}
}
while
(
@oper_stack
) {
die
"Mismatched parentheses\n"
if
$oper_stack
[-1] eq
'('
;
push
@expr_queue
,
pop
@oper_stack
;
}
return
\
@expr_queue
;
}
sub
_shunt_number {
my
(
$expr_queue
,
$oper_stack
,
$num
) =
@_
;
push
@$expr_queue
,
$num
;
return
1;
}
sub
_shunt_operator {
my
(
$expr_queue
,
$oper_stack
,
$oper
) =
@_
;
my
$oper_stat
= _operator(
$oper
);
my
$assoc
=
$oper_stat
->{assoc};
while
(
@$oper_stack
and
defined
_operator(
my
$top_oper
=
$oper_stack
->[-1])) {
if
(
$oper_stat
->{lower_than}{
$top_oper
}
or (
$assoc
eq
'left'
and
$oper_stat
->{equal_to}{
$top_oper
})) {
push
@$expr_queue
,
pop
@$oper_stack
;
}
else
{
last
;
}
}
push
@$oper_stack
,
$oper
;
return
1;
}
sub
_shunt_function_with_args {
my
(
$expr_queue
,
$oper_stack
,
$function
) =
@_
;
push
@$oper_stack
,
$function
;
return
1;
}
sub
_shunt_function_no_args {
my
(
$expr_queue
,
$oper_stack
,
$function
) =
@_
;
push
@$expr_queue
,
$function
;
return
1;
}
sub
_shunt_left_paren {
my
(
$expr_queue
,
$oper_stack
) =
@_
;
push
@$oper_stack
,
'('
;
return
1;
}
sub
_shunt_right_paren {
my
(
$expr_queue
,
$oper_stack
) =
@_
;
while
(
@$oper_stack
and
$oper_stack
->[-1] ne
'('
) {
push
@$expr_queue
,
pop
@$oper_stack
;
}
return
0
unless
@$oper_stack
and
$oper_stack
->[-1] eq
'('
;
pop
@$oper_stack
;
if
(
@$oper_stack
and
$oper_stack
->[-1] ne
'('
and !
defined
_operator(
$oper_stack
->[-1])) {
push
@$expr_queue
,
pop
@$oper_stack
;
}
return
1;
}
sub
_shunt_comma {
my
(
$expr_queue
,
$oper_stack
) =
@_
;
while
(
@$oper_stack
and
$oper_stack
->[-1] ne
'('
) {
push
@$expr_queue
,
pop
@$oper_stack
;
}
return
0
unless
@$oper_stack
and
$oper_stack
->[-1] eq
'('
;
return
1;
}
sub
calc ($) { __PACKAGE__->evaluate(
$_
[0]) }
sub
evaluate {
my
(
$self
,
$expr
) =
@_
;
$self
= _instance(
$self
);
$expr
=
$self
->parse(
$expr
)
unless
ref
$expr
eq
'ARRAY'
;
die
"No expression to evaluate\n"
unless
@$expr
;
my
@eval_stack
;
foreach
my
$token
(
@$expr
) {
die
"Undefined token in evaluate\n"
unless
defined
$token
;
if
(
exists
$self
->_functions->{
$token
}) {
my
$function
=
$self
->_functions->{
$token
};
my
$num_args
=
$function
->{args};
die
"Malformed expression\n"
if
@eval_stack
<
$num_args
;
my
@args
=
$num_args
> 0 ?
splice
@eval_stack
, -
$num_args
: ();
local
$@;
my
$result
;
my
$rc
=
eval
{
$result
=
$function
->{code}(
@args
); 1 };
unless
(
$rc
) {
my
$err
= $@;
$err
=~ s/ at .+? line \d+\.$//i;
die
$err
;
}
die
"Undefined result from function or operator \"$token\"\n"
unless
defined
$result
;
{
no
warnings
'numeric'
;
push
@eval_stack
, 0+
$result
;
}
}
elsif
(looks_like_number
$token
) {
push
@eval_stack
,
$token
;
}
else
{
die
"Invalid function or operator \"$token\"\n"
;
}
}
die
"Malformed expression\n"
if
@eval_stack
> 1;
return
$eval_stack
[0];
}
sub
try_evaluate {
my
(
$self
,
$expr
) =
@_
;
$self
= _instance(
$self
);
$self
->clear_error;
undef
$ERROR
;
local
$@;
my
$result
;
my
$rc
=
eval
{
$result
=
$self
->evaluate(
$expr
); 1 };
unless
(
$rc
) {
my
$err
= $@;
chomp
$err
;
$self
->_set_error(
$ERROR
=
$err
);
return
undef
;
}
return
$result
;
}
1;