Math-Formula

 view release on metacpan or  search on metacpan

ChangeLog  view on Meta::CPAN

	- 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.

MANIFEST  view on Meta::CPAN

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;

t/63code.t  view on Meta::CPAN

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"';

t/71lead.t  view on Meta::CPAN


$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';



( run in 0.760 second using v1.01-cache-2.11-cpan-26ccb49234f )