NAME
Database::BI::Calc - Safe recursive-descent expression evaluator for Database::BI
VERSION
0.012.0
SYNOPSIS
use Database::BI::Calc;
my $calc = Database::BI::Calc->new(
expr => 'if(qty > 0, round(amount / qty, 2), 0)',
columns => [qw(amount qty)],
);
die $calc->error if $calc->error;
for my $row (@rows) {
my $val = $calc->evaluate($row); # undef on runtime error
$row->{unit_price} = $val;
}
DESCRIPTION
A compile-once, evaluate-many expression engine. Expressions are parsed into a pure-Perl closure tree at construction time. No eval of user-supplied strings is ever performed anywhere in this module.
CONSTRUCTOR
new(%args)
my $calc = Database::BI::Calc->new(
expr => 'if(year(date) = 2026, amount * 1.1, amount)',
columns => [qw(date amount)],
);
Recognised keys:
- expr
-
The expression string. Maximum 512 characters.
- columns
-
Arrayref of allowed column names. Any identifier in
exprthat is not a known function name must appear in this list (or must match when underscores are replaced with spaces). Unknown identifiers cause a compile-time error stored inerror().
Always check error() after construction before calling evaluate().
METHODS
error
Returns the compile-time error string, or undef if the expression compiled successfully.
has_error
Returns true when error() is defined.
evaluate(\%row)
Evaluates the compiled expression against a data row. Returns the computed scalar value, or undef on any runtime error (division by zero, non-numeric operand, undef column value, etc.) rather than croaking.
OPERATORS
+ addition - subtraction
* multiplication / division (division by zero -> undef)
-x unary negation +x unary identity
(expr) grouping
= equal != not equal
< less than <= less-or-equal
> greater than >= greater-or-equal
Operator precedence (highest to lowest): unary, then * /, then + -, then comparisons. Comparisons yield 1 (true) or 0 (false). When both operands look numeric, numeric comparison is used; otherwise case-insensitive string comparison, matching _apply_filter_spec semantics.
Use = for equality, not ==. Writing == is a compile-time error.
BUILT-IN FUNCTIONS
String
upper(s) uc($s)
lower(s) lc($s)
trim(s) strip leading/trailing whitespace
len(s) length($s)
concat(a, b, ...) join('', ...), undef treated as ''
substr(s, start) 0-based start, to end of string
substr(s, start, len) 0-based start, len characters
Numeric
round(n, decimals) sprintf-based rounding
floor(n) POSIX::floor
ceil(n) POSIX::ceil
abs(n) absolute value
Conditional
if(cond, true, false) short-circuit ternary: only the taken branch is
evaluated, so if(qty > 0, amount/qty, 0) is safe
isnull(val, default) val if defined, else default
coalesce(v1, v2, ...) first defined value
Date (ISO-8601 YYYY-MM-DD only)
year(s) four-digit year (2026)
month(s) month number 1-12
day(s) day of month 1-31
quarter(s) quarter 1-4
week(s) ISO 8601 week number 1-53
month_name(s) full English month name ('January' .. 'December')
weekday(s) ISO 8601 weekday: 1 = Monday, 7 = Sunday
now() today's date as 'YYYY-MM-DD' (no arguments)
All date functions return undef when the value does not start with a valid YYYY-MM-DD prefix, rather than croaking.
Date normalisation (standalone helper)
date_normalise($value, $pattern) -> 'YYYY-MM-DD' or undef
Converts $value from a non-ISO format to ISO-8601 YYYY-MM-DD. Returns undef when the value does not match the pattern. Supported patterns:
%m/%d/%Y US slash-date (01/15/2026)
%d/%m/%Y EU slash-date (15/01/2026)
%d-%b-%Y DD-Mon-YYYY (15-Jan-2026; month abbreviation is case-insensitive)
This is called by Dashboard::_apply_date_normalization before calc fields are evaluated, so YEAR(date) sees ISO values regardless of the source format.
Binning
bin(value, width) '[lo,hi)' label for the half-open interval containing
value. Width must be a positive number. Integer widths
produce integer labels ('[0,100)'); fractional widths
use %g ('[0.5,1)')
COLUMN NAMES WITH SPACES
Column names containing spaces can be referenced by replacing spaces with underscores. per_unit_price in an expression addresses a column named "Per Unit Price" automatically.
SECURITY
No
evalof user-supplied strings is performed anywhere in this module.Column references are validated at compile time against the
columnswhitelist. Unknown identifiers are rejected atnew()time.Function names are validated against a hardcoded dispatch table. Unknown function names are rejected at
new()time.Expressions longer than 512 characters are rejected.
Nesting deeper than 20 levels is rejected.
Division by zero and undef/non-numeric operands return
undefrather than croaking, so a bad row does not abort an export pipeline.
DEPENDENCIES
POSIX, Scalar::Util, Readonly, Carp -- all present in the Database::BI dependency set.
SEE ALSO
Database::BI, Database::BI::Controller::Dashboard
AUTHOR
Nigel Horne, <njh@nigelhorne.com>
LICENSE
Same terms as Database::BI.