The Perl Toolchain Summit 2025 Needs You: You can help 🙏 Learn more

use 5.006;
use strict;
no warnings 'recursion';
=pod
=encoding utf8
=head1 NAME
Math::Symbolic::Custom::ToTallString - Pretty-print Math::Symbolic expressions
=head1 VERSION
Version 0.11
=cut
our $VERSION = '0.11';
use Math::Symbolic qw(:all);
BEGIN {*import = \&Math::Symbolic::Custom::Base::aggregate_import}
our $Aggregate_Export = [qw/to_tall_string/];
use Carp;
=pod
=head1 SYNOPSIS
use strict;
use Math::Symbolic 0.613 qw(:all);
use Math::Symbolic::Custom::ToTallString;
my $example1 = "x / 5";
print parse_from_string($example1)->to_tall_string(), "\n\n";
# x
# ---
# 5
my $example2 = "(sin((1 / x) - (1 / y))) / (x + y)";
print parse_from_string($example2)->to_tall_string(), "\n\n";
# ( 1 1 )
# sin(--- - ---)
# ( x y )
# ----------------
# x + y
my $example3 = "K + (K * ((1 - exp(-2 * K * t))/(1 + exp(-2 * K * t))) )";
print parse_from_string($example3)->to_tall_string(10), "\n\n";
# ( (-2*K*t) )
# ( 1 - e^ )
# K + (K * ----------------)
# ( (-2*K*t) )
# ( 1 + e^ )
my $example4 = "((e^x) + (e^-x))/2";
print parse_from_string($example4)->to_tall_string(3), "\n\n";
# x -x
# e^ + e^
# ------------
# 2
=head1 DESCRIPTION
Provides C<to_tall_string()> through the Math::Symbolic module extension class. Large Math::Symbolic expressions can sometimes be difficult to read when displayed with C<to_string()> and C<to_shorter_infix_string()> (from L<Math::Symbolic::Custom::ToShorterString>). The primary obstacles are the division and exponent operators, so C<to_tall_string()> 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<to_shorter_infix_string()> the output from C<to_tall_string()> is in no way compatible with the Math::Symbolic parser.
C<to_tall_string()> accepts one optional parameter, the number of spaces to indent the returned string block.
=cut
sub to_tall_string {
my ($t, $indent) = @_;
my $pretty = _prettify($t);
if ( defined($pretty) && (ref($pretty) eq 'ARRAY') ) {
if ( defined $indent ) {
if ( $indent =~ /\A \d+ \z/msx ) {
my ($frag, $h, $w) = @{$pretty};
my @rows = split(/\n/, $frag);
my @new_rows;
foreach my $row (@rows) {
my $new_line = (" " x $indent) . $row;
push @new_rows, $new_line;
}
return join("\n", @new_rows);
}
else {
carp "to_tall_string(): Indent must be numeric";
return $pretty->[0];
}
}
else {
return $pretty->[0];
}
}
carp "to_tall_string(): Could not create output string";
return q{};
}
sub _prettify {
my ($t, $p, $op, $brackets_on) = @_;
$brackets_on = 1 unless defined $brackets_on;
if ( $t->term_type() == T_VARIABLE ) {
my $fragment = $t->to_string();
my $frag_h = 1;
my $frag_w = length($fragment);
return [$fragment, $frag_h, $frag_w];
}
if ( $t->term_type() == T_CONSTANT ) {
if ( $t->{special} eq 'euler' ) {
return ['e', 1, 1];
}
else {
my $fragment = $t->to_string();
my $frag_h = 1;
my $frag_w = length($fragment);
return [$fragment, $frag_h, $frag_w];
}
}
if ( $t->term_type() == T_OPERATOR ) {
my $op_info = $Math::Symbolic::Operator::Op_Types[$t->type()];
my $op_str = $op_info->{infix_string};
my $opn = $op_str;
$opn = $op_info->{prefix_string} unless defined $opn;
if ( $t->arity() == 2 ) {
if ( not defined $op_str ) {
# write ln(x) instead of log(e, x)
if ( ($op_info->{prefix_string} eq 'log') && ($t->op1()->term_type() == T_CONSTANT) && ($t->op1()->{special} eq 'euler') ) {
my $fragment = _prettify($t->op2(), $t, "ln", 1);
my $prefix = "ln";
return _compose_prefix_frag($fragment, $prefix);
}
else {
my $fragment = _compose_dual($t, $p, $op, $brackets_on, ',', $opn);
my ($frag, $h, $w) = @{$fragment};
my $op_len = length($opn);
my $height_offset = int($h/2);
my @rows = split("\n", $frag);
my @new_rows;
foreach my $i (0..$h-1) {
my $line;
if ( $i == $height_offset ) {
$line = "$opn(" . $rows[$i] . ")";
}
else {
$line = (" " x $op_len) . "(" . $rows[$i] . ")";
}
push @new_rows, $line;
}
my $new_frag = join("\n", @new_rows);
return [$new_frag, scalar(@new_rows), $w+2+$op_len];
}
}
elsif ( $t->type() == B_DIVISION ) {
my $frag_num = _prettify($t->op1(), $t, $opn, $brackets_on);
my $frag_den = _prettify($t->op2(), $t, $opn, $brackets_on);
my ($num, $num_h, $num_w) = @{$frag_num};
my ($den, $den_h, $den_w) = @{$frag_den};
my $tot_h = $num_h + 1 + $den_h;
my $tot_w;
my $padding = 2;
if ( $num_w > $den_w ) {
$tot_w = $num_w + $padding;
}
else {
$tot_w = $den_w + $padding;
}
my $line = "-" x $tot_w;
my @new_num_rows;
my $pre_num = int(($tot_w - $num_w)/2);
foreach my $line (split("\n", $num)) {
my $new_line = " " x $pre_num;
$new_line .= $line;
while ( length($new_line) < $tot_w ) {
$new_line .= " ";
}
push @new_num_rows, $new_line;
}
my @new_den_rows;
my $pre_den = int(($tot_w - $den_w)/2);
foreach my $line (split("\n", $den)) {
my $new_line = " " x $pre_den;
$new_line .= $line;
while ( length($new_line) < $tot_w ) {
$new_line .= " ";
}
push @new_den_rows, $new_line;
}
my $fragment;
if ( defined($p) && ($p->term_type() == T_OPERATOR) && ($p->type() == B_EXP) ) {
$fragment = join("\n", (map { "(" . $_ . ")" } (@new_num_rows, $line, @new_den_rows)));
$tot_w += 2;
}
else {
$fragment = join("\n", (@new_num_rows, $line, @new_den_rows));
}
return [$fragment, $tot_h, $tot_w];
}
elsif ( $t->type() == B_EXP ) {
# write sqrt()
if ( (($t->op2()->term_type() == T_CONSTANT) && ($t->op2()->value() == 0.5)) ||
(($t->op2()->term_type() == T_OPERATOR) && ($t->op2()->type() == B_DIVISION) &&
($t->op2()->op1()->term_type == T_CONSTANT) && ($t->op2()->op1()->value() == 1) &&
($t->op2()->op2()->term_type == T_CONSTANT) && ($t->op2()->op2()->value() == 2))
) {
my $fragment = _prettify($t->op1(), $t, "sqrt", 1);
my $prefix = "sqrt";
return _compose_prefix_frag($fragment, $prefix);
}
my $frag_num = _prettify($t->op1(), $t, $opn, $brackets_on);
my $frag_pow = _prettify($t->op2(), $t, $opn, $brackets_on);
my ($num, $num_h, $num_w) = @{$frag_num};
my ($pow, $pow_h, $pow_w) = @{$frag_pow};
my @frag1_rows = split("\n", $num);
my @frag2_rows = split("\n", $pow);
my @new_rows;
my $done_op = 0;
EXP_LOOP: while ( 1 ) {
my $new_row;
if ( scalar(@frag2_rows) ) {
my $pr = shift @frag2_rows;
$new_row = " " x ($num_w+1);
$new_row .= $pr;
}
else {
my $nr = shift @frag1_rows;
$new_row = $nr;
if ( $done_op ) {
$new_row .= " ";
}
else {
$new_row .= "^";
}
$new_row .= " " x $pow_w;
}
push @new_rows, $new_row;
last EXP_LOOP unless scalar(@frag1_rows);
}
my $new_h = scalar(@new_rows);
my $new_w = $num_w + 1 + $pow_w;
my $new_fragment = join("\n", @new_rows);
return [$new_fragment, $new_h, $new_w];
}
else {
return _compose_dual($t, $p, $op, $brackets_on, $op_str, $opn);
}
}
elsif ( $t->arity() == 1 ) {
if ( not defined $op_str ) {
my $fragment = _prettify($t->op1(), $t, $opn, 1);
my $prefix = $op_info->{prefix_string};
return _compose_prefix_frag($fragment, $prefix);
}
elsif ($op_str eq "-") {
my $fragment = _prettify($t->op1(), $t, $opn, 1);
my ($frag, $h, $w) = @{$fragment};
if ( ($t->op1()->term_type() == T_VARIABLE) || ($t->op1()->term_type() == T_CONSTANT) ) {
my $new_frag = "-" . $frag;
return [$new_frag, 1, length($new_frag)];
}
my $height_offset = int($h/2);
my @rows = split("\n", $frag);
my @new_rows;
foreach my $i (0..$h-1) {
my $line;
if ( $i == $height_offset ) {
$line = "-( " . $rows[$i] . " )";
}
else {
$line = " " . "( " . $rows[$i] . " )";
}
push @new_rows, $line;
}
my $new_frag = join("\n", @new_rows);
return [$new_frag, scalar(@new_rows), $w+5];
}
else {
croak "operator not recognised";
}
}
croak "arity not recognised";
}
croak "term type not recognised";
}
sub _compose_prefix_frag {
my ($fragment, $prefix) = @_;
my ($frag, $h, $w) = @{$fragment};
my $prefix_len = length($prefix);
my $height_offset = int($h/2);
my @rows = split("\n", $frag);
my @new_rows;
foreach my $i (0..$h-1) {
my $line;
if ( $i == $height_offset ) {
$line = $prefix . "(" . $rows[$i] . ")";
}
else {
$line = (" " x $prefix_len) . "(" . $rows[$i] . ")";
}
push @new_rows, $line;
}
my $new_w = $prefix_len + $w + 2;
my $new_h = $h;
my $new_frag = join("\n", @new_rows);
return [$new_frag, $new_h, $new_w];
}
sub _compose_dual {
my ($t, $p, $op, $brackets_on, $op_str, $opn) = @_;
my $brackets_on_2 = $brackets_on;
if ( $brackets_on ) {
# check if we can turn brackets off for the tree below
if ( _is_all_operator($t, B_PRODUCT) || _is_all_operator($t, B_SUM) ) {
$brackets_on_2 = 0;
}
if ( _is_all_operator($t, [B_SUM, B_DIFFERENCE, B_PRODUCT, U_MINUS, B_EXP]) && _is_expanded($t) ) {
$brackets_on_2 = 0;
}
}
my $frag1_r = _prettify($t->op1(), $t, $opn, $brackets_on_2);
my $frag2_r = _prettify($t->op2(), $t, $opn, $brackets_on_2);
my ($frag1, $h1, $w1) = @{$frag1_r};
my ($frag2, $h2, $w2) = @{$frag2_r};
my $new_h;
my $f1_h_offset = 0;
my $f2_h_offset = 0;
if ( $h1 > $h2 ) {
$new_h = $h1;
$f2_h_offset = int(($new_h - $h2)/2);
if ( ($new_h > 1) && ($new_h % 2 == 0) ) {
$f2_h_offset++;
}
}
elsif ( $h1 < $h2 ) {
$new_h = $h2;
$f1_h_offset = int(($new_h - $h1)/2);
if ( ($new_h > 1) && ($new_h % 2 == 0) ) {
$f1_h_offset++;
}
}
else {
$new_h = $h1;
}
my $f1_w_offset = 0;
my $f2_w_offset = $w1 + 1 + length($op_str) + 1;
my $op_space = " " x length($op_str);
my $op_buf = " ";
if ( ($op_str eq "*") ) {
if ( ($h1 == 1) && ($h2 == 1) ) {
$op_buf = "";
}
elsif ( ($t->op1()->term_type() == T_OPERATOR) && ($t->op1()->type() == B_EXP) ) {
$op_buf = "";
}
elsif ( ($t->op2()->term_type() == T_OPERATOR) && ($t->op2()->type() == B_EXP) ) {
$op_buf = "";
}
}
my $new_w = $w1 + length($op_str) + $w2 + (2*length($op_buf));
my $op_h_offset = int($new_h/2);
my @frag1_rows = split("\n", $frag1);
my @frag2_rows = split("\n", $frag2);
my @new_rows;
foreach my $i (0..$new_h-1) {
my $f1;
if ( ($i >= $f1_h_offset) && scalar(@frag1_rows) ) {
$f1 = shift @frag1_rows;
}
else {
$f1 = " " x $w1;
}
my $f2;
if ( ($i >= $f2_h_offset) && scalar(@frag2_rows) ) {
$f2 = shift @frag2_rows;
}
else {
$f2 = " " x $w2;
}
my $new_row;
if ( $i == $op_h_offset ) {
$new_row = $f1 . $op_buf . $op_str . $op_buf . $f2;
}
else {
$new_row = $f1 . $op_buf . $op_space . $op_buf . $f2;
}
push @new_rows, $new_row;
}
my $new_fragment;
my $do_brackets = $brackets_on;
$do_brackets &= defined($p);
$do_brackets = 1 if defined($p) && ($p->term_type() == T_OPERATOR) && ($p->type() == B_EXP);
$do_brackets = 0 if defined($p) && ($p->term_type() == T_OPERATOR) && ($p->type() == B_DIVISION);
$do_brackets = 0 if defined($p) && ($p->arity() == 1);
$do_brackets = 0 if defined($op) && (($op eq 'ln') || ($op eq 'sqrt'));
if ( $do_brackets ) {
$new_fragment = join("\n", (map { "(" . $_ . ")" } @new_rows));
$new_w += 2;
}
else {
$new_fragment = join("\n", @new_rows);
}
return [$new_fragment, $new_h, $new_w];
}
### These routines duplicated from ToShorterString.pm
sub _is_all_operator {
my ($t, $op_type) = @_;
return 1 if ($t->term_type() == T_CONSTANT) || ($t->term_type() == T_VARIABLE);
# this will stop descent into e.g. sin, cos
my $op = $Math::Symbolic::Operator::Op_Types[$t->type()];
if ( defined($op->{prefix_string}) and not defined($op->{infix_string}) ) {
return 1;
}
if ( ref($op_type) eq "ARRAY" ) {
my @m = grep { $_ == $t->type() } @{$op_type};
return 0 if scalar(@m) == 0;
}
else {
return 0 if $t->type() != $op_type;
}
my $ok = 1;
$ok &= _is_all_operator($_, $op_type) for @{$t->{operands}};
return $ok;
}
sub _is_expanded {
my ($t, $flag) = @_;
$flag = 0 unless defined $flag;
return 1 if ($t->term_type() == T_CONSTANT) || ($t->term_type() == T_VARIABLE);
my $op = $Math::Symbolic::Operator::Op_Types[$t->type()];
if ( defined($op->{prefix_string}) and not defined($op->{infix_string}) ) {
return 1;
}
if ( $flag && (($t->type() == B_SUM) || ($t->type() == B_DIFFERENCE)) ) {
return 0;
}
if ( ($t->type() == B_PRODUCT) || ($t->type() == B_DIFFERENCE) ) {
$flag = 1;
}
my $ok = 1;
$ok &= _is_expanded($_, $flag) for @{$t->{operands}};
return $ok;
}
=pod
=head1 SEE ALSO
L<Math::Symbolic>
L<Math::Symbolic::Custom::ToShorterString>
=head1 AUTHOR
Matt Johnson, C<< <mjohnson at cpan.org> >>
=head1 ACKNOWLEDGEMENTS
Steffen Mueller, author of Math::Symbolic
=head1 LICENSE AND COPYRIGHT
This software is copyright (c) 2025 by Matt Johnson.
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
=cut
1;
__END__