File Coverage

blib/lib/Math/Symbolic/Custom/ToShorterString.pm
Criterion Covered Total %
statement 96 98 97.9
branch 71 74 95.9
condition 58 69 84.0
subroutine 12 12 100.0
pod 0 1 0.0
total 237 254 93.3


line stmt bran cond sub pod time code
1             package Math::Symbolic::Custom::ToShorterString;
2              
3 2     2   332681 use 5.006;
  2         10  
4 2     2   13 use strict;
  2         5  
  2         113  
5 2     2   14 use warnings;
  2         2  
  2         168  
6 2     2   13 no warnings 'recursion';
  2         5  
  2         140  
7              
8             =pod
9              
10             =encoding utf8
11              
12             =head1 NAME
13              
14             Math::Symbolic::Custom::ToShorterString - Shorter string representations of Math::Symbolic trees
15              
16             =head1 VERSION
17              
18             Version 0.2
19              
20             =cut
21              
22             our $VERSION = '0.2';
23              
24 2     2   630 use Math::Symbolic qw(:all);
  2         172912  
  2         652  
25 2     2   25 use Math::Symbolic::Custom::Base;
  2         5  
  2         122  
26              
27 2     2   252 BEGIN {*import = \&Math::Symbolic::Custom::Base::aggregate_import}
28            
29             our $Aggregate_Export = [qw/to_shorter_infix_string/];
30              
31             # define ln in the parser as the natural logarithm
32             use Math::SymbolicX::ParserExtensionFactory (
33             ln => sub {
34 4         146246 my $arg = shift;
35 4         17 return Math::Symbolic::Operator->new('log', Math::Symbolic::Constant->euler(), $arg);
36             },
37 2     2   1652 );
  2         3290  
  2         33  
38              
39 2     2   74089 use Carp;
  2         6  
  2         3679  
40              
41             =pod
42              
43             =head1 SYNOPSIS
44              
45             use Math::Symbolic 0.613 qw(:all);
46             use Math::Symbolic::Custom::ToShorterString 0.2;
47              
48             # Note: ToShorterString v0.2 automatically adds ln(x) as an alias for log(e,x) in the parser
49             my $f = parse_from_string("1*2+3*4+5*sqrt(x+y+z)+ln(y)");
50              
51             # Try displaying with Math::Symbolic's to_string()
52             my $to_string = $f->to_string();
53             print "to_string():\t$to_string\n";
54             # to_string(): (((1 * 2) + (3 * 4)) + (5 * (((x + y) + z) ^ 0.5))) + (log(2.71828182845905, y))
55              
56             # Try displaying with ToShorterString
57             my $to_shorter_infix_string = $f->to_shorter_infix_string();
58             print "to_shorter_infix_string():\t$to_shorter_infix_string\n";
59             # to_shorter_infix_string(): ((1*2 + 3*4) + (5*sqrt(x + y + z))) + ln(y)
60              
61             # Check that the two output string representations parse to the same expression
62             my $f2 = parse_from_string($to_string);
63             my $f3 = parse_from_string($to_shorter_infix_string);
64              
65             if ( $f2->to_string() eq $f3->to_string() ) {
66             print "Parsed to same string\n";
67             }
68              
69             =head1 DESCRIPTION
70              
71             Provides C through the Math::Symbolic module extension class. "to_shorter_infix_string()" attempts to provide a string representation of a Math::Symbolic tree that is shorter and therefore more readable than the existing (infix) C method.
72              
73             The "to_string()" method wraps every branch in parentheses/brackets, which makes larger expressions difficult to read. "to_shorter_infix_string()" tries to determine whether parentheses are required and omits them. One of the goals of this module is that the output string should parse to a Math::Symbolic tree that is (at least numerically) equivalent to the original expression - even if the resulting Math::Symbolic tree might not be completely identical to the original (for that, use "to_string()"). Where appropriate, it produces strings containing the Math::Symbolic parser aliases C and C.
74              
75             From v0.2, the module uses L to automatically add C as an alias for C in the parser, and uses it for string output as well (in the same way as C and C).
76              
77             The "to_shorter_infix_string()" does not replace the "to_string()" method, it has to be called explicitly.
78              
79             =cut
80              
81             sub to_shorter_infix_string {
82 2350     2350 0 33988107 my ($t, $brackets_on) = @_;
83              
84 2350 100 100     6118 if ( ($t->term_type() == T_CONSTANT) || ($t->term_type() == T_VARIABLE) ) {
85 1159         7281 return $t->to_string();
86             }
87              
88 1191 100       10672 $brackets_on = 1 unless defined $brackets_on;
89              
90 1191 100       2761 if ( $brackets_on ) {
91            
92             # check if we can turn brackets off for the tree below
93 932 100 100     2759 if ( _is_all_operator($t, B_PRODUCT) || _is_all_operator($t, B_SUM) ) {
94 252         502 $brackets_on = 0;
95             }
96             # "expanded" for a simple expression essentially defined as no +/- below a * in the tree
97 932 100 100     7777 if ( _is_all_operator($t, [B_SUM, B_DIFFERENCE, B_PRODUCT]) && _is_expanded($t) ) {
98 342         615 $brackets_on = 0;
99             }
100             }
101              
102             # at this point the top of $t must be an operator
103 1191         3172 my $string = '';
104 1191         2964 my $op_info = $Math::Symbolic::Operator::Op_Types[$t->type()];
105 1191         7494 my $op_str = $op_info->{infix_string};
106              
107 1191 100       3467 if ( $t->arity() == 2 ) {
    50          
108              
109             # handle special cases
110             # prefix operator
111 982 100       6938 if ( not defined $op_str ) {
    100          
112              
113             # write ln(x) instead of log(e, x)
114 12 100 66     67 if ( ($op_info->{prefix_string} eq 'log') && ($t->op1()->term_type() == T_CONSTANT) && ($t->op1()->{special} eq 'euler') ) {
      100        
115              
116 1         16 $string .= "ln(" . to_shorter_infix_string($t->op2(), $brackets_on) . ")";
117             }
118             else {
119              
120 11         140 $string .= $op_info->{prefix_string} . "(";
121             $string .= join( ', ',
122 11         21 map { to_shorter_infix_string($_, $brackets_on) } @{ $t->{operands} } );
  22         116  
  11         24  
123 11         93 $string .= ')';
124             }
125             }
126             # 'sqrt' and 'exp' are in the parser, use them
127             elsif ( $t->type() == B_EXP ) {
128              
129 174 100 100     1272 if ( ($t->op2()->term_type() == T_CONSTANT) && ($t->op2()->value() == 0.5) ) {
    50 100        
    100 66        
      33        
      33        
      0        
      100        
130              
131 12         346 $string .= "sqrt(" . to_shorter_infix_string($t->op1(), $brackets_on) . ")";
132             }
133             elsif ( ($t->op2()->term_type() == T_OPERATOR) && ($t->op2()->type() == B_DIVISION) &&
134             ($t->op2()->op1()->term_type == T_CONSTANT) && ($t->op2()->op1()->value() == 1) &&
135             ($t->op2()->op2()->term_type == T_CONSTANT) && ($t->op2()->op2()->value() == 2) ) {
136            
137 0         0 $string .= "sqrt(" . to_shorter_infix_string($t->op1(), $brackets_on) . ")";
138             }
139             elsif ( ($t->op1()->term_type() == T_CONSTANT) && ($t->op1()->{special} eq 'euler') ) {
140              
141 54         1984 $string .= "exp(" . to_shorter_infix_string($t->op2(), $brackets_on) . ")";
142             }
143             }
144            
145 982 100       10620 if ( $string eq '' ) {
146             # various conditions for temporarily disabling brackets
147 904         2779 my @brackets = ($brackets_on, $brackets_on);
148              
149 904 100       2177 if ( $brackets_on ) {
150              
151 479         1199 foreach my $i (0,1) {
152            
153 958         4694 my $op = $t->{operands}[$i];
154            
155 958 100 100     2277 if ( ($op->term_type() == T_CONSTANT) || ($op->term_type() == T_VARIABLE) ) {
    50          
156             # it's a constant or a variable
157 371         2151 $brackets[$i] = 0;
158             }
159             elsif ( $op->term_type() == T_OPERATOR ) {
160             # it's going to be a prefix operator (e.g. sin)
161 587 100       6433 if ( !defined($Math::Symbolic::Operator::Op_Types[$op->type()]->{infix_string}) ) {
    100          
162 61         486 $brackets[$i] = 0;
163             }
164             # remove brackets around exponentiation operator ^
165             elsif ( $op->type() == B_EXP ) {
166 148         1925 $brackets[$i] = 0;
167             }
168             }
169             }
170             }
171              
172             # keep the spaces around * for readability when removing brackets around exponents
173 904 100 100     6447 $op_str = " $op_str " unless ($op_str eq '^') || ( ($op_str eq '*') && !(($t->op1()->term_type() == T_OPERATOR) && ($t->op1()->type() == B_EXP)) );
      100        
      100        
174            
175 904 100       6858 $string .=
    100          
    100          
    100          
176             ( $brackets[0] ? '(' : '' )
177             . to_shorter_infix_string($t->op1(), $brackets_on)
178             . ( $brackets[0] ? ')' : '' )
179             . $op_str
180             . ( $brackets[1] ? '(' : '' )
181             . to_shorter_infix_string($t->op2(), $brackets_on)
182             . ( $brackets[1] ? ')' : '' );
183             }
184             }
185             elsif ( $t->arity() == 1 ) {
186             # force brackets around the contents of prefix/function-style operators
187 209 100       2359 if ( not defined $op_str ) {
188 164         555 $string .= $op_info->{prefix_string} . "(" . to_shorter_infix_string($t->op1(), 1) . ")";
189             }
190             else {
191 45         143 my $is_op1 = $t->op1()->term_type() == T_OPERATOR;
192              
193 45 100       513 $string .= "$op_str"
    100          
194             . ( $is_op1 ? '(' : '' )
195             . to_shorter_infix_string($t->op1(), 1)
196             . ( $is_op1 ? ')' : '' );
197             }
198             }
199             else {
200 0         0 carp("Cannot proceed deeper with operator using unsupported number of arguments: " . $t->arity());
201             }
202              
203 1191         11655 return $string;
204             }
205              
206             # _is_all_operator
207             # returns 1 if the passed in tree $t is comprised entirely of the
208             # operator(s) specified in $op_type (excluding prefix-only operators)
209             sub _is_all_operator {
210 5679     5679   20665 my ($t, $op_type) = @_;
211            
212 5679 100 100     13298 return 1 if ($t->term_type() == T_CONSTANT) || ($t->term_type() == T_VARIABLE);
213              
214             # this will stop descent into e.g. sin, cos
215 4105         30387 my $op = $Math::Symbolic::Operator::Op_Types[$t->type()];
216 4105 100 66     32202 if ( defined($op->{prefix_string}) and not defined($op->{infix_string}) ) {
217 342         1134 return 1;
218             }
219            
220 3763 100       8705 if ( ref($op_type) eq "ARRAY" ) {
221 1721         2843 my @m = grep { $_ == $t->type() } @{$op_type};
  5163         24712  
  1721         3639  
222 1721 100       13696 return 0 if scalar(@m) == 0;
223             }
224             else {
225 2042 100       4198 return 0 if $t->type() != $op_type;
226             }
227            
228 1524         5084 my $ok = 1;
229 1524         2572 $ok &= _is_all_operator($_, $op_type) for @{$t->{operands}};
  1524         4867  
230 1524         9980 return $ok;
231             }
232              
233             # _is_expanded
234             # returns 1 if there are no +/- below a * in the tree.
235             # FIXME: Cannot really be run by itself - has to be restricted to the operators involved, i.e.:
236             # _is_all_operator($t, [B_SUM, B_DIFFERENCE, B_PRODUCT]) && _is_expanded($t)
237             sub _is_expanded {
238 1376     1376   5260 my ($t, $flag) = @_;
239            
240 1376 100       2892 $flag = 0 unless defined $flag;
241              
242 1376 100 100     2992 return 1 if ($t->term_type() == T_CONSTANT) || ($t->term_type() == T_VARIABLE);
243            
244 739         5603 my $op = $Math::Symbolic::Operator::Op_Types[$t->type()];
245 739 100 66     6166 if ( defined($op->{prefix_string}) and not defined($op->{infix_string}) ) {
246 181         567 return 1;
247             }
248            
249 558 100 100     1407 if ( $flag && (($t->type() == B_SUM) || ($t->type() == B_DIFFERENCE)) ) {
      100        
250 67         718 return 0;
251             }
252              
253 491 100 100     2023 if ( ($t->type() == B_PRODUCT) || ($t->type() == B_DIFFERENCE) ) {
254 314         2761 $flag = 1;
255             }
256              
257 491         4448 my $ok = 1;
258 491         851 $ok &= _is_expanded($_, $flag) for @{$t->{operands}};
  491         1549  
259 491         6352 return $ok;
260             }
261              
262             =pod
263              
264             =head1 SEE ALSO
265              
266             L
267              
268             =head1 AUTHOR
269              
270             Matt Johnson, C<< >>
271              
272             =head1 ACKNOWLEDGEMENTS
273              
274             Steffen Mueller, author of Math::Symbolic
275              
276             =head1 LICENSE AND COPYRIGHT
277              
278             This software is copyright (c) 2024 by Matt Johnson.
279              
280             This is free software; you can redistribute it and/or modify it under
281             the same terms as the Perl 5 programming language system itself.
282              
283             =cut
284              
285             1;
286             __END__