File Coverage

blib/lib/Math/Symbolic/Custom/ToTallString.pm
Criterion Covered Total %
statement 253 285 88.7
branch 97 112 86.6
condition 85 106 80.1
subroutine 14 14 100.0
pod 0 1 0.0
total 449 518 86.6


line stmt bran cond sub pod time code
1             package Math::Symbolic::Custom::ToTallString;
2              
3 2     2   404072 use 5.006;
  2         7  
4 2     2   12 use strict;
  2         4  
  2         70  
5 2     2   8 use warnings;
  2         4  
  2         151  
6 2     2   9 no warnings 'recursion';
  2         5  
  2         159  
7              
8             =pod
9              
10             =encoding utf8
11              
12             =head1 NAME
13              
14             Math::Symbolic::Custom::ToTallString - Pretty-print Math::Symbolic expressions
15              
16             =head1 VERSION
17              
18             Version 0.11
19              
20             =cut
21              
22             our $VERSION = '0.11';
23              
24 2     2   732 use Math::Symbolic qw(:all);
  2         176033  
  2         665  
25 2     2   18 use Math::Symbolic::Custom::Base;
  2         3  
  2         85  
26              
27 2     2   76 BEGIN {*import = \&Math::Symbolic::Custom::Base::aggregate_import}
28            
29             our $Aggregate_Export = [qw/to_tall_string/];
30              
31 2     2   11 use Carp;
  2         3  
  2         8769  
32              
33             =pod
34              
35             =head1 SYNOPSIS
36              
37             use strict;
38             use Math::Symbolic 0.613 qw(:all);
39             use Math::Symbolic::Custom::ToTallString;
40              
41             my $example1 = "x / 5";
42             print parse_from_string($example1)->to_tall_string(), "\n\n";
43              
44             # x
45             # ---
46             # 5
47              
48             my $example2 = "(sin((1 / x) - (1 / y))) / (x + y)";
49             print parse_from_string($example2)->to_tall_string(), "\n\n";
50              
51             # ( 1 1 )
52             # sin(--- - ---)
53             # ( x y )
54             # ----------------
55             # x + y
56              
57             my $example3 = "K + (K * ((1 - exp(-2 * K * t))/(1 + exp(-2 * K * t))) )";
58             print parse_from_string($example3)->to_tall_string(10), "\n\n";
59              
60             # ( (-2*K*t) )
61             # ( 1 - e^ )
62             # K + (K * ----------------)
63             # ( (-2*K*t) )
64             # ( 1 + e^ )
65              
66             my $example4 = "((e^x) + (e^-x))/2";
67             print parse_from_string($example4)->to_tall_string(3), "\n\n";
68              
69             # x -x
70             # e^ + e^
71             # ------------
72             # 2
73              
74             =head1 DESCRIPTION
75              
76             Provides C through the Math::Symbolic module extension class. Large Math::Symbolic expressions can sometimes be difficult to read when displayed with C and C (from L). The primary obstacles are the division and exponent operators, so C will compose numerator and denominator onto different lines of output and will put exponents on the line above in an attempt to improve readability. See the examples above. Note that unlike C the output from C is in no way compatible with the Math::Symbolic parser.
77              
78             C accepts one optional parameter, the number of spaces to indent the returned string block.
79              
80             =cut
81              
82             sub to_tall_string {
83 30     30 0 2187834 my ($t, $indent) = @_;
84              
85 30         152 my $pretty = _prettify($t);
86              
87 30 50 33     221 if ( defined($pretty) && (ref($pretty) eq 'ARRAY') ) {
88              
89 30 50       81 if ( defined $indent ) {
90              
91 0 0       0 if ( $indent =~ /\A \d+ \z/msx ) {
92              
93 0         0 my ($frag, $h, $w) = @{$pretty};
  0         0  
94 0         0 my @rows = split(/\n/, $frag);
95 0         0 my @new_rows;
96 0         0 foreach my $row (@rows) {
97 0         0 my $new_line = (" " x $indent) . $row;
98 0         0 push @new_rows, $new_line;
99             }
100 0         0 return join("\n", @new_rows);
101             }
102             else {
103 0         0 carp "to_tall_string(): Indent must be numeric";
104 0         0 return $pretty->[0];
105             }
106              
107             }
108             else {
109 30         155 return $pretty->[0];
110             }
111             }
112            
113 0         0 carp "to_tall_string(): Could not create output string";
114 0         0 return q{};
115             }
116              
117              
118             sub _prettify {
119 251     251   2005 my ($t, $p, $op, $brackets_on) = @_;
120              
121 251 100       660 $brackets_on = 1 unless defined $brackets_on;
122              
123 251 100       850 if ( $t->term_type() == T_VARIABLE ) {
124              
125 49         239 my $fragment = $t->to_string();
126 49         568 my $frag_h = 1;
127 49         96 my $frag_w = length($fragment);
128            
129 49         158 return [$fragment, $frag_h, $frag_w];
130             }
131              
132 202 100       947 if ( $t->term_type() == T_CONSTANT ) {
133 85 100       407 if ( $t->{special} eq 'euler' ) {
134 4         13 return ['e', 1, 1];
135             }
136             else {
137 81         219 my $fragment = $t->to_string();
138 81         821 my $frag_h = 1;
139 81         163 my $frag_w = length($fragment);
140            
141 81         246 return [$fragment, $frag_h, $frag_w];
142             }
143             }
144              
145 117 50       549 if ( $t->term_type() == T_OPERATOR ) {
146              
147 117         600 my $op_info = $Math::Symbolic::Operator::Op_Types[$t->type()];
148 117         1031 my $op_str = $op_info->{infix_string};
149 117         219 my $opn = $op_str;
150 117 100       283 $opn = $op_info->{prefix_string} unless defined $opn;
151              
152 117 100       338 if ( $t->arity() == 2 ) {
    50          
153              
154 108 100       802 if ( not defined $op_str ) {
    100          
    100          
155              
156             # write ln(x) instead of log(e, x)
157 2 50 66     24 if ( ($op_info->{prefix_string} eq 'log') && ($t->op1()->term_type() == T_CONSTANT) && ($t->op1()->{special} eq 'euler') ) {
      66        
158              
159 0         0 my $fragment = _prettify($t->op2(), $t, "ln", 1);
160 0         0 my $prefix = "ln";
161 0         0 return _compose_prefix_frag($fragment, $prefix);
162             }
163             else {
164 2         47 my $fragment = _compose_dual($t, $p, $op, $brackets_on, ',', $opn);
165 2         5 my ($frag, $h, $w) = @{$fragment};
  2         7  
166              
167 2         6 my $op_len = length($opn);
168 2         7 my $height_offset = int($h/2);
169 2         8 my @rows = split("\n", $frag);
170 2         5 my @new_rows;
171 2         8 foreach my $i (0..$h-1) {
172 4         6 my $line;
173 4 100       11 if ( $i == $height_offset ) {
174 2         8 $line = "$opn(" . $rows[$i] . ")";
175             }
176             else {
177 2         8 $line = (" " x $op_len) . "(" . $rows[$i] . ")";
178             }
179 4         11 push @new_rows, $line;
180             }
181              
182 2         8 my $new_frag = join("\n", @new_rows);
183 2         14 return [$new_frag, scalar(@new_rows), $w+2+$op_len];
184             }
185              
186             }
187             elsif ( $t->type() == B_DIVISION ) {
188              
189 30         286 my $frag_num = _prettify($t->op1(), $t, $opn, $brackets_on);
190 30         95 my $frag_den = _prettify($t->op2(), $t, $opn, $brackets_on);
191              
192 30         51 my ($num, $num_h, $num_w) = @{$frag_num};
  30         107  
193 30         48 my ($den, $den_h, $den_w) = @{$frag_den};
  30         63  
194              
195 30         62 my $tot_h = $num_h + 1 + $den_h;
196              
197 30         50 my $tot_w;
198 30         46 my $padding = 2;
199              
200 30 100       70 if ( $num_w > $den_w ) {
201 6         13 $tot_w = $num_w + $padding;
202             }
203             else {
204 24         39 $tot_w = $den_w + $padding;
205             }
206            
207 30         112 my $line = "-" x $tot_w;
208              
209 30         53 my @new_num_rows;
210 30         78 my $pre_num = int(($tot_w - $num_w)/2);
211 30         114 foreach my $line (split("\n", $num)) {
212 45         81 my $new_line = " " x $pre_num;
213 45         78 $new_line .= $line;
214 45         104 while ( length($new_line) < $tot_w ) {
215 58         135 $new_line .= " ";
216             }
217 45         90 push @new_num_rows, $new_line;
218             }
219              
220 30         56 my @new_den_rows;
221 30         68 my $pre_den = int(($tot_w - $den_w)/2);
222 30         85 foreach my $line (split("\n", $den)) {
223 39         94 my $new_line = " " x $pre_den;
224 39         64 $new_line .= $line;
225 39         89 while ( length($new_line) < $tot_w ) {
226 57         146 $new_line .= " ";
227             }
228 39         83 push @new_den_rows, $new_line;
229             }
230              
231 30         55 my $fragment;
232 30 100 66     123 if ( defined($p) && ($p->term_type() == T_OPERATOR) && ($p->type() == B_EXP) ) {
      100        
233 2         28 $fragment = join("\n", (map { "(" . $_ . ")" } (@new_num_rows, $line, @new_den_rows)));
  6         22  
234 2         6 $tot_w += 2;
235             }
236             else {
237 28         269 $fragment = join("\n", (@new_num_rows, $line, @new_den_rows));
238             }
239              
240 30         175 return [$fragment, $tot_h, $tot_w];
241              
242             }
243             elsif ( $t->type() == B_EXP ) {
244              
245             # write sqrt()
246 22 50 100     310 if ( (($t->op2()->term_type() == T_CONSTANT) && ($t->op2()->value() == 0.5)) ||
      100        
      100        
      100        
      66        
      66        
      100        
247             (($t->op2()->term_type() == T_OPERATOR) && ($t->op2()->type() == B_DIVISION) &&
248             ($t->op2()->op1()->term_type == T_CONSTANT) && ($t->op2()->op1()->value() == 1) &&
249             ($t->op2()->op2()->term_type == T_CONSTANT) && ($t->op2()->op2()->value() == 2))
250             ) {
251              
252 4         428 my $fragment = _prettify($t->op1(), $t, "sqrt", 1);
253 4         14 my $prefix = "sqrt";
254 4         17 return _compose_prefix_frag($fragment, $prefix);
255             }
256              
257 18         588 my $frag_num = _prettify($t->op1(), $t, $opn, $brackets_on);
258 18         75 my $frag_pow = _prettify($t->op2(), $t, $opn, $brackets_on);
259              
260 18         37 my ($num, $num_h, $num_w) = @{$frag_num};
  18         50  
261 18         32 my ($pow, $pow_h, $pow_w) = @{$frag_pow};
  18         65  
262              
263 18         98 my @frag1_rows = split("\n", $num);
264 18         51 my @frag2_rows = split("\n", $pow);
265              
266 18         40 my @new_rows;
267 18         33 my $done_op = 0;
268 18         36 EXP_LOOP: while ( 1 ) {
269            
270 41         63 my $new_row;
271 41 100       86 if ( scalar(@frag2_rows) ) {
272              
273 23         51 my $pr = shift @frag2_rows;
274 23         66 $new_row = " " x ($num_w+1);
275 23         54 $new_row .= $pr;
276             }
277             else {
278            
279 18         34 my $nr = shift @frag1_rows;
280 18         35 $new_row = $nr;
281 18 50       43 if ( $done_op ) {
282 0         0 $new_row .= " ";
283             }
284             else {
285 18         34 $new_row .= "^";
286             }
287 18         44 $new_row .= " " x $pow_w;
288             }
289              
290 41         75 push @new_rows, $new_row;
291 41 100       123 last EXP_LOOP unless scalar(@frag1_rows);
292             }
293              
294 18         36 my $new_h = scalar(@new_rows);
295 18         46 my $new_w = $num_w + 1 + $pow_w;
296              
297 18         59 my $new_fragment = join("\n", @new_rows);
298 18         127 return [$new_fragment, $new_h, $new_w];
299             }
300             else {
301 54         783 return _compose_dual($t, $p, $op, $brackets_on, $op_str, $opn);
302             }
303              
304             }
305             elsif ( $t->arity() == 1 ) {
306              
307 9 100       145 if ( not defined $op_str ) {
    50          
308              
309 6         43 my $fragment = _prettify($t->op1(), $t, $opn, 1);
310 6         16 my $prefix = $op_info->{prefix_string};
311 6         20 return _compose_prefix_frag($fragment, $prefix);
312             }
313             elsif ($op_str eq "-") {
314            
315 3         9 my $fragment = _prettify($t->op1(), $t, $opn, 1);
316 3         25 my ($frag, $h, $w) = @{$fragment};
  3         9  
317              
318 3 50 66     11 if ( ($t->op1()->term_type() == T_VARIABLE) || ($t->op1()->term_type() == T_CONSTANT) ) {
319 3         50 my $new_frag = "-" . $frag;
320 3         15 return [$new_frag, 1, length($new_frag)];
321             }
322              
323 0         0 my $height_offset = int($h/2);
324 0         0 my @rows = split("\n", $frag);
325 0         0 my @new_rows;
326 0         0 foreach my $i (0..$h-1) {
327 0         0 my $line;
328 0 0       0 if ( $i == $height_offset ) {
329 0         0 $line = "-( " . $rows[$i] . " )";
330             }
331             else {
332 0         0 $line = " " . "( " . $rows[$i] . " )";
333             }
334 0         0 push @new_rows, $line;
335             }
336              
337 0         0 my $new_frag = join("\n", @new_rows);
338 0         0 return [$new_frag, scalar(@new_rows), $w+5];
339             }
340             else {
341 0         0 croak "operator not recognised";
342             }
343             }
344 0         0 croak "arity not recognised";
345             }
346 0         0 croak "term type not recognised";
347             }
348              
349             sub _compose_prefix_frag {
350 10     10   29 my ($fragment, $prefix) = @_;
351              
352 10         21 my ($frag, $h, $w) = @{$fragment};
  10         27  
353 10         23 my $prefix_len = length($prefix);
354 10         33 my $height_offset = int($h/2);
355 10         39 my @rows = split("\n", $frag);
356              
357 10         17 my @new_rows;
358 10         31 foreach my $i (0..$h-1) {
359 27         42 my $line;
360 27 100       51 if ( $i == $height_offset ) {
361 10         38 $line = $prefix . "(" . $rows[$i] . ")";
362             }
363             else {
364 17         39 $line = (" " x $prefix_len) . "(" . $rows[$i] . ")";
365             }
366 27         85 push @new_rows, $line;
367             }
368            
369 10         24 my $new_w = $prefix_len + $w + 2;
370 10         19 my $new_h = $h;
371 10         31 my $new_frag = join("\n", @new_rows);
372              
373 10         58 return [$new_frag, $new_h, $new_w];
374             }
375              
376             sub _compose_dual {
377 56     56   174 my ($t, $p, $op, $brackets_on, $op_str, $opn) = @_;
378            
379 56         135 my $brackets_on_2 = $brackets_on;
380 56 100       694 if ( $brackets_on ) {
381            
382             # check if we can turn brackets off for the tree below
383 39 100 100     136 if ( _is_all_operator($t, B_PRODUCT) || _is_all_operator($t, B_SUM) ) {
384 18         38 $brackets_on_2 = 0;
385             }
386 39 100 100     209 if ( _is_all_operator($t, [B_SUM, B_DIFFERENCE, B_PRODUCT, U_MINUS, B_EXP]) && _is_expanded($t) ) {
387 26         59 $brackets_on_2 = 0;
388             }
389             }
390              
391 56         284 my $frag1_r = _prettify($t->op1(), $t, $opn, $brackets_on_2);
392 56         178 my $frag2_r = _prettify($t->op2(), $t, $opn, $brackets_on_2);
393              
394 56         101 my ($frag1, $h1, $w1) = @{$frag1_r};
  56         141  
395 56         103 my ($frag2, $h2, $w2) = @{$frag2_r};
  56         154  
396              
397 56         116 my $new_h;
398 56         124 my $f1_h_offset = 0;
399 56         125 my $f2_h_offset = 0;
400 56 100       185 if ( $h1 > $h2 ) {
    100          
401 5         10 $new_h = $h1;
402 5         13 $f2_h_offset = int(($new_h - $h2)/2);
403 5 50 33     28 if ( ($new_h > 1) && ($new_h % 2 == 0) ) {
404 5         13 $f2_h_offset++;
405             }
406             }
407             elsif ( $h1 < $h2 ) {
408 13         23 $new_h = $h2;
409 13         38 $f1_h_offset = int(($new_h - $h1)/2);
410 13 100 66     94 if ( ($new_h > 1) && ($new_h % 2 == 0) ) {
411 6         11 $f1_h_offset++;
412             }
413             }
414             else {
415 38         94 $new_h = $h1;
416             }
417              
418 56         98 my $f1_w_offset = 0;
419 56         157 my $f2_w_offset = $w1 + 1 + length($op_str) + 1;
420 56         194 my $op_space = " " x length($op_str);
421 56         135 my $op_buf = " ";
422 56 100       159 if ( ($op_str eq "*") ) {
423 19 100 66     107 if ( ($h1 == 1) && ($h2 == 1) ) {
    50 66        
    100 66        
424 14         27 $op_buf = "";
425             }
426             elsif ( ($t->op1()->term_type() == T_OPERATOR) && ($t->op1()->type() == B_EXP) ) {
427 0         0 $op_buf = "";
428             }
429             elsif ( ($t->op2()->term_type() == T_OPERATOR) && ($t->op2()->type() == B_EXP) ) {
430 3         136 $op_buf = "";
431             }
432             }
433              
434 56         190 my $new_w = $w1 + length($op_str) + $w2 + (2*length($op_buf));
435              
436 56         157 my $op_h_offset = int($new_h/2);
437              
438 56         170 my @frag1_rows = split("\n", $frag1);
439 56         150 my @frag2_rows = split("\n", $frag2);
440              
441 56         111 my @new_rows;
442 56         144 foreach my $i (0..$new_h-1) {
443              
444 101         155 my $f1;
445 101 100 100     330 if ( ($i >= $f1_h_offset) && scalar(@frag1_rows) ) {
446 74         154 $f1 = shift @frag1_rows;
447             }
448             else {
449 27         59 $f1 = " " x $w1;
450             }
451            
452 101         164 my $f2;
453 101 100 100     310 if ( ($i >= $f2_h_offset) && scalar(@frag2_rows) ) {
454 96         198 $f2 = shift @frag2_rows;
455             }
456             else {
457 5         12 $f2 = " " x $w2;
458             }
459              
460 101         170 my $new_row;
461 101 100       210 if ( $i == $op_h_offset ) {
462 56         116 $new_row = $f1 . $op_buf . $op_str . $op_buf . $f2;
463             }
464             else {
465 45         100 $new_row = $f1 . $op_buf . $op_space . $op_buf . $f2;
466             }
467 101         266 push @new_rows, $new_row;
468             }
469            
470 56         87 my $new_fragment;
471 56         105 my $do_brackets = $brackets_on;
472 56         117 $do_brackets &= defined($p);
473 56 100 66     215 $do_brackets = 1 if defined($p) && ($p->term_type() == T_OPERATOR) && ($p->type() == B_EXP);
      100        
474 56 100 66     603 $do_brackets = 0 if defined($p) && ($p->term_type() == T_OPERATOR) && ($p->type() == B_DIVISION);
      100        
475 56 100 100     522 $do_brackets = 0 if defined($p) && ($p->arity() == 1);
476 56 100 66     449 $do_brackets = 0 if defined($op) && (($op eq 'ln') || ($op eq 'sqrt'));
      100        
477            
478 56 100       111 if ( $do_brackets ) {
479 14         39 $new_fragment = join("\n", (map { "(" . $_ . ")" } @new_rows));
  22         96  
480 14         32 $new_w += 2;
481             }
482             else {
483 42         143 $new_fragment = join("\n", @new_rows);
484             }
485              
486 56         345 return [$new_fragment, $new_h, $new_w];
487             }
488              
489             ### These routines duplicated from ToShorterString.pm
490              
491             sub _is_all_operator {
492 332     332   1362 my ($t, $op_type) = @_;
493            
494 332 100 100     788 return 1 if ($t->term_type() == T_CONSTANT) || ($t->term_type() == T_VARIABLE);
495              
496             # this will stop descent into e.g. sin, cos
497 189         1493 my $op = $Math::Symbolic::Operator::Op_Types[$t->type()];
498 189 100 66     1510 if ( defined($op->{prefix_string}) and not defined($op->{infix_string}) ) {
499 4         18 return 1;
500             }
501            
502 185 100       443 if ( ref($op_type) eq "ARRAY" ) {
503 89         165 my @m = grep { $_ == $t->type() } @{$op_type};
  445         2478  
  89         491  
504 89 100       710 return 0 if scalar(@m) == 0;
505             }
506             else {
507 96 100       211 return 0 if $t->type() != $op_type;
508             }
509            
510 111         355 my $ok = 1;
511 111         229 $ok &= _is_all_operator($_, $op_type) for @{$t->{operands}};
  111         364  
512 111         1009 return $ok;
513             }
514              
515             sub _is_expanded {
516 147     147   480 my ($t, $flag) = @_;
517            
518 147 100       340 $flag = 0 unless defined $flag;
519              
520 147 100 100     389 return 1 if ($t->term_type() == T_CONSTANT) || ($t->term_type() == T_VARIABLE);
521            
522 66         564 my $op = $Math::Symbolic::Operator::Op_Types[$t->type()];
523 66 100 66     563 if ( defined($op->{prefix_string}) and not defined($op->{infix_string}) ) {
524 2         9 return 1;
525             }
526            
527 64 100 66     179 if ( $flag && (($t->type() == B_SUM) || ($t->type() == B_DIFFERENCE)) ) {
      66        
528 4         42 return 0;
529             }
530              
531 60 100 100     314 if ( ($t->type() == B_PRODUCT) || ($t->type() == B_DIFFERENCE) ) {
532 20         153 $flag = 1;
533             }
534              
535 60         522 my $ok = 1;
536 60         94 $ok &= _is_expanded($_, $flag) for @{$t->{operands}};
  60         220  
537 60         404 return $ok;
538             }
539              
540             =pod
541              
542             =head1 SEE ALSO
543              
544             L
545              
546             L
547              
548             =head1 AUTHOR
549              
550             Matt Johnson, C<< >>
551              
552             =head1 ACKNOWLEDGEMENTS
553              
554             Steffen Mueller, author of Math::Symbolic
555              
556             =head1 LICENSE AND COPYRIGHT
557              
558             This software is copyright (c) 2025 by Matt Johnson.
559              
560             This is free software; you can redistribute it and/or modify it under
561             the same terms as the Perl 5 programming language system itself.
562              
563             =cut
564              
565             1;
566             __END__