File Coverage

blib/lib/Math/NumSeq/Expression.pm
Criterion Covered Total %
statement 75 167 44.9
branch 18 68 26.4
condition 1 5 20.0
subroutine 17 23 73.9
pod 4 4 100.0
total 115 267 43.0


line stmt bran cond sub pod time code
1             # Copyright 2010, 2011, 2012, 2013, 2014, 2016 Kevin Ryde
2              
3             # This file is part of Math-NumSeq.
4             #
5             # Math-NumSeq is free software; you can redistribute it and/or modify
6             # it under the terms of the GNU General Public License as published by the
7             # Free Software Foundation; either version 3, or (at your option) any later
8             # version.
9             #
10             # Math-NumSeq is distributed in the hope that it will be useful, but
11             # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12             # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
13             # for more details.
14             #
15             # You should have received a copy of the GNU General Public License along
16             # with Math-NumSeq. If not, see .
17              
18             package Math::NumSeq::Expression;
19 1     1   6384 use 5.004;
  1         3  
20 1     1   3 use strict;
  1         1  
  1         15  
21 1     1   4 use Carp;
  1         1  
  1         62  
22 1     1   4 use List::Util;
  1         1  
  1         45  
23 1     1   379 use Math::Libm;
  1         3941  
  1         53  
24 1     1   389 use Module::Util;
  1         2070  
  1         37  
25              
26 1     1   4 use vars '$VERSION', '@ISA';
  1         2  
  1         39  
27             $VERSION = 72;
28 1     1   351 use Math::NumSeq;
  1         1  
  1         102  
29             @ISA = ('Math::NumSeq');
30              
31             # uncomment this to run the ### lines
32             # use Smart::Comments;
33              
34              
35 0         0 BEGIN {
36 1     1   77 my ($have_MS, $have_MEE, $have_LE, @evaluators, @evaluators_display);
37             BEGIN {
38 1     1   3 $have_MS
39             = defined(Module::Util::find_installed('Math::Symbolic'));
40 1         358 $have_MEE
41             = defined(Module::Util::find_installed('Math::Expression::Evaluator'));
42              
43             # lower case Compiler::perl is the incompatible change in 0.24, required
44             # by the code here
45 1         222 $have_LE
46             = defined(Module::Util::find_installed('Language::Expr::Compiler::perl'));
47              
48             ### $have_MS
49             ### $have_MEE
50             ### $have_LE
51              
52 1 50       259 @evaluators = ('Perl',
    50          
    50          
53             ($have_MS ? 'MS' : ()),
54             ($have_MEE ? 'MEE' : ()),
55             ($have_LE ? 'LE' : ()));
56 1 50       2 @evaluators_display = (Math::NumSeq::__('Perl'),
    50          
    50          
57             ($have_MS ? Math::NumSeq::__('MS') : ()),
58             ($have_MEE ? Math::NumSeq::__('MEE') : ()),
59             ($have_LE ? Math::NumSeq::__('LE') : ()));
60             ### @evaluators
61             }
62              
63             # use constant name => Math::NumSeq::__('Arbitrary Expression');
64 1 50       2 use constant description =>
    50          
    50          
65             join ("\n",
66             Math::NumSeq::__('An arbitrary expression. It should be a function of \"i\" at 0,1,2, etc. For example (2*i)^2 would give the even perfect squares.
67              
68             Syntax is per the chosen evaluator, an invalid expression displays an error message.
69             Perl (the default) is either 2*i+1 or 2*$i+1.'),
70              
71             ($have_MS ?
72             Math::NumSeq::__('Math::Symbolic is like 2*i^2.')
73             : ()),
74              
75             ($have_MEE ?
76             Math::NumSeq::__('Math::Expression::Evaluator is like t=2*i;t^2')
77             : ()),
78              
79             ($have_LE ?
80             Math::NumSeq::__('Language::Expr is like $k**2 + $k - 1.')
81 1     1   4 : ()));
  1         1  
82              
83 1     1   4 use constant i_start => 0;
  1         1  
  1         78  
84 1         2 use constant parameter_info_array =>
85             [
86             { name => 'expression',
87             display => Math::NumSeq::__('Expression'),
88             type => 'string',
89             default => '3*i*i + i + 2',
90             width => 30,
91             description => Math::NumSeq::__('A mathematical expression giving values to display, for example x^2+x+41. Only one variable is allowed, see the chosen evaluator Math::Symbolic or Math::Expression::Evaluator for possible operators and function.'),
92             },
93             { name => 'expression_evaluator',
94             display => Math::NumSeq::__('Evaluator'),
95             type => 'enum',
96             default => $evaluators[0],
97             choices => \@evaluators,
98             choices_display => \@evaluators_display,
99             description => Math::NumSeq::__('The expression evaluator module, Perl for Perl itself, MS for Math::Symbolic, MEE for Math::Expression::Evaluator, LE for Language::Expr.'),
100             },
101 1     1   3 ];
  1         1  
102             }
103             ### parameter_info_array: parameter_info_array()
104             ### parameter_info_hash: __PACKAGE__->parameter_info_hash
105             ### evaluator default: __PACKAGE__->parameter_default('expression_evaluator')
106              
107             #------------------------------------------------------------------------------
108             my %oeis_anum;
109              
110             # some experimental A-number generators for easy expressions not with their
111             # own module
112              
113             # but A008865 starts from i=1
114             # $oeis_anum{'i*i-2'} = 'A008865';
115             # # OEIS-Catalogue: A008865 expression=i*i-2
116             #
117             # A162395 start i=1
118             # $oeis_anum{'i*i*(-1)**(i+1)'} = 'A162395';
119             # # OEIS-Catalogue: A162395 expression=i*i*(-1)**(i+1)
120              
121             $oeis_anum{'i*(i+2)'} = 'A005563';
122             # OEIS-Catalogue: A005563 expression=i*(i+2)
123              
124             $oeis_anum{'i*(4*i*i-1)/3'} = 'A000447'; # sum of odd squares
125             # OEIS-Catalogue: A000447 expression=i*(4*i*i-1)/3
126              
127             $oeis_anum{'(2*i)**3'} = 'A016743'; # even cubes (2i)^3
128             # OEIS-Catalogue: A016743 expression=(2*i)**3
129              
130             # FIXME: should promote to bigint when necessary
131             # cf A131577 zero and powers of 2
132             # A171449 powers of 2 with -1 instead of 1
133             $oeis_anum{'2**i'} = 'A000079'; # powers of 2
134             $oeis_anum{'3**i'} = 'A000244'; # powers of 3
135             $oeis_anum{'4**i'} = 'A000302'; # powers of 4
136             $oeis_anum{'10**i'} = 'A011557'; # powers of 10
137             # OEIS-Catalogue: A000079 expression=2**i
138             # OEIS-Catalogue: A000244 expression=3**i
139             # OEIS-Catalogue: A000302 expression=4**i
140             # OEIS-Catalogue: A011557 expression=10**i
141              
142             sub oeis_anum {
143 0     0 1 0 my ($self) = @_;
144             ### oeis_anum(): $self
145 0         0 return $oeis_anum{$self->{'expression'}};
146             }
147              
148             #------------------------------------------------------------------------------
149              
150              
151              
152              
153             {
154             package Math::NumSeq::Expression::LanguageExpr;
155 1     1   4 use List::Util 'min', 'max';
  1         1  
  1         72  
156 1     1   3 use vars '$pi', '$e', '$phi', '$gam';
  1         1  
  1         585  
157             $pi = Math::Libm::M_PI();
158             $e = Math::Libm::M_E();
159             $phi = (1+sqrt(5))/2;
160             $gam = 0.5772156649015328606065120;
161             }
162              
163             sub new {
164 2     2 1 1613 my ($class, %options) = @_;
165              
166 2         3 my $expression = $options{'expression'};
167 2 50       8 if (! defined $expression) {
168 0         0 $expression = $class->parameter_default('expression');
169             }
170              
171 2   33     4 my $evaluator = $options{'expression_evaluator'}
172             || $class->parameter_default('expression_evaluator')
173             || croak "No expression evaluator modules available";
174             ### $evaluator
175              
176 2         2 my $subr;
177 2 100       9 if ($evaluator eq 'Perl') {
    50          
    50          
    50          
178              
179             # Workaround: Something fishy in Safe 2.29 and perl 5.14.2 meant that
180             # after a Safe->new(), any subsequently loaded code dragging in %- named
181             # captures fails to load Tie::Hash::NamedCapture. Load it now, if it
182             # exists. This affects Language::Expr which uses Regexp::Grammars which
183             # has $-{'foo'}.
184             #
185             # Safe 2.30 has it fixed, so can skip there, unless or until want to
186             # depend outright on that version
187             # http://perl5.git.perl.org/perl.git/commitdiff/ad084f51cd17539ef55b510228156cd4f83c9729
188             #
189 1         12 eval { Safe->VERSION(2.30); 1 }
  0         0  
190 1 50       2 or eval { require Tie::Hash::NamedCapture };
  1         420  
191              
192 1         761 require Safe;
193 1         23111 my $safe = Safe->new;
194 1         628 $safe->permit('print',
195             ':base_math', # sqrt(), rand(), etc
196             );
197 1 50       8 if (eval { require List::Util; 1 }) {
  1         6  
  1         4  
198 1         4 $safe->share_from('List::Util', [ 'min','max' ]);
199             }
200 1         415 require POSIX;
201 1         3878 $safe->share_from('POSIX', [ 'floor','ceil' ]);
202 1         509 require Math::Trig;
203 1         8559 $safe->share_from('Math::Trig', [qw(tan
204             asin acos atan
205             csc cosec sec cot cotan
206             acsc acosec asec acot acotan
207             sinh cosh tanh
208             csch cosech sech coth cotanh
209             asinh acosh atanh
210             acsch acosech asech acoth acotanh
211             )]);
212 1         282 require Math::Libm;
213 1         5 $safe->share_from('Math::Libm', [qw(cbrt
214             erf
215             erfc
216             expm1
217             hypot
218             j0
219             j1
220             jn
221             lgamma_r
222             log10
223             log1p
224             pow
225             rint
226             y0
227             y1
228             yn)]);
229              
230 1         147 my $pi = Math::Libm::M_PI();
231 1         5 my $e = Math::Libm::M_E();
232 1         15 $subr = $safe->reval("\n#line ".(__LINE__+2)." \"".__FILE__."\"\n"
233             . <<"HERE");
234             my \$pi = $pi;
235             my \$e = $e;
236             my \$phi = (1+sqrt(5))/2;
237             my \$gam = 0.5772156649015328606065120;
238             my \$i;
239             sub i () { return \$i }
240             sub {
241             \$i = \$_[0];
242             return do { $expression }
243             }
244             HERE
245             ### $subr
246 1 50       760 if (! $subr) {
247 1         238 croak "Invalid or unsafe expression: $@\n";
248             }
249              
250             } elsif ($evaluator eq 'MS') {
251 0         0 require Math::Symbolic;
252 0         0 my $tree = Math::Symbolic->parse_from_string($expression);
253 0 0       0 if (! defined $tree) {
254 0         0 croak "Cannot parse MS expression: $expression";
255             }
256              
257             # simplify wrong result on x+(-5)*y before 0.605 ...
258 0 0       0 if (eval { $tree->VERSION(0.605); 1 }) {
  0         0  
  0         0  
259 0         0 $tree = $tree->simplify;
260             }
261              
262 0         0 my @vars = $tree->signature;
263 0 0       0 if (@vars > 1) {
264 0         0 croak "More than one variable in MS expression: $expression\n(simplified to $tree)";
265             }
266             ### code: $tree->to_code
267 0         0 ($subr) = $tree->to_sub(\@vars);
268             ### $subr
269              
270             } elsif ($evaluator eq 'MEE') {
271 0         0 require Math::Expression::Evaluator;
272 0         0 my $me = Math::Expression::Evaluator->new;
273 0         0 $me->set_function('min', \&List::Util::min);
274 0         0 $me->set_function('max', \&List::Util::max);
275 0         0 $me->parse('pi='.Math::Libm::M_PI()
276             .'; e='.Math::Libm::M_E()
277             .'; phi=(1+sqrt(5))/2'
278             .'; gam=0.5772156649015328606065120');
279 0         0 $me->val;
280              
281 0 0       0 eval { $me->parse ($expression); 1 }
  0         0  
  0         0  
282             or croak "Cannot parse MEE expression: $expression\n$@";
283              
284             # my @vars = $me->variables;
285 0         0 my @vars = _me_free_variables($me);
286 0 0       0 if (@vars > 1) {
287 0         0 croak "More than one variable in MEE expression: $expression";
288             }
289              
290 0         0 my $hashsub = $me->compiled;
291             ### $hashsub
292             ### _ast_to_perl: $me->_ast_to_perl($me->{ast})
293              
294 0         0 my $v = $vars[0];
295 0         0 my %vars;
296 0 0       0 if (@vars) {
297             $subr = sub {
298 0     0   0 $vars{$v} = $_[0];
299 0         0 return &$hashsub(\%vars);
300 0         0 };
301             } else {
302             ### no variables in expression ...
303             $subr = sub {
304 0     0   0 return &$hashsub(\%vars);
305 0         0 };
306             }
307              
308             } elsif ($evaluator eq 'LE') {
309 1         142 require Language::Expr;
310 0           my $le = Language::Expr->new;
311 0           my $var_enumer = $le->get_interpreter('var_enumer');
312 0           my $varef;
313 0 0         eval { $varef = $var_enumer->eval ($expression); 1 }
  0            
  0            
314             or croak "Cannot parse LE expression: $expression\n$@";
315             ### $varef
316             my @vars = grep { # only vars, not functions as such
317 0           do {
  0            
318 1     1   4 no strict;
  1         1  
  1         430  
319 0           ! defined ${"Math::NumSeq::Expression::LanguageExpr::$_"}
  0            
320             }
321             } @$varef;
322 0 0         if (@vars > 1) {
323 0           croak "More than one variable in LE expression: $expression";
324             }
325              
326 0           my $pc = $le->get_compiler('perl');
327 0           my $perlstr;
328 0 0         eval { $perlstr = $pc->compile ($expression); 1 }
  0            
  0            
329             or croak "Cannot parse LE expression: $expression\n$@";
330             ### $perlstr
331              
332 0   0       my $v = $vars[0] || 'i';
333             ### $v
334             ### eval: "sub { my \$$v = \$_[0]; $perlstr }"
335 0 0         $subr = eval "package Math::NumSeq::Expression::LanguageExpr;
336             use strict;
337             sub { my \$$v = \$_[0]; $perlstr }"
338             or croak "Cannot compile $expression\n$perlstr\n$@";
339             ### $subr
340             ### at zero: $subr->(0)
341              
342              
343              
344              
345              
346              
347             # require Language::Expr;
348             # my $le = Language::Expr->new;
349             # my $varef;
350             # eval { $varef = $le->enum_vars ($expression); 1 }
351             # or croak "Cannot parse LE expression: $expression\n$@";
352             # ### $varef
353             # my @vars = grep { # only vars, not functions as such
354             # do {
355             # no strict;
356             # ! defined ${"Math::NumSeq::Expression::LanguageExpr::$_"}
357             # }
358             # } @$varef;
359             # if (@vars > 1) {
360             # croak "More than one variable in LE expression: $expression";
361             # }
362             #
363             # require Language::Expr::Compiler::Perl;
364             # my $pe = Language::Expr::Compiler::Perl->new;
365             # my $perlstr;
366             # eval { $perlstr = $pe->perl ($expression); 1 }
367             # or croak "Cannot parse LE expression: $expression\n$@";
368             #
369             # my $v = $vars[0] || 'i';
370             # ### $v
371             # ### eval: "sub { my \$$v = \$_[0]; $perlstr }"
372             # $subr = eval "package Math::NumSeq::Expression::LanguageExpr;
373             # use strict;
374             # sub { my \$$v = \$_[0]; $perlstr }"
375             # or croak "Cannot compile $expression\n$perlstr\n$@";
376             # ### $subr
377             # ### at zero: $subr->(0)
378              
379             } else {
380 0           croak "Unknown evaluator: $evaluator";
381             }
382              
383 0           my $self = bless {
384             # hi => $options{'hi'},
385             subr => $subr,
386             expression => $expression, # for oeis_anum() and dumps
387             }, $class;
388 0           $self->rewind;
389 0           return $self;
390             }
391              
392             sub rewind {
393 0     0 1   my ($self) = @_;
394 0           $self->{'i'} = $self->i_start;
395 0           $self->{'above'} = 0;
396             }
397              
398             sub next {
399 0     0 1   my ($self) = @_;
400 0           my $i = $self->{'i'}++;
401              
402 0           for (;;) {
403 0 0         if ($self->{'above'} >= 10) { # || $i > $self->{'hi'}
404 0           return;
405             }
406 0           my $n = eval { $self->{'subr'}->($i) };
  0            
407 0 0         if (! defined $n) {
408             # eg. division by zero
409             ### expression undef: $@
410 0           $self->{'above'}++;
411 0           next;
412             }
413             ### expression result: $n
414             # if ($n > $self->{'hi'}) {
415             # $self->{'above'}++;
416             # }
417 0           return ($i, $n);
418             }
419             }
420              
421             #------------------------------------------------------------------------------
422             # Math::Expression::Evaluator helpers
423              
424             # $me is a Math::Expression::Evaluator
425             # return a list of the free variables in it
426             sub _me_free_variables {
427 0     0     my ($me) = @_;
428 0           my %assigned = %{$me->{'variables'}};
  0            
429 0           my %free;
430 0           my @pending = ($me->{'ast'});
431 0           while (@pending) {
432 0           my $node = shift @pending;
433 0 0         ref $node or next;
434             # ### $node
435 0           push @pending, @$node[1..$#$node];
436              
437 0 0         if ($node->[0] eq '$') {
    0          
438 0           my $varname = $node->[1];
439 0 0         if (! $assigned{$varname}) {
440             ### free: $varname
441 0           $free{$varname} = 1;
442             }
443             } elsif ($node->[0] eq '=') {
444 0           my $vnode = $node->[1];
445 0 0         if ($vnode->[0] eq '$') {
446             ### assigned: $vnode->[1]
447 0           $assigned{$vnode->[1]} = 1;
448             }
449             }
450             }
451 0           return keys %free;
452             }
453              
454              
455             1;
456             __END__