File Coverage

blib/lib/Math/NumSeq/Expression.pm
Criterion Covered Total %
statement 75 167 44.9
branch 14 68 20.5
condition 1 5 20.0
subroutine 17 23 73.9
pod 4 4 100.0
total 111 267 41.5


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