view release on metacpan or search on metacpan
- implemented // for names
- implemented config save
version 0.11: Sat Feb 25 12:41:37 CET 2023
Fixes:
- module name registrations for PAUSE.
- handle accidental MF::REGEXP on 'like'.
- ::Type::new() did not appear in the docs.
- nested fragments now work.
- aliasing of fragments, attributes, formulas.
Improvements:
- warn when trying to convert qr to pattern.
- textual corrections seen on metacpan.
version 0.10: Fri 24 Feb 15:08:56 CET 2023
- initial release, for final documentation clean-ups, to test pause etc.
lib/Math/Formula/Config/JSON.pm
lib/Math/Formula/Config/JSON.pod
lib/Math/Formula/Config/YAML.pm
lib/Math/Formula/Config/YAML.pod
lib/Math/Formula/Context.pm
lib/Math/Formula/Context.pod
lib/Math/Formula/Token.pm
lib/Math/Formula/Type.pm
lib/Math/Formula/Type.pod
t/01use.t
t/10formula.t
t/11totype.t
t/20integers.t
t/21booleans.t
t/22strings.t
t/23duration.t
t/24datetime.t
t/25date.t
t/26time.t
t/27names.t
t/28pattern.t
Makefile.PL view on Meta::CPAN
},
homepage => 'http://perl.overmeer.net/CPAN/',
license => [ 'http://dev.perl.org/licenses/' ],
}
}
);
sub MY::postamble { <<'__POSTAMBLE' }
# for DIST
RAWDIR = ../public_html/math-formula/raw
DISTDIR = ../public_html/math-formula/source
LICENSE = perl
# for POD
FIRST_YEAR = 2023
EMAIL = markov@cpan.org
WEBSITE = http://perl.overmeer.net/CPAN/
__POSTAMBLE
lib/Math/Formula.pm view on Meta::CPAN
package Math::Formula;
use vars '$VERSION';
$VERSION = '0.16';
use warnings;
use strict;
use utf8;
use Log::Report 'math-formula';
use Scalar::Util qw/blessed/;
use Math::Formula::Token;
use Math::Formula::Type;
#--------------------------
sub new(%)
{ my ($class, $name, $expr, %self) = @_;
$self{_name} = $name;
$self{_expr} = $expr;
(bless {}, $class)->init(\%self);
}
sub init($)
{ my ($self, $args) = @_;
my $name = $self->{MSBE_name} = $args->{_name} or panic "every formular requires a name";
my $expr = $args->{_expr} or panic "every formular requires an expression";
my $returns = $self->{MSBE_returns} = $args->{returns};
if(ref $expr eq 'SCALAR')
{ $expr = MF::STRING->new(undef, $$expr);
}
elsif(! ref $expr && $returns && $returns->isa('MF::STRING'))
{ $expr = MF::STRING->new(undef, $expr);
}
$self->{MSBE_expr} = $expr;
lib/Math/Formula.pod view on Meta::CPAN
=encoding utf8
=head1 NAME
Math::Formula - expressions on steroids
=head1 SYNOPSIS
my $formula = Math::Formula->new('size', '42k + 324', %options);
my $formula = Math::Formula->new(Ï => 3.14);
my $size = $formula->evaluate;
# For a bit more complex formulas, you need a context object.
my $context = Math::Formula::Context->new(name => 'example');
$context->add( { size => '42k', header => '324', total => 'size + header' });
my $total = $context->value('total');
# To build connectors to objects in your program, interfaces.
# See Math::Formula::Context.
my $formula = Math::Formula->new(size => \&own_sub, %options);
=head1 DESCRIPTION
B<WARNING:> This is not a programming language: it lacks control
structures, like loops and blocks. This module can be used
to offer (very) flexible configuration (files) for users of your
application. See L<Math::Formula::Context|Math::Formula::Context> and L<Math::Formula::Config|Math::Formula::Config>.
B<What makes Math::Formula special?> Zillions of expression evaluators
have been written in the past. The application where this module was
lib/Math/Formula.pod view on Meta::CPAN
The expression needs a $name. Expressions can refer to each other via this name.
The $expression is usually a (utf8) string, which will get parsed and
evaluated on demand. The $expression may also be a prepared node (any
<Math::Formula::Type> object).
As special hook, you may also provide a CODE as $expression. This will
be called as
$expression->($context, $this_formula, %options);
Optimally, the expression returns any L<Math::Formula::Type|Math::Formula::Type> object. Otherwise,
auto-detection of the computed result kicks in. The %options are passed to
L<evaluate()|Math::Formula/"Running"> More details below in L<Math::Formula::Context/"CODE as expression">.
-Option --Default
returns undef
=over 2
=item returns => $type
Enforce that the type produced by the calculation of this $type. Otherwise, it may
be different when other people are permitted to configure the formulas... people can
make mistakes.
=back
=back
=head2 Accessors
=over 4
lib/Math/Formula.pod view on Meta::CPAN
used as a string; a wrong guess without consequences. It is preferred
that your CODE expressions return explicit types: for optimal safety and
performance.
See L<Math::Formula::Context/"CODE as expression"> for details.
=back
=head1 DETAILS
This module handles formulas. Someone (your application user) gets more power
in configuring its settings. Very simple example:
# In a back-up script, configured in JSON
"daily_backups" : "/var/tmp/backups/daily",
"weekly_backups" : "/var/tmp/backups/weekly",
# With Math::Formula::Config::JSON
"backup_dir" : "/var/tmp/backups/",
"daily_backups" : "= backup_dir ~ 'daily'",
"weekly_backups" : "= backup_dir ~ 'weekly'",
lib/Math/Formula.pod view on Meta::CPAN
own solution via L<Math::Formula::Context::new(lead_expressions)|Math::Formula::Context/"Constructors">. It is
possible to configure that all strings get the normal quotes, and expressions
start with C<=> (or any other leading string).
Your expressions can look like this:
my_age => '(#system.now.date - 1966-05-04).years',
is_adult => 'my_age >= 18',
Expressions can refer to values computed by other expressions. Also,
external objects can maintain libraries of formulas or produce compatible
data.
=head3 Sets of formulas
Let's start with a large group of related formulas, and the types they produce:
birthday: 1966-04-05 # DATE
os_lib: #system # other context is a FRAGMENT
now: os_lib.now # DATETIME 'now' is an attribute of system
today: now.date # DATE 'today' is an attribute of DATETIME
alive: today - birthday # DURATION
age: alive.years # INTEGER 'years' is an attr of DURATION
# this can also be written in one line:
lib/Math/Formula.pod view on Meta::CPAN
Or some backup configuration lines:
backup_needed: #system.now.day_of_week <= 5 # Monday = 1
backup_start: 23:00:00
backup_max_duration: PT2H30M
backup_dir: "/var/tmp/backups"
backup_name: backup_dir ~ '/' ~ "backup-" ~ weekday ~ ".tgz"
The application which uses this configuration, will run the expressions with
the names as listed. It may also provide some own formulas, fragments, and
helper methods as features.
=head2 Operators
As B<prefix> operator, you can use C<not>, C<->, C<+>, and C<exists>
on applicable data types. The C<#> (fragment) and C<.> (attributes)
prefixes are weird cases: see L<Math::Formula::Context|Math::Formula::Context>.
Operators only work on specific data types, but some types will
automatically convert. For instance, all types can be cast into
lib/Math/Formula/Config.pm view on Meta::CPAN
# Pod stripped from pm file by OODoc 2.03.
package Math::Formula::Config;
use vars '$VERSION';
$VERSION = '0.16';
use warnings;
use strict;
use File::Spec ();
use Log::Report 'math-formula';
sub new(%) { my $class = shift; (bless {}, $class)->init({@_}) }
sub init($)
{ my ($self, $args) = @_;
my $dir = $self->{MFC_dir} = $args->{directory}
or error __x"Save directory required";
-d $dir
lib/Math/Formula/Config.pod view on Meta::CPAN
=encoding utf8
=head1 NAME
Math::Formula::Config - load/save formulas to file
=head1 INHERITANCE
Math::Formula::Config is extended by
Math::Formula::Config::INI
Math::Formula::Config::JSON
Math::Formula::Config::YAML
=head1 SYNOPSIS
lib/Math/Formula/Config/INI.pm view on Meta::CPAN
# Pod stripped from pm file by OODoc 2.03.
package Math::Formula::Config::INI;
use vars '$VERSION';
$VERSION = '0.16';
use base 'Math::Formula::Config';
use warnings;
use strict;
use Log::Report 'math-formula';
use Scalar::Util 'blessed';
use Config::INI::Writer ();
use Config::INI::Reader ();
use Math::Formula::Context ();
use Math::Formula ();
#----------------------
sub save($%)
{ my ($self, $context, %args) = @_;
my $name = $context->name;
my $index = $context->_index;
my %tree = (
_ => $self->_set_encode($index->{attributes}),
formulas => $self->_set_encode($index->{formulas}),
);
my $fn = $self->path_for($args{filename} || "$name.ini");
Config::INI::Writer->write_file(\%tree, $fn);
}
sub _set_encode($)
{ my ($self, $set) = @_;
my %data;
$data{$_ =~ s/^ctx_//r} = $self->_serialize($_, $set->{$_}) for keys %$set;
lib/Math/Formula/Config/INI.pm view on Meta::CPAN
sub load($%)
{ my ($self, $name, %args) = @_;
my $fn = $self->path_for($args{filename} || "$name.ini");
my $read = Config::INI::Reader->read_file($fn);
my $attrs = $self->_set_decode($read->{_});
Math::Formula::Context->new(name => $name,
%$attrs,
formulas => $self->_set_decode($read->{formulas}),
);
}
sub _set_decode($)
{ my ($self, $set) = @_;
$set or return {};
my %forms;
$forms{$_} = $self->_unpack($_, $set->{$_}) for keys %$set;
\%forms;
lib/Math/Formula/Config/INI.pod view on Meta::CPAN
=encoding utf8
=head1 NAME
Math::Formula::Config::INI - load/save formulas to file as INI
=head1 INHERITANCE
Math::Formula::Config::INI
is a Math::Formula::Config
=head1 SYNOPSIS
my $context = Math::Formula::Content->new(name => 'test');
my $config = Math::Formula::Config::INI->new(directory => $dir);
lib/Math/Formula/Config/INI.pod view on Meta::CPAN
Only the quoted data may contain attributes.
B<. Example>
name = test
version = 1
created = "2023-02-26T20:07:54+0000"
updated = "2023-02-26T20:07:54+0000"
mf_version = 0
[formulas]
expr2 = "\"abc\".size + 3k"; returns='MF::INTEGER'
fakes = "false"
dinertime = "18:05:07"
expr1 = "1 + 2 * 3"
string = abc
some_truth = "true"
=head1 SEE ALSO
This module is part of Math-Formula distribution version 0.16,
lib/Math/Formula/Config/JSON.pm view on Meta::CPAN
# Pod stripped from pm file by OODoc 2.03.
package Math::Formula::Config::JSON;
use vars '$VERSION';
$VERSION = '0.16';
use base 'Math::Formula::Config';
use warnings;
use strict;
use Log::Report 'math-formula';
use Scalar::Util 'blessed';
use File::Slurper 'read_binary';
use Cpanel::JSON::XS ();
my $json = Cpanel::JSON::XS->new->pretty->utf8->canonical(1);
#----------------------
sub save($%)
{ my ($self, $context, %args) = @_;
my $name = $context->name;
my $index = $context->_index;
my $tree = $self->_set($index->{attributes});
$tree->{formulas} = $self->_set($index->{formulas});
my $fn = $self->path_for($args{filename} || "$name.json");
open my $fh, '>:raw', $fn
or fault __x"Trying to save context '{name}' to {fn}", name => $name, fn => $fn;
$fh->print($json->encode($tree));
$fh->close
or fault __x"Error on close while saving '{name}' to {fn}", name => $name, fn => $fn;
}
lib/Math/Formula/Config/JSON.pm view on Meta::CPAN
return $v;
}
sub load($%)
{ my ($self, $name, %args) = @_;
my $fn = $self->path_for($args{filename} || "$name.json");
my $tree = $json->decode(read_binary $fn);
my $formulas = delete $tree->{formulas};
my $attrs = $self->_set_decode($tree);
Math::Formula::Context->new(name => $name,
%$attrs,
formulas => $self->_set_decode($formulas),
);
}
sub _set_decode($)
{ my ($self, $set) = @_;
$set or return {};
my %forms;
$forms{$_} = $self->_unpack($_, $set->{$_}) for keys %$set;
\%forms;
lib/Math/Formula/Config/JSON.pod view on Meta::CPAN
=encoding utf8
=head1 NAME
Math::Formula::Config::JSON - load/save formulas to file
=head1 INHERITANCE
Math::Formula::Config::JSON
is a Math::Formula::Config
=head1 SYNOPSIS
my $context = Math::Formula::Content->new(name => 'test');
my $config = Math::Formula::Config::JSON->new(directory => $dir);
lib/Math/Formula/Config/JSON.pod view on Meta::CPAN
=head1 DETAILS
JSON seems to be everyone's favorite serialization syntax, nowadays. It natively
supports integers, floats, booleans, and strings. Formulas get a leading '='
(not yet configurable).
B<. Example>
{
"created" : "2023-02-28T16:30:27+0000",
"formulas" : {
"expr1" : "=1 + 2 * 3",
"expr2" : "=\"abc\".size + 3k; returns='MF::INTEGER'",
"fakes" : false,
"float" : 3.14,
"int" : 42,
"longer" : "abc def yes no",
"no_quotes" : "abc",
"some_truth" : true,
"string" : "true"
},
lib/Math/Formula/Config/YAML.pm view on Meta::CPAN
# Pod stripped from pm file by OODoc 2.03.
package Math::Formula::Config::YAML;
use vars '$VERSION';
$VERSION = '0.16';
use base 'Math::Formula::Config';
use warnings;
use strict;
use Log::Report 'math-formula';
use YAML::XS qw/Dump Load/;
use boolean ();
use File::Slurper 'read_binary';
# It is not possible to use YAML.pm, because it cannot produce output where
# boolean true and a string with content 'true' can be distinguished.
use Scalar::Util 'blessed';
lib/Math/Formula/Config/YAML.pm view on Meta::CPAN
my $name = $context->name;
local $YAML::XS::Boolean = "boolean";
my $index = $context->_index;
my $fn = $self->path_for($args{filename} || "$name.yml");
open my $fh, '>:encoding(utf8)', $fn
or fault __x"Trying to save context '{name}' to {fn}", name => $name, fn => $fn;
$fh->print(Dump $self->_set($index->{attributes}));
$fh->print(Dump $self->_set($index->{formulas}));
$fh->print(Dump $self->_set($index->{fragments}));
$fh->close
or fault __x"Error on close while saving '{name}' to {fn}", name => $name, fn => $fn;
}
sub _set($)
{ my ($self, $set) = @_;
my %data;
$data{$_ =~ s/^ctx_//r} = $self->_serialize($_, $set->{$_}) for keys %$set;
lib/Math/Formula/Config/YAML.pm view on Meta::CPAN
sub load($%)
{ my ($self, $name, %args) = @_;
my $fn = $self->path_for($args{filename} || "$name.yml");
local $YAML::XS::Boolean = "boolean";
my ($attributes, $forms, $frags) = Load(read_binary $fn);
my $attrs = $self->_set_decode($attributes);
Math::Formula::Context->new(name => $name,
%$attrs,
formulas => $self->_set_decode($forms),
);
}
sub _set_decode($)
{ my ($self, $set) = @_;
$set or return {};
my %forms;
$forms{$_} = $self->_unpack($_, $set->{$_}) for keys %$set;
\%forms;
lib/Math/Formula/Config/YAML.pod view on Meta::CPAN
=encoding utf8
=head1 NAME
Math::Formula::Config::YAML - load/save formulas to file in YAML
=head1 INHERITANCE
Math::Formula::Config::YAML
is a Math::Formula::Config
=head1 SYNOPSIS
my $context = Math::Formula::Content->new(name => 'test');
my $config = Math::Formula::Config::YAML->new(directory => $dir);
$config->save($context);
my $context = $config->load('test');
=head1 DESCRIPTION
Write a Context to file, and read it back again.
The attributes, formulas, and fragments are written as three separate documents.
You need to have installed B<YAML::XS>, minimal version 0.81 (for security reasons)
and module C<boolean.pm>. They are not in the dependencies of this packages, because
we do not want to add complications to the main code.
Extends L<"DESCRIPTION" in Math::Formula::Config|Math::Formula::Config/"DESCRIPTION">.
=head1 METHODS
Extends L<"METHODS" in Math::Formula::Config|Math::Formula::Config/"METHODS">.
lib/Math/Formula/Config/YAML.pod view on Meta::CPAN
=back
=back
=head1 DETAILS
YAML has a super powerful syntax, which natively supports integers,
floats, booleans, and strings. But it can do so much more! (What we
are not gonna use (yet))
The Context's attributes are in the first document. The formulas are
in the second document. The fragments will get a place in the third
document (but are not yet supported).
On Perl, you will need YAML::XS to be able to treat booleans
correctly. For instance, C<YAML.pm> will create a string with content
'true' without quotes... which makes it a boolean.
B<. Example>
---
lib/Math/Formula/Context.pm view on Meta::CPAN
# Pod stripped from pm file by OODoc 2.03.
package Math::Formula::Context;
use vars '$VERSION';
$VERSION = '0.16';
use warnings;
use strict;
use Log::Report 'math-formula';
use Scalar::Util qw/blessed/;
sub new(%) { my $class = shift; (bless {}, $class)->init({@_}) }
sub _default($$$$)
{ my ($self, $name, $type, $value, $default) = @_;
my $form
= ! $value ? $type->new(undef, $default)
: ! blessed $value ? ($value ? Math::Formula->new($name, $value) : undef)
lib/Math/Formula/Context.pm view on Meta::CPAN
ctx_name => $node,
ctx_version => $self->_default(version => 'MF::STRING', $args->{version}, "1.00"),
ctx_created => $self->_default(created => 'MF::DATETIME', $args->{created}, $now = DateTime->now),
ctx_updated => $self->_default(updated => 'MF::DATETIME', $args->{updated}, $now //= DateTime->now),
ctx_mf_version => $self->_default(mf_version => 'MF::STRING', $args->{mf_version}, $Math::Formula::VERSION),
};
$self->{MFC_lead} = $args->{lead_expressions} // '';
$self->{MFC_forms} = { };
$self->{MFC_frags} = { };
if(my $forms = $args->{formulas})
{ $self->add(ref $forms eq 'ARRAY' ? @$forms : $forms);
}
$self->{MFC_claims} = { };
$self->{MFC_capts} = [ ];
$self;
}
# For save()
sub _index()
{ my $self = shift;
+{ attributes => $self->{MFC_attrs},
formulas => $self->{MFC_forms},
fragments => $self->{MFC_frags},
};
}
#--------------
sub name { $_[0]->{MFC_name} }
sub lead_expressions { $_[0]->{MFC_lead} }
#--------------
lib/Math/Formula/Context.pm view on Meta::CPAN
{ if(ref $obj eq 'HASH')
{ $self->add($_, $obj->{$_}) for keys %$obj;
}
elsif(blessed $obj && $obj->isa('Math::Formula'))
{ $self->{MFC_forms}{$obj->name} = $obj;
}
elsif(blessed $obj && $obj->isa('Math::Formula::Context'))
{ $self->{MFC_frags}{$obj->name} = $obj;
}
else
{ panic __x"formula add '{what}' not understood", what => $obj;
}
}
undef;
}
sub addFormula(@)
{ my ($self, $name) = (shift, shift);
my $next = $_[0];
lib/Math/Formula/Context.pm view on Meta::CPAN
}
if(length(my $leader = $self->lead_expressions))
{ my $typed = $data =~ s/^\Q$leader// ? $data : \$data;
return $forms->{$name} = Math::Formula->new($name, $typed, %attrs);
}
return $forms->{$name} = Math::Formula->new($name, $data, %attrs);
}
error __x"formula declaration '{name}' not understood", name => $name;
}
sub formula($) { $_[0]->{MFC_forms}{$_[1]} }
sub addFragment($;$)
{ my $self = shift;
my ($name, $fragment) = @_==2 ? @_ : ($_[0]->name, $_[0]);
$self->{MFC_frags}{$name} = MF::FRAGMENT->new($name, $fragment);
}
sub fragment($) { $_[0]->{MFC_frags}{$_[1]} }
#-------------------
sub evaluate($$%)
{ my ($self, $name) = (shift, shift);
# Wow, I am impressed! Caused by prefix(#,.) -> infix
length $name or return $self;
my $form = $name =~ /^ctx_/ ? $self->attribute($name) : $self->formula($name);
unless($form)
{ warning __x"no formula '{name}' in {context}", name => $name, context => $self->name;
return undef;
}
my $claims = $self->{MFC_claims};
! $claims->{$name}++
or error __x"recursion in expression '{name}' at {context}",
name => $name, context => $self->name;
my $result = $form->evaluate($self, @_);
lib/Math/Formula/Context.pod view on Meta::CPAN
=head2 Constructors
=over 4
=item Math::Formula::Context-E<gt>B<new>(%options)
Many of the %options make sense when this context is reloaded for file.
-Option --Default
formula []
lead_expressions ''
=over 2
=item formula => $form|ARRAY
One or more formula, passed to L<add()|Math::Formula::Context/"Formula and Fragment management">.
=item lead_expressions => ''|STRING
Read section L</"Keep strings apart from expressions"> below. When a blank string,
you will need to put (single or double) quotes around your strings within your strings,
or pass a SCALAR reference. But that may be changed.
=back
=back
=head2 Attributes
=over 4
=item $obj-E<gt>B<lead_expressions>()
Returns the string which needs to be prepended to each of the formulas
which are added to the context. When an empty string (the default),
formulas have no identification which distinguishes them from string. In that case,
be sure to pass strings as references.
=item $obj-E<gt>B<name>()
Contexts are required to have a name. Usually, this is the name of the fragment as
well.
=back
=head2 Fragment (this context) attributes
Basic data types usually have attributes (string C<length>), which operator on the type
to produce some fact. The fragment type (which manages Context objects), however,
cannot distinguish between attributes and formula names: both use the dot (C<.>)
operator. Therefore, all context attributes will start with C<ctx_>.
The following attributes are currently defined:
ctx_name MF::STRING same as $context->name
ctx_version MF::STRING optional version of the context data
ctx_created MF::DATETIME initial creation of this context data
ctx_updated MF::DATETIME last save of this context data
ctx_mf_version MF::STRING Math::Formula version, useful for read/save
lib/Math/Formula/Context.pod view on Meta::CPAN
=head2 Formula and Fragment management
=over 4
=item $obj-E<gt>B<add>(LIST)
Add one or more items to the context.
When a LIST is used and the first argument is a name, then the data is
used to create a $formula or fragment (when the name starts with a '#').
Otherwise, the LIST is a sequence of prepared formulas and fragments,
or a HASH with
example: :
$context->add(wakeup => '07:00:00', returns => 'MF::TIME');
$context->add(fruit => \'apple', returns => 'MF::TIME');
my $form = Math::Formula->new(wakeup => '07:00:00', returns => 'MF::TIME');
$context->add($form, @more_forms, @fragments, @hashes);
my %library = (
breakfast => 'wakeup + P2H',
to_work => 'PT10M', # mind the 'T': minutes not months
work => [ 'breakfast + to_work', returns => 'MF::TIME' ],
#filesys => $fragment,
);
$context->add($form, \%library, $frag);
=item $obj-E<gt>B<addFormula>(LIST)
Add a single formula to this context. The formula is returned.
example: of addFormula
Only the 3rd and 4th line of the examples below are affected by C<new(lead_expressions)>:
only in those cases it is unclear whether we speak about a STRING or an expression. But,
inconveniently, those are popular choices.
$context->addFormula($form); # already created somewhere else
$context->addFormula(wakeup => $form); # register under a (different) name
$context->addFormula(wakeup => '07:00:00'); # influenced by lead_expressions
lib/Math/Formula/Context.pod view on Meta::CPAN
$context->addFormula(wakeup => [ '07:00:00', returns => 'MF::TIME' ]);
$context->addFormula(wakeup => sub { '07:00:00' }, returns => 'MF::TIME' ]);
$context->addFormula(wakeup => MF::TIME->new('07:00:00'));
$context->addFormula(wakeup => \'early');
=item $obj-E<gt>B<addFragment>( [$name], $fragment )
A $fragment is simply a different Context. Fragments are addressed via the '#'
operator.
=item $obj-E<gt>B<formula>($name)
Returns the formula with this specified name.
=item $obj-E<gt>B<fragment>($name)
Returns the fragment (context) with $name. This is not sufficient to switch
between contexts, which is done during execution.
=back
=head2 Runtime
lib/Math/Formula/Context.pod view on Meta::CPAN
expr_field => '= 1 + 2 * 3'
Of course, this introduces the security risk in the C<$string> case, which might
carry a C<=> by accident. So: although usable, refrain from using that form
unless you are really, really sure this can never be confused.
Other suggestions for C<lead_expressions> are C<+> or C<expr: >. Any constant string
will do.
I<The third solution> for this problem, is that your application exactly knows
which fields are formula, and which fields are plain strings. For instance, my
own application is XML based. I have defined
<let name="some-field1" string="string content" />
<let name="some-field2" be="expression content" />
=head2 Creating an interface to an object (fragment)
For safety reasons, the formulas can not directly call methods on data
objects, but need to use a well defined interface which hides the internals
of your program. Some (Perl) people call this "inside-out objects".
With introspection, it would be quite simple to offer access to, for instance,
a DateTime object which implements the DATETIME logic. This would, however,
open a pit full of security and compatibility worms. So: the DATETIME object
will only offer a small set of B<attributes>, which produce results also
provided by other time computing libraries.
The way to create an interface looks: (first the long version)
lib/Math/Formula/Context.pod view on Meta::CPAN
$context->add($expr);
$context->value($expr); # simpler
Of course, there are various simplifications possible, when the calculations
are not too complex:
my ($dir, $filename) = (..., ...);
my $fragment = Math::Formula::Context->new(
name => 'file',
formulas => {
name => \$filename,
path => sub { \File::Spec->catfile($dir, $filename) },
is_image => 'name like "*.{jpg,png,gif}"',
Ï => MF::FLOAT->new(undef, 3.14), # constant
# $_[0] = $context
size => sub { -s $_[0]->value('path') },
});
$context->addFragment($fragment);
$context->addFormula(allocate => '#file.size * 10k');
print $context->value('#file.allocate');
In above example, the return type of the CODE for C<size> is explicit: this is
the fastest and safest way to return data. However, it can also be guessed:
size => sub { -s $filename },
For clarity: the three syntaxes:
.ctx_name an attribute to the context
allocate a formula in the context
allocate.abs an attribute of the expression result
#file interface to an object, registered in the context
#file.size an attribute to an object
#filesys.file(name).size file(name) produces an object
=head2 Aliasing
It is possible to produce an alias formula to hide or simplify the fragment.
This also works for formulas and attributes!
fs => '#filesys' # alias fragment
dt => '#system#datetime' # alias nested fragments
size => '"abc".size' # alias attribute
now => 'dt.now' # alias formula
=head2 CODE as expression
It should be the common practice to use strings as expressions. Those strings get
tokenized and evaluated. However, when you need calculations which are not offered
by this module, or need connections to objects (see fragments in L<Math::Formula::Context|Math::Formula::Context>),
then you will need CODE references as expression.
The CODE reference returns either an B<explicit type> or a guessed type.
When the type is explicit, you MUST decide whether the data is a "token"
lib/Math/Formula/Token.pm view on Meta::CPAN
sub level { $_[0][1] }
#-------------------
# MF::OPERATOR, operator of yet unknown type.
# In the AST upgraded to either MF::PREFIX or MF::INFIX.
package
MF::OPERATOR;
use base 'Math::Formula::Token';
use Log::Report 'math-formula', import => [ 'panic' ];
use constant {
# Associativity
LTR => 1, RTL => 2, NOCHAIN => 3,
};
# method operator(): Returns the operator value in this token, which
# "accidentally" is the same value as the M<token()> method produces.
sub operator() { $_[0][0] }
lib/Math/Formula/Type.pm view on Meta::CPAN
package Math::Formula::Type;
use vars '$VERSION';
$VERSION = '0.16';
use base 'Math::Formula::Token';
#!!! The declarations of all other packages in this file are indented to avoid
#!!! indexing by CPAN.
use Log::Report 'math-formula', import => [ qw/warning error __x/ ];
# Object is an ARRAY. The first element is the token, as read from the formula
# or constructed from a computed value. The second is a value, which can be
# used in computation. More elements are type specific.
#-----------------
sub cast($)
{ my ($self, $to, $context) = @_;
return MF::STRING->new(undef, $self->token)
lib/Math/Formula/Type.pm view on Meta::CPAN
);
sub attribute($) { $string_attrs{$_[1]} || $_[0]->SUPER::attribute($_[1]) }
#-----------------
package
MF::INTEGER;
use base 'Math::Formula::Type';
use Log::Report 'math-formula', import => [ qw/error __x/ ];
sub cast($)
{ my ($self, $to) = @_;
$to eq 'MF::BOOLEAN' ? MF::BOOLEAN->new(undef, $_[0]->value == 0 ? 0 : 1)
: $to eq 'MF::FLOAT' ? MF::FLOAT->new(undef, $_[0]->value)
: $self->SUPER::cast($to);
}
sub prefix($)
{ my ($self, $op, $context) = @_;
lib/Math/Formula/Type.pm view on Meta::CPAN
: $op eq '/' ? $self->value / $right->value
: undef;
return MF::FLOAT->new(undef, $v) if defined $v;
return MF::INTEGER->new(undef, $self->value <=> $right->value)
if $op eq '<=>';
}
$self->SUPER::infix(@_);
}
# I really do not want a math library in here! Use formulas with CODE expr
# my %float_attrs;
#sub attribute($) { $float_attrs{$_[1]} || $_[0]->SUPER::attribute($_[1]) }
#-----------------
package
MF::DATETIME;
use base 'Math::Formula::Type';
lib/Math/Formula/Type.pm view on Meta::CPAN
{ $dt_attrs{$_[1]} || $MF::DATE::date_attrs{$_[1]} || $_[0]->SUPER::attribute($_[1]);
}
#-----------------
package
MF::DATE;
use base 'Math::Formula::Type';
use Log::Report 'math-formula', import => [ qw/error warning __x/ ];
use DateTime::TimeZone ();
use DateTime::TimeZone::OffsetOnly ();
sub _match { '[12][0-9]{3} \- (?:0[1-9]|1[012]) \- (?:0[1-9]|[12][0-9]|3[01]) (?:[+-][0-9]{4})?' }
sub _token($) { $_[1]->ymd . ($_[1]->time_zone->name =~ s/UTC$/+0000/r) }
sub _value($)
{ my ($self, $token) = @_;
lib/Math/Formula/Type.pm view on Meta::CPAN
);
sub attribute($) { $dur_attrs{$_[1]} || $_[0]->SUPER::attribute($_[1]) }
#-----------------
package
MF::NAME;
use base 'Math::Formula::Type';
use Log::Report 'math-formula', import => [ qw/error __x/ ];
my $pattern = '[_\p{Alpha}][_\p{AlNum}]*';
sub _match() { $pattern }
sub value($) { error __x"name '{name}' cannot be used as value.", name => $_[0]->token }
sub validated($$)
{ my ($class, $name, $where) = @_;
lib/Math/Formula/Type.pm view on Meta::CPAN
{ my $frag = $self->token eq '' ? $context : $context->fragment($self->token);
return MF::FRAGMENT->new($frag->name, $frag) if $frag;
}
$context->evaluate($self->token, expect => $type);
}
sub prefix($$)
{ my ($self, $op, $context) = @_;
return MF::BOOLEAN->new(undef, defined $context->formula($self->token))
if $op eq 'exists';
$self->SUPER::prefix($op, $context);
}
sub infix(@)
{ my $self = shift;
my ($op, $right, $context) = @_;
my $name = $self->token;
lib/Math/Formula/Type.pm view on Meta::CPAN
{ my $left = $name eq '' ? MF::FRAGMENT->new($context->name, $context) : $context->evaluate($name);
return $left->infix(@_) if $left;
}
if($op eq '#')
{ my $left = $name eq '' ? MF::FRAGMENT->new($context->name, $context) : $context->fragment($name);
return $left->infix(@_) if $left;
}
if($op eq '//')
{ return defined $context->formula($name) ? $context->evaluate($name) : $right->compute($context);
}
my $left = $context->evaluate($name);
$left ? $left->infix($op, $right, $context): undef;
}
#-----------------
package
MF::PATTERN;
use base 'MF::STRING';
use Log::Report 'math-formula', import => [ qw/warning __x/ ];
sub _token($) {
warning __x"cannot convert qr back to pattern, do {regexp}", regexp => $_[1];
"pattern meaning $_[1]";
}
sub _from_string($)
{ my ($class, $string) = @_;
$string->token; # be sure the pattern is kept as token: cannot be recovered
bless $string, $class;
lib/Math/Formula/Type.pm view on Meta::CPAN
my $value = $self->value =~ s!/!\\/!gr;
$self->[2] = qr/$value/xu;
}
#-----------------
package
MF::FRAGMENT;
use base 'Math::Formula::Type';
use Log::Report 'math-formula', import => [ qw/panic error __x/ ];
sub name { $_[0][0] }
sub context { $_[0][1] }
sub infix($$@)
{ my $self = shift;
my ($op, $right, $context) = @_;
my $name = $right->token;
if($op eq '#' && $right->isa('MF::NAME'))
lib/Math/Formula/Type.pod view on Meta::CPAN
=head2 MF::TIME, a moment during any day
Useful to indicate a daily repeating event. For instance, C<start-backup: 04:00:12>.
Times do no have a timezone: it only gets a meaning when added to a (local) time.
Time supports numeric comparison. When you add (C<+>) a (short) duration to a time, it
will result in a new time (modulo 24 hours).
When you subtract (C<->) one time from another, you will get a duration modulo 24 hours.
This does not take possible jumps to and from Daylight Savings time into
account. When you care about that, than create a formula involving
the actual date:
bedtime = 23:00:00
wakeup = 06:30:00
now = #system.now
sleep = ((now+P1D).date + wakeup) - (now.date + bedtime) #DURATION
B<. Example: for time>
12:00:34 # lunch-time, in context default time-zone
lib/Math/Formula/Type.pod view on Meta::CPAN
operator.
A name which is not right of a hash (C<#>) or dot (C<.>) can be cast
into an object from the context.
Names are symbol: are not a value by themselves, so have no values to
be ordered. However, they may exist however: test it with prefix operator
C<exists>.
The more complicated concept is the 'defaults to' operator (C<//>).
When there is no formula with the name on the left side, the right side
is taken. This is often stacked with a constant default value at the end.
B<. Example: of names>
tic
route66
the_boss
_42 # and '_' works as a character
αβΩ # Unicode alpha nums allowed
7eleven # not allowed: no start with number
See "Math::Formula::Context" for the following
#frag # (prefix #) fragment of default object
.method # (prefix .) method on default object
name#frag # fragment of object 'name'
file.size # method 'size' on object 'file'
Attributes on names
exists live => BOOLEAN # does formula 'live' exist?
not exists live => BOOLEAN
live // dead // false # pick first rule which exists
=over 4
=item Math::Formula::Type-E<gt>B<validated>($string, $where)
Create a MF::NAME from a $string which needs to be validated for being a valid
name. The $where will be used in error messages when the $string is invalid.
t/10formula.t view on Meta::CPAN
use Test::More;
use DateTime ();
use DateTime::Duration ();
use Math::Formula ();
use Math::Formula::Context ();
### expression as string
my $expr1 = Math::Formula->new(test1 => 1);
ok defined $expr1, 'created normal formula';
is $expr1->name, 'test1';
is $expr1->expression, '1';
my $answer1 = $expr1->evaluate;
ok defined $answer1, '... got answer';
isa_ok $answer1, 'Math::Formula::Type', '...';
cmp_ok $answer1->value, '==', 1;
### expression as code
my $expr2 = Math::Formula->new(test2 => sub { MF::INTEGER->new(2) });
ok defined $expr2, 'created formula from CODE';
is $expr2->name, 'test2';
isa_ok $expr2->expression, 'CODE';
my $answer2 = $expr2->evaluate;
ok defined $answer2, '... got answer';
isa_ok $answer2, 'Math::Formula::Type', '...';
cmp_ok $answer2->value, '==', 2;
### Return a node
my $expr3 = Math::Formula->new(Ï => MF::FLOAT->new(undef, 3.14));
ok defined $expr3, 'formula with node';
my $answer3 = $expr3->evaluate;
ok defined $answer3, '... answer';
isa_ok $answer3, 'MF::FLOAT', '...';
is $answer3->token, '3.14';
done_testing;
t/27names.t view on Meta::CPAN
if($] =~ m/^5\.2[01]/)
{ diag "utf8 names are broken in 5.20"; # regexp issue: get double encoded in $+
}
else
{ is_deeply $expr->_tokenize('ÐеленÑÑкий'), [ MF::NAME->new('ÐеленÑÑкий') ];
}
is_deeply $expr->_tokenize('tic tac toe'), [MF::NAME->new('tic'), MF::NAME->new('tac'), MF::NAME->new('toe')];
my $context = Math::Formula::Context->new(name => 'test',
formulas => { live => '42' },
);
ok defined $context, 'Testing existence';
is $context->value('live'), 42, '... live';
is $context->run('exists live')->token, 'true';
is $context->run('not exists live')->token, 'false';
is $context->run('exists green_man')->token, 'false', '... green man';
is $context->run('not exists green_man')->token, 'true';
is $context->run('live // green_man')->value, 42, 'default, not needed';
t/30fragment.t view on Meta::CPAN
### Simplest form
{ package
A; # help PAUSE
sub new { bless {}, shift }
sub toe { MF::INTEGER->new(42) }
}
my $the_real_thing = A->new;
ok $context->addFormula(tac => sub { $the_real_thing->toe }), 'add formula';
my $tac = $context->formula('tac');
ok defined $tac, '... found attr back';
isa_ok $tac, 'Math::Formula';
my $res1 = $tac->evaluate;
isa_ok $res1, 'MF::INTEGER', '... result';
is $res1->value, 42, 'Yeh!!';
### NESTED CONTEXTS (finally!)
my $system = Math::Formula::Context->new(name => 'system');
$system->addFormula(os => '"linux"');
$context->addFragment($system);
is $context->value('.ctx_name'), 'test', 'context attribute';
is $context->value('ctx_name'), 'test';
is $context->value('#system.ctx_name'), 'system', 'system attribute';
is $context->value("#system.os"), 'linux', 'system formula';
ok $context->addFormula(system2 => '#system'), 'shortcut for fragment';
is $context->value("system2.os"), 'linux', 'system formula';
ok $context->addFormula(os2 => '#system.os'), 'form in fragment';
is $context->value("os2 ~ ''"), 'linux', 'system formula';
is $context->value("os2"), 'linux';
ok $context->addFormula(os3 => 'system2.os'), 'form in aliased fragment';
is $context->value("os3 ~ ''"), 'linux', 'system formula';
is $context->value("os3"), 'linux';
ok $context->addFormula(size => '"abc".length'), 'aliased attribute';
is $context->value("size + 0"), '3', 'system formula';
is $context->value("size"), '3';
done_testing;
use utf8;
use Math::Formula ();
use Test::More;
use_ok 'Math::Formula::Context';
my $flag = 4;
sub own_code
{ my ($context, $formula, %other) = @_;
my $expect_flag = $other{flag};
ok defined $expect_flag, "call $expect_flag";
is $flag, $expect_flag, '... encosed';
isa_ok $context, 'Math::Formula::Context', '...';
isa_ok $formula, 'Math::Formula', '...';
my $int = MF::INTEGER->new(undef, $flag);
isa_ok $int, 'MF::INTEGER';
$int;
}
my $expr = Math::Formula->new(test => \&own_code);
ok defined $expr, 'created expression with code';
isa_ok $expr->expression, 'CODE';
t/70context.t view on Meta::CPAN
my $context = Math::Formula::Context->new(name => 'test');
ok defined $context, 'created a context';
isa_ok $context, 'Math::Formula::Context';
is $context->name, 'test';
my $name_attr = $context->attribute('ctx_name');
ok defined $name_attr, '... has a name';
isa_ok $name_attr, 'Math::Formula', '...';
is $name_attr->evaluate->value, 'test';
### Create formulas verbose
my $f1 = $context->addFormula(wakeup => '07:00:00', returns => 'MF::TIME');
ok defined $f1, 'created a first formula';
isa_ok $f1, 'Math::Formula', '...';
is $f1->name, 'wakeup';
is $f1->expression, '07:00:00';
my $f1b = $context->formula('wakeup');
ok defined $f1b, '... retrieved';
is $f1b->name, 'wakeup';
is $f1b->expression, '07:00:00';
my $f2 = $context->addFormula(gosleep => [ '23:30:00', returns => 'MF::TIME' ]);
ok defined $f2, 'formula with params as array';
isa_ok $f2, 'Math::Formula', '...';
is $f2->name, 'gosleep';
is_deeply $f2->expression, '23:30:00';
my $f2b = $context->formula('gosleep');
ok defined $f2b, '... retrieved';
is $f2b->name, 'gosleep';
is_deeply $f2b->expression, '23:30:00';
my $f3 = my $awake =
Math::Formula->new(awake => 'gosleep - wakeup', returns => 'MF::DURATION');
ok defined $f3, 'pass pre-created formula';
my $f3b = $context->addFormula($f3);
ok defined $f3b, '... add does return form';
is $f3b->name, 'awake';
my $f4 = $context->addFormula(renamed => $awake);
ok defined $f4, 'add form under a different name';
is $f4->name, 'awake';
my $f4b = $context->formula('renamed');
ok defined $f4b, '... retrieved';
is $f4b->name, 'awake';
### Now, create all in one go!
my $c2 = Math::Formula::Context->new(name => 'test');
$c2->add($awake, {
wakeup => '07:00:00',
gosleep => [ '23:30:00', returns => 'MF::TIME' ],
renamed => $awake,
});
ok $c2->formula('wakeup' )->name, 'wakeup';
ok $c2->formula('gosleep')->name, 'gosleep';
ok $c2->formula('awake' )->name, 'awake';
ok $c2->formula('renamed')->name, 'awake';
### Even nicer
my %rules = (
person => \'Larry',
wakeup => '07:00:00',
gosleep => [ '23:30:00', returns => 'MF::TIME' ],
renamed => $awake,
);
my $c3 = Math::Formula::Context->new(name => 'test',
formulas => [ $awake, \%rules ],
);
ok $c3->formula('wakeup' )->name, 'wakeup';
ok $c3->formula('gosleep')->name, 'gosleep';
ok $c3->formula('awake' )->name, 'awake';
ok $c3->formula('renamed')->name, 'awake';
### RUN without operators
my $wakeup = $c3->evaluate('wakeup');
ok defined $wakeup, 'evaluate wakeup';
isa_ok $wakeup, 'MF::TIME';
is $wakeup->token, '07:00:00';
### RUN with INFIX operators
my $run2 = $c3->evaluate('awake');
ok defined $run2, 'run with infix operator';
isa_ok $run2, 'MF::DURATION';
is $run2->token, 'PT16H30M0S';
### RUN with PREFIX operators
ok 1, 'test context in infix op';
$c3->add(asleep => 'PT24H + -awake');
is $c3->formula('asleep')->name, 'asleep', 'test context in prefix op';
# Got a string?
my $s4 = $c3->formula('person');
ok defined $s4, 'Got the string';
is $s4->name, 'person';
my $e4 = $s4->expression;
isa_ok $e4, 'MF::STRING';
my $n4 = $s4->evaluate;
isa_ok $n4, 'MF::STRING';
is $n4->token, '"Larry"';
$c1->add({
person => 'Larry',
awake => '=gosleep - wakeup',
wakeup => '=07:00:00',
gosleep => [ '23:30:00', returns => 'MF::TIME' ],
});
# Got a string?
my $s1 = $c1->formula('person');
ok defined $s1, 'Got the string';
is $s1->name, 'person';
my $e1 = $s1->expression;
isa_ok $e1, 'MF::STRING', '... is a STRING';
my $n1 = $s1->evaluate;
isa_ok $n1, 'MF::STRING', '... evals to STRING';
is $n1->token, '"Larry"', '... token with dquotes';