package Math::Symbolic::Custom::ToShorterString; use 5.006; use strict; use warnings; no warnings 'recursion'; =pod =encoding utf8 =head1 NAME Math::Symbolic::Custom::ToShorterString - Shorter string representations of Math::Symbolic trees =head1 VERSION Version 0.2 =cut our $VERSION = '0.2'; use Math::Symbolic qw(:all); use Math::Symbolic::Custom::Base; BEGIN {*import = \&Math::Symbolic::Custom::Base::aggregate_import} our $Aggregate_Export = [qw/to_shorter_infix_string/]; # define ln in the parser as the natural logarithm use Math::SymbolicX::ParserExtensionFactory ( ln => sub { my $arg = shift; return Math::Symbolic::Operator->new('log', Math::Symbolic::Constant->euler(), $arg); }, ); use Carp; =pod =head1 SYNOPSIS use Math::Symbolic 0.613 qw(:all); use Math::Symbolic::Custom::ToShorterString 0.2; # Note: ToShorterString v0.2 automatically adds ln(x) as an alias for log(e,x) in the parser my $f = parse_from_string("1*2+3*4+5*sqrt(x+y+z)+ln(y)"); # Try displaying with Math::Symbolic's to_string() my $to_string = $f->to_string(); print "to_string():\t$to_string\n"; # to_string(): (((1 * 2) + (3 * 4)) + (5 * (((x + y) + z) ^ 0.5))) + (log(2.71828182845905, y)) # Try displaying with ToShorterString my $to_shorter_infix_string = $f->to_shorter_infix_string(); print "to_shorter_infix_string():\t$to_shorter_infix_string\n"; # to_shorter_infix_string(): ((1*2 + 3*4) + (5*sqrt(x + y + z))) + ln(y) # Check that the two output string representations parse to the same expression my $f2 = parse_from_string($to_string); my $f3 = parse_from_string($to_shorter_infix_string); if ( $f2->to_string() eq $f3->to_string() ) { print "Parsed to same string\n"; } =head1 DESCRIPTION Provides C<to_shorter_infix_string()> 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<to_string()> method. 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<sqrt()> and C<exp()>. From v0.2, the module uses L<Math::SymbolicX::ParserExtensionFactory> to automatically add C<ln(x)> as an alias for C<log(e,x)> in the parser, and uses it for string output as well (in the same way as C<sqrt()> and C<exp()>). The "to_shorter_infix_string()" does not replace the "to_string()" method, it has to be called explicitly. =cut sub to_shorter_infix_string { my ($t, $brackets_on) = @_; if ( ($t->term_type() == T_CONSTANT) || ($t->term_type() == T_VARIABLE) ) { return $t->to_string(); } $brackets_on = 1 unless defined $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 = 0; } # "expanded" for a simple expression essentially defined as no +/- below a * in the tree if ( _is_all_operator($t, [B_SUM, B_DIFFERENCE, B_PRODUCT]) && _is_expanded($t) ) { $brackets_on = 0; } } # at this point the top of $t must be an operator my $string = ''; my $op_info = $Math::Symbolic::Operator::Op_Types[$t->type()]; my $op_str = $op_info->{infix_string}; if ( $t->arity() == 2 ) { # handle special cases # prefix operator 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') ) { $string .= "ln(" . to_shorter_infix_string($t->op2(), $brackets_on) . ")"; } else { $string .= $op_info->{prefix_string} . "("; $string .= join( ', ', map { to_shorter_infix_string($_, $brackets_on) } @{ $t->{operands} } ); $string .= ')'; } } # 'sqrt' and 'exp' are in the parser, use them elsif ( $t->type() == B_EXP ) { if ( ($t->op2()->term_type() == T_CONSTANT) && ($t->op2()->value() == 0.5) ) { $string .= "sqrt(" . to_shorter_infix_string($t->op1(), $brackets_on) . ")"; } elsif ( ($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) ) { $string .= "sqrt(" . to_shorter_infix_string($t->op1(), $brackets_on) . ")"; } elsif ( ($t->op1()->term_type() == T_CONSTANT) && ($t->op1()->{special} eq 'euler') ) { $string .= "exp(" . to_shorter_infix_string($t->op2(), $brackets_on) . ")"; } } if ( $string eq '' ) { # various conditions for temporarily disabling brackets my @brackets = ($brackets_on, $brackets_on); if ( $brackets_on ) { foreach my $i (0,1) { my $op = $t->{operands}[$i]; if ( ($op->term_type() == T_CONSTANT) || ($op->term_type() == T_VARIABLE) ) { # it's a constant or a variable $brackets[$i] = 0; } elsif ( $op->term_type() == T_OPERATOR ) { # it's going to be a prefix operator (e.g. sin) if ( !defined($Math::Symbolic::Operator::Op_Types[$op->type()]->{infix_string}) ) { $brackets[$i] = 0; } # remove brackets around exponentiation operator ^ elsif ( $op->type() == B_EXP ) { $brackets[$i] = 0; } } } } # keep the spaces around * for readability when removing brackets around exponents $op_str = " $op_str " unless ($op_str eq '^') || ( ($op_str eq '*') && !(($t->op1()->term_type() == T_OPERATOR) && ($t->op1()->type() == B_EXP)) ); $string .= ( $brackets[0] ? '(' : '' ) . to_shorter_infix_string($t->op1(), $brackets_on) . ( $brackets[0] ? ')' : '' ) . $op_str . ( $brackets[1] ? '(' : '' ) . to_shorter_infix_string($t->op2(), $brackets_on) . ( $brackets[1] ? ')' : '' ); } } elsif ( $t->arity() == 1 ) { # force brackets around the contents of prefix/function-style operators if ( not defined $op_str ) { $string .= $op_info->{prefix_string} . "(" . to_shorter_infix_string($t->op1(), 1) . ")"; } else { my $is_op1 = $t->op1()->term_type() == T_OPERATOR; $string .= "$op_str" . ( $is_op1 ? '(' : '' ) . to_shorter_infix_string($t->op1(), 1) . ( $is_op1 ? ')' : '' ); } } else { carp("Cannot proceed deeper with operator using unsupported number of arguments: " . $t->arity()); } return $string; } # _is_all_operator # returns 1 if the passed in tree $t is comprised entirely of the # operator(s) specified in $op_type (excluding prefix-only operators) 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; } # _is_expanded # returns 1 if there are no +/- below a * in the tree. # FIXME: Cannot really be run by itself - has to be restricted to the operators involved, i.e.: # _is_all_operator($t, [B_SUM, B_DIFFERENCE, B_PRODUCT]) && _is_expanded($t) 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> =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) 2024 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__