Free cookie consent management tool by TermsFeed Policy Generator

Changeset 18211


Ignore:
Timestamp:
01/27/22 11:02:45 (2 years ago)
Author:
gkronber
Message:

#3145: refactored infix formatter to improve output (less parenthesis) and added unit tests to test that the infix formatter works correclty.

Location:
trunk
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • trunk/HeuristicLab.Encodings.SymbolicExpressionTreeEncoding/3.4/Interfaces/ISymbolicExpressionTreeNode.cs

    r17180 r18211  
    5050    void InsertSubtree(int index, ISymbolicExpressionTreeNode tree);
    5151    void RemoveSubtree(int index);
     52    void ReplaceSubtree(int index, ISymbolicExpressionTreeNode tree);
     53    void ReplaceSubtree(ISymbolicExpressionTreeNode orig, ISymbolicExpressionTreeNode repl);
    5254
    5355    void ResetLocalParameters(IRandom random);
  • trunk/HeuristicLab.Encodings.SymbolicExpressionTreeEncoding/3.4/SymbolicExpressionTreeNode.cs

    r17180 r18211  
    177177      ResetCachedValues();
    178178    }
     179    public virtual void ReplaceSubtree(int index, ISymbolicExpressionTreeNode repl) {
     180      subtrees[index].Parent = null;
     181      subtrees[index] = repl;
     182      repl.Parent = this;
     183      ResetCachedValues();
     184    }
     185    public virtual void ReplaceSubtree(ISymbolicExpressionTreeNode old, ISymbolicExpressionTreeNode repl) {
     186      var index = IndexOfSubtree(old);
     187      subtrees[index].Parent = null;
     188      subtrees[index] = repl;
     189      repl.Parent = this;
     190      ResetCachedValues();
     191    }
    179192
    180193    public IEnumerable<ISymbolicExpressionTreeNode> IterateNodesBreadth() {
  • trunk/HeuristicLab.Problems.DataAnalysis.Symbolic/3.4/Formatters/InfixExpressionFormatter.cs

    r18203 r18211  
    3232namespace HeuristicLab.Problems.DataAnalysis.Symbolic {
    3333  public static class BaseInfixExpressionFormatter {
     34    /// <summary>
     35    ///  Performs some basic re-writing steps to simplify the code for formatting. Tree is changed.
     36    ///  Removes single-argument +, * which have no effect
     37    ///  Replaces variables with coefficients by an explicitly multiplication
     38    ///  Replaces single-argument / with 1 / (..)
     39    ///  Replaces multi-argument +, *, /, - with nested binary operations
     40    ///  Rotates operations to remove necessity to group sub-expressions
     41    /// </summary>
     42    /// <param name="tree">The tree is changed</param>
     43    public static void ConvertToBinaryLeftAssoc(ISymbolicExpressionTree tree) {
     44      ConvertToBinaryLeftAssocRec(tree.Root.GetSubtree(0), tree.Root.GetSubtree(0).GetSubtree(0));
     45    }
     46
     47    private static void ConvertToBinaryLeftAssocRec(ISymbolicExpressionTreeNode parent, ISymbolicExpressionTreeNode n) {
     48      // recurse post-order
     49      foreach (var subtree in n.Subtrees.ToArray()) ConvertToBinaryLeftAssocRec(n, subtree); // ToArray required as n.Subtrees is changed in method
     50
     51      if (n is VariableTreeNode varTreeNode && varTreeNode.Weight != 1.0) {
     52        var mul = new Multiplication().CreateTreeNode();
     53        var num = (NumberTreeNode)new Number().CreateTreeNode();
     54        num.Value = varTreeNode.Weight;
     55        varTreeNode.Weight = 1.0;
     56        parent.ReplaceSubtree(n, mul);
     57        mul.AddSubtree(num);
     58        mul.AddSubtree(varTreeNode);
     59      } else if (n.SubtreeCount == 1 && (n.Symbol is Addition || n.Symbol is Multiplication || n.Symbol is And || n.Symbol is Or || n.Symbol is Xor)) {
     60        // single-argument addition or multiplication has no effect -> remove
     61        parent.ReplaceSubtree(n, n.GetSubtree(0));
     62      } else if (n.SubtreeCount == 1 && (n.Symbol is Division)) {
     63        // single-argument division is 1/f(x)
     64        n.InsertSubtree(0, new Constant() { Value = 1.0 }.CreateTreeNode());
     65      } else if (n.SubtreeCount > 2 && IsLeftAssocOp(n.Symbol)) {
     66        // multi-argument +, -, *, / are the same as multiple binary operations (left-associative)
     67        var sy = n.Symbol;
     68
     69        var additionalTrees = n.Subtrees.Skip(2).ToArray();
     70        while (n.SubtreeCount > 2) n.RemoveSubtree(2); // keep only the first two arguments
     71
     72        var childIdx = parent.IndexOfSubtree(n);
     73        parent.RemoveSubtree(childIdx);
     74        var newChild = n;
     75        // build a tree bottom to top, each time adding a subtree on the right
     76        for (int i = 0; i < additionalTrees.Length; i++) {
     77          var newOp = sy.CreateTreeNode();
     78          newOp.AddSubtree(newChild);
     79          newOp.AddSubtree(additionalTrees[i]);
     80          newChild = newOp;
     81        }
     82        parent.InsertSubtree(childIdx, newChild);
     83      } else if (n.SubtreeCount == 2 && n.GetSubtree(1).SubtreeCount == 2 &&
     84                 IsAssocOp(n.Symbol) && IsOperator(n.GetSubtree(1).Symbol) &&
     85                 Priority(n.Symbol) == Priority(n.GetSubtree(1).Symbol)) {
     86        // f(x) <op> (g(x) <op> h(x))) is the same as  (f(x) <op> g(x)) <op> h(x) for associative <op>
     87        // which is the same as f(x) <op> g(x) <op> h(x) for left-associative <op>
     88        // The latter version is preferred because we do not need to write the parentheses.
     89        // rotation:
     90        //     (op1)              (op2)
     91        //     /   \              /  \
     92        //    a    (op2)       (op1)  c
     93        //         /  \        /  \
     94        //        b    c      a    b     
     95        var op1 = n;
     96        var op2 = n.GetSubtree(1);
     97        var b = op2.GetSubtree(0); op2.RemoveSubtree(0);
     98        op1.ReplaceSubtree(op2, b);
     99        parent.ReplaceSubtree(op1, op2);
     100        op2.InsertSubtree(0, op1);
     101      }
     102    }
    34103    public static void FormatRecursively(ISymbolicExpressionTreeNode node, StringBuilder strBuilder,
    35104                                          NumberFormatInfo numberFormat, string formatString, List<KeyValuePair<string, double>> parameters = null) {
    36       if (node.SubtreeCount > 1) {
    37         var token = GetToken(node.Symbol);
    38         // operators
    39         if (token == "+" || token == "-" || token == "OR" || token == "XOR") {
    40           var parenthesisRequired = false;
    41           if (node.Parent != null && IsOperator(node.Parent.Symbol)) {
    42             var parentOp = GetToken(node.Parent.Symbol);
    43             if (parentOp != "+" && parentOp != "-" && parentOp != "OR" && parentOp != "XOR")
    44               parenthesisRequired = true;
    45           }
     105      // This method assumes that the tree has been converted to binary and left-assoc form (see ConvertToBinaryLeftAssocRec).
     106      // An exception is thrown if the tree has a different shape.
     107
     108      if (node.SubtreeCount == 0) {
     109        // no subtrees
     110        if (node.Symbol is Variable) {
     111          var varNode = node as VariableTreeNode;
     112          if (varNode.Weight != 1.0) { throw new NotSupportedException("Infix formatter does not support variables with coefficients."); }
     113          AppendVariableName(strBuilder, varNode.VariableName);
     114        } else if (node is INumericTreeNode numNode) {
     115          var parenthesisRequired = RequiresParenthesis(node.Parent, node);
    46116          if (parenthesisRequired) strBuilder.Append("(");
    47           FormatRecursively(node.Subtrees.First(), strBuilder, numberFormat, formatString, parameters);
    48 
    49           foreach (var subtree in node.Subtrees.Skip(1)) {
    50             strBuilder.Append(" ").Append(token).Append(" ");
    51             FormatRecursively(subtree, strBuilder, numberFormat, formatString, parameters);
    52           }
    53 
     117
     118          AppendNumber(strBuilder, parameters, numNode.Value, formatString, numberFormat);
    54119          if (parenthesisRequired) strBuilder.Append(")");
    55         } else if (token == "*" || token == "/" || token == "AND") {
    56           FormatRecursively(node.Subtrees.First(), strBuilder, numberFormat, formatString, parameters);
    57 
    58           foreach (var subtree in node.Subtrees.Skip(1)) {
    59             strBuilder.Append(" ").Append(token).Append(" ");
    60             // a / b * c => a / (b * C)
    61             if (subtree.SubtreeCount > 1 && token == "/" && GetToken(subtree.Symbol) == "*") {
    62               strBuilder.Append("(");
    63               FormatRecursively(subtree, strBuilder, numberFormat, formatString, parameters);
    64               strBuilder.Append(")");
    65             } else {
    66               FormatRecursively(subtree, strBuilder, numberFormat, formatString, parameters);
    67             }
    68           }
    69         } else if (token == "^") {
    70           // handle integer powers directly
    71           FormatRecursively(node.Subtrees.First(), strBuilder, numberFormat, formatString, parameters);
    72 
    73           var power = node.GetSubtree(1);
    74           if (power is INumericTreeNode numNode && Math.Truncate(numNode.Value) == numNode.Value) {
    75             strBuilder.Append(" ").Append(token).Append(" ").Append(numNode.Value.ToString(formatString, numberFormat));
    76           } else {
    77             strBuilder.Append(" ").Append(token).Append(" ");
    78             FormatRecursively(power, strBuilder, numberFormat, formatString, parameters);
    79           }
    80         } else {
    81           // function with multiple arguments
    82           strBuilder.Append(token).Append("(");
    83           FormatRecursively(node.Subtrees.First(), strBuilder, numberFormat, formatString, parameters);
    84           foreach (var subtree in node.Subtrees.Skip(1)) {
    85             strBuilder.Append(", ");
    86             FormatRecursively(subtree, strBuilder, numberFormat, formatString, parameters);
    87           }
    88 
    89           strBuilder.Append(")");
    90         }
    91       } else if (node.SubtreeCount == 1) {
    92         var token = GetToken(node.Symbol);
    93         if (token == "-" || token == "NOT") {
    94           strBuilder.Append(token);
    95           FormatRecursively(node.GetSubtree(0), strBuilder, numberFormat, formatString, parameters);
    96         } else if (token == "/") {
    97           strBuilder.Append("1/");
    98           FormatRecursively(node.GetSubtree(0), strBuilder, numberFormat, formatString, parameters);
    99         } else if (token == "+" || token == "*") {
    100           FormatRecursively(node.GetSubtree(0), strBuilder, numberFormat, formatString, parameters);
    101         } else {
    102           // function with only one argument
    103           strBuilder.Append(token).Append("(");
    104           FormatRecursively(node.GetSubtree(0), strBuilder, numberFormat, formatString, parameters);
    105           strBuilder.Append(")");
    106         }
    107       } else {
    108         // no subtrees
    109         if (node.Symbol is LaggedVariable) {
     120        } else if (node.Symbol is LaggedVariable) {
    110121          var varNode = node as LaggedVariableTreeNode;
    111122          if (!varNode.Weight.IsAlmost(1.0)) {
     
    119130                    .AppendFormat(numberFormat, "{0}", varNode.Lag)
    120131                    .Append(")");
    121         } else if (node.Symbol is Variable) {
    122           var varNode = node as VariableTreeNode;
    123           if (!varNode.Weight.IsAlmost(1.0)) {
    124             AppendNumber(strBuilder, parameters, varNode.Weight, formatString, numberFormat);
    125             strBuilder.Append("*");
    126           }
    127           AppendVariableName(strBuilder, varNode.VariableName);
    128132        } else if (node.Symbol is FactorVariable) {
    129133          var factorNode = node as FactorVariableTreeNode;
     
    145149          strBuilder.Append(" = ");
    146150          AppendVariableName(strBuilder, factorNode.VariableValue);
    147         } else if (node is INumericTreeNode numNode) {
    148           if (parameters == null && numNode.Value < 0) {
    149             // negative value
    150             strBuilder.Append(numNode.Value.ToString(formatString, numberFormat));
    151           } else {
    152             AppendNumber(strBuilder, parameters, numNode.Value, formatString, numberFormat);
    153           }
    154151        }
    155       }
    156     }
    157 
    158     private static bool IsOperator(ISymbol sy) {
    159       return sy is Addition ||
    160         sy is Subtraction ||
    161         sy is Multiplication ||
    162         sy is Division ||
    163         sy is And ||
    164         sy is Or ||
    165         sy is Xor;
     152      } else if (node.SubtreeCount == 1) {
     153        // only functions and single-argument subtraction (=negation) or NOT are possible here.
     154        var token = GetToken(node.Symbol);
     155        // the only operators that are allowed with a single argument
     156        if (node.Symbol is Subtraction || node.Symbol is Not) {
     157          if (RequiresParenthesis(node.Parent, node)) {
     158            strBuilder.Append("(");
     159          }
     160          strBuilder.Append(token);
     161          FormatRecursively(node.GetSubtree(0), strBuilder, numberFormat, formatString, parameters);
     162          if (RequiresParenthesis(node.Parent, node)) {
     163            strBuilder.Append(")");
     164          }
     165        } else if (IsOperator(node.Symbol)) {
     166          throw new FormatException("Single-argument version of " + node.Symbol.Name + " is not supported.");
     167        } else {
     168          // function with only one argument
     169          strBuilder.Append(token);
     170          strBuilder.Append("(");
     171          FormatRecursively(node.GetSubtree(0), strBuilder, numberFormat, formatString, parameters);
     172          strBuilder.Append(")");
     173        }
     174      } else if (node.SubtreeCount > 1) {
     175        var token = GetToken(node.Symbol);
     176        // operators
     177        if (IsOperator(node.Symbol)) {
     178          var parenthesisRequired = RequiresParenthesis(node.Parent, node);
     179          if (parenthesisRequired) strBuilder.Append("(");
     180          FormatRecursively(node.Subtrees.First(), strBuilder, numberFormat, formatString, parameters);
     181
     182          foreach (var subtree in node.Subtrees.Skip(1)) {
     183            strBuilder.Append(" ").Append(token).Append(" ");
     184            FormatRecursively(subtree, strBuilder, numberFormat, formatString, parameters);
     185          }
     186
     187          if (parenthesisRequired) strBuilder.Append(")");
     188        } else {
     189          // function with multiple arguments (AQ)
     190
     191          strBuilder.Append(token);
     192          strBuilder.Append("(");
     193
     194          FormatRecursively(node.Subtrees.First(), strBuilder, numberFormat, formatString, parameters);
     195          foreach (var subtree in node.Subtrees.Skip(1)) {
     196            strBuilder.Append(", ");
     197            FormatRecursively(subtree, strBuilder, numberFormat, formatString, parameters);
     198          }
     199          strBuilder.Append(")");
     200        }
     201      }
     202    }
     203
     204    private static int Priority(ISymbol symbol) {
     205      if (symbol is Addition || symbol is Subtraction || symbol is Or || symbol is Xor) return 1;
     206      if (symbol is Division || symbol is Multiplication || symbol is And) return 2;
     207      if (symbol is Power || symbol is Not) return 3;
     208      throw new NotSupportedException();
     209    }
     210
     211    private static bool RequiresParenthesis(ISymbolicExpressionTreeNode parent, ISymbolicExpressionTreeNode child) {
     212      if (child.SubtreeCount > 2 && IsOperator(child.Symbol)) throw new NotSupportedException("Infix formatter does not support operators with more than two children.");
     213
     214      // Basically: We need a parenthesis for child if the parent symbol binds stronger than child symbol.
     215      if (parent.Symbol == null || parent.Symbol is ProgramRootSymbol || parent.Symbol is StartSymbol) return false;
     216      if (IsFunction(parent.Symbol)) return false;
     217      if (parent.SubtreeCount == 1 && (parent.Symbol is Subtraction)) return true;
     218      if (child.SubtreeCount == 0) return false;
     219      var parentPrio = Priority(parent.Symbol);
     220      var childPrio = Priority(child.Symbol);
     221      if (parentPrio > childPrio) return true;
     222      else if (parentPrio == childPrio) {
     223        if (IsLeftAssocOp(child.Symbol)) return parent.GetSubtree(0) != child; // (..) required only for right child for left-assoc op
     224        if (IsRightAssocOp(child.Symbol)) return parent.GetSubtree(1) != child;
     225      }
     226      return false;
     227    }
     228
     229    private static bool IsFunction(ISymbol symbol) {
     230      // functions are formatted in prefix form e.g. sin(...)
     231      return !IsOperator(symbol) && !IsLeaf(symbol);
     232    }
     233
     234    private static bool IsLeaf(ISymbol symbol) {
     235      return symbol.MaximumArity == 0;
     236    }
     237
     238    private static bool IsOperator(ISymbol symbol) {
     239      return IsLeftAssocOp(symbol) || IsRightAssocOp(symbol);
     240    }
     241
     242    private static bool IsAssocOp(ISymbol symbol) {
     243      // (a <op> b) <op> c = a <op> (b <op> c)
     244      return symbol is Addition ||
     245        symbol is Multiplication ||
     246        symbol is And ||
     247        symbol is Or ||
     248        symbol is Xor;
     249    }
     250
     251    private static bool IsLeftAssocOp(ISymbol symbol) {
     252      // a <op> b <op> c = (a <op> b) <op> c
     253      return symbol is Addition ||
     254        symbol is Subtraction ||
     255        symbol is Multiplication ||
     256        symbol is Division ||
     257        symbol is And ||
     258        symbol is Or ||
     259        symbol is Xor;
     260    }
     261
     262    private static bool IsRightAssocOp(ISymbol symbol) {
     263      // a <op> b <op> c = a <op> (b <op> c)
     264      // Negation (single-argument subtraction) is also right-assoc, but we do not have a separate symbol for negation.
     265      return symbol is Not ||
     266             symbol is Power; // x^y^z = x^(y^z) (as in Fortran or Mathematica)
    166267    }
    167268
     
    224325      // skip root and start symbols
    225326      StringBuilder strBuilder = new StringBuilder();
    226       BaseInfixExpressionFormatter.FormatRecursively(symbolicExpressionTree.Root.GetSubtree(0).GetSubtree(0),
     327      var cleanTree = (ISymbolicExpressionTree)symbolicExpressionTree.Clone();
     328      BaseInfixExpressionFormatter.ConvertToBinaryLeftAssoc(cleanTree);
     329      BaseInfixExpressionFormatter.FormatRecursively(cleanTree.Root.GetSubtree(0).GetSubtree(0),
    227330        strBuilder, numberFormat, formatString);
    228331      return strBuilder.ToString();
     
    235338
    236339  [StorableType("54D917E8-134E-4066-9A60-2737C12D81DC")]
    237   [Item("Infix String Formater", "Formatter for symbolic expressions, which produces an infix expression " +
     340  [Item("Infix String Formatter", "Formatter for symbolic expressions, which produces an infix expression " +
    238341                                 "as well as a list of all coefficient values")]
    239342  public sealed class InfixExpressionStringFormatter : NamedItem, ISymbolicExpressionTreeStringFormatter {
     
    255358      StringBuilder strBuilder = new StringBuilder();
    256359      var parameters = new List<KeyValuePair<string, double>>();
    257       BaseInfixExpressionFormatter.FormatRecursively(symbolicExpressionTree.Root.GetSubtree(0).GetSubtree(0),
     360      var cleanTree = (ISymbolicExpressionTree)symbolicExpressionTree.Clone();
     361      BaseInfixExpressionFormatter.ConvertToBinaryLeftAssoc(cleanTree);
     362      BaseInfixExpressionFormatter.FormatRecursively(cleanTree.Root.GetSubtree(0).GetSubtree(0),
    258363        strBuilder, NumberFormatInfo.InvariantInfo, "G", parameters);
    259364      strBuilder.Append($"{Environment.NewLine}{Environment.NewLine}");
  • trunk/HeuristicLab.Tests/HeuristicLab.Problems.DataAnalysis.Symbolic-3.4/InfixExpressionParserTest.cs

    r18203 r18211  
    2222
    2323using System;
     24using System.Linq;
    2425using HeuristicLab.Encodings.SymbolicExpressionTreeEncoding;
    2526using Microsoft.VisualStudio.TestTools.UnitTesting;
     
    3738      Assert.AreEqual("3", formatter.Format(parser.Parse("3")));
    3839      Assert.AreEqual("3 * 3", formatter.Format(parser.Parse("3*3")));
    39       Assert.AreEqual("3 * 4",formatter.Format(parser.Parse("3 * 4")));
    40       Assert.AreEqual("0.123",formatter.Format(parser.Parse("123E-03")));
    41       Assert.AreEqual("0.123",formatter.Format(parser.Parse("123e-03")));
    42       Assert.AreEqual("123000",formatter.Format(parser.Parse("123e+03")));
    43       Assert.AreEqual("123000",formatter.Format(parser.Parse("123E+03")));
    44       Assert.AreEqual("0.123",formatter.Format(parser.Parse("123.0E-03")));
    45       Assert.AreEqual("0.123",formatter.Format(parser.Parse("123.0e-03")));
    46       Assert.AreEqual("123000",formatter.Format(parser.Parse("123.0e+03")));
    47       Assert.AreEqual("123000",formatter.Format(parser.Parse("123.0E+03")));
    48       Assert.AreEqual("0.123",formatter.Format(parser.Parse("123.0E-3")));
    49       Assert.AreEqual("0.123",formatter.Format(parser.Parse("123.0e-3")));
    50       Assert.AreEqual("123000",formatter.Format(parser.Parse("123.0e+3")));
    51       Assert.AreEqual("123000",formatter.Format(parser.Parse("123.0E+3")));
    52 
    53       Assert.AreEqual("3.1415 + 2",formatter.Format(parser.Parse("3.1415+2.0")));
    54       Assert.AreEqual("3.1415 / 2",formatter.Format(parser.Parse("3.1415/2.0")));
    55       Assert.AreEqual("3.1415 * 2",formatter.Format(parser.Parse("3.1415*2.0")));
     40      Assert.AreEqual("3 * 4", formatter.Format(parser.Parse("3 * 4")));
     41      Assert.AreEqual("0.123", formatter.Format(parser.Parse("123E-03")));
     42      Assert.AreEqual("0.123", formatter.Format(parser.Parse("123e-03")));
     43      Assert.AreEqual("123000", formatter.Format(parser.Parse("123e+03")));
     44      Assert.AreEqual("123000", formatter.Format(parser.Parse("123E+03")));
     45      Assert.AreEqual("0.123", formatter.Format(parser.Parse("123.0E-03")));
     46      Assert.AreEqual("0.123", formatter.Format(parser.Parse("123.0e-03")));
     47      Assert.AreEqual("123000", formatter.Format(parser.Parse("123.0e+03")));
     48      Assert.AreEqual("123000", formatter.Format(parser.Parse("123.0E+03")));
     49      Assert.AreEqual("0.123", formatter.Format(parser.Parse("123.0E-3")));
     50      Assert.AreEqual("0.123", formatter.Format(parser.Parse("123.0e-3")));
     51      Assert.AreEqual("123000", formatter.Format(parser.Parse("123.0e+3")));
     52      Assert.AreEqual("123000", formatter.Format(parser.Parse("123.0E+3")));
     53
     54      Assert.AreEqual("3.1415 + 2", formatter.Format(parser.Parse("3.1415+2.0")));
     55      Assert.AreEqual("3.1415 / 2", formatter.Format(parser.Parse("3.1415/2.0")));
     56      Assert.AreEqual("3.1415 * 2", formatter.Format(parser.Parse("3.1415*2.0")));
    5657      Assert.AreEqual("3.1415 - 2", formatter.Format(parser.Parse("3.1415-2.0")));
    5758      // round-trip
    58       Assert.AreEqual("3.1415 - 2",formatter.Format(parser.Parse(formatter.Format(parser.Parse("3.1415-2.0")))));
    59       Assert.AreEqual("3.1415 + 2",formatter.Format(parser.Parse("3.1415+(2.0)")));
     59      Assert.AreEqual("3.1415 - 2", formatter.Format(parser.Parse(formatter.Format(parser.Parse("3.1415-2.0")))));
     60      Assert.AreEqual("3.1415 + 2", formatter.Format(parser.Parse("3.1415+(2.0)")));
    6061      Assert.AreEqual("3.1415 + 2", formatter.Format(parser.Parse("(3.1415+(2.0))")));
    6162
    6263
    63       Assert.AreEqual("LOG(3)",formatter.Format(parser.Parse("log(3)")));
    64       Assert.AreEqual("LOG(-3)",formatter.Format(parser.Parse("log(-3)")));
    65       Assert.AreEqual("EXP(3)",formatter.Format(parser.Parse("exp(3)")));
    66       Assert.AreEqual("EXP(-3)",formatter.Format(parser.Parse("exp(-3)")));
     64      Assert.AreEqual("LOG(3)", formatter.Format(parser.Parse("log(3)")));
     65      Assert.AreEqual("LOG(-3)", formatter.Format(parser.Parse("log(-3)")));
     66      Assert.AreEqual("EXP(3)", formatter.Format(parser.Parse("exp(3)")));
     67      Assert.AreEqual("EXP(-3)", formatter.Format(parser.Parse("exp(-3)")));
    6768      Assert.AreEqual("SQRT(3)", formatter.Format(parser.Parse("sqrt(3)")));
    6869
    6970      Assert.AreEqual("SQR(-3)", formatter.Format(parser.Parse("sqr((-3))")));
    7071
    71       Assert.AreEqual("3 / 3 + 2 / 2 + 1 / 1",formatter.Format(parser.Parse("3/3+2/2+1/1")));
     72      Assert.AreEqual("3 / 3 + 2 / 2 + 1 / 1", formatter.Format(parser.Parse("3/3+2/2+1/1")));
    7273      Assert.AreEqual("-3 + 30 - 2 + 20 - 1 + 10", formatter.Format(parser.Parse("-3+30-2+20-1+10")));
    7374
     
    7980
    8081      // signed variables / constants
    81       Assert.AreEqual("-1*'x1'", formatter.Format(parser.Parse("-x1")));
     82      Assert.AreEqual("-1 * 'x1'", formatter.Format(parser.Parse("-x1")));
    8283      Assert.AreEqual("1", formatter.Format(parser.Parse("--1.0")));
    8384      Assert.AreEqual("1", formatter.Format(parser.Parse("----1.0")));
     
    8788
    8889
    89       Assert.AreEqual("'x1'",formatter.Format(parser.Parse("\"x1\"")));
    90       Assert.AreEqual("'var name'",formatter.Format(parser.Parse("\'var name\'")));
    91       Assert.AreEqual("'var name'",formatter.Format(parser.Parse("\"var name\"")));
    92       Assert.AreEqual("'1'",formatter.Format(parser.Parse("\"1\"")));
    93 
    94       Assert.AreEqual("'var \" name'",formatter.Format(parser.Parse("'var \" name\'")));
     90      Assert.AreEqual("'x1'", formatter.Format(parser.Parse("\"x1\"")));
     91      Assert.AreEqual("'var name'", formatter.Format(parser.Parse("\'var name\'")));
     92      Assert.AreEqual("'var name'", formatter.Format(parser.Parse("\"var name\"")));
     93      Assert.AreEqual("'1'", formatter.Format(parser.Parse("\"1\"")));
     94
     95      Assert.AreEqual("'var \" name'", formatter.Format(parser.Parse("'var \" name\'")));
    9596      Assert.AreEqual("\"var ' name\"", formatter.Format(parser.Parse("\"var \' name\"")));
    9697
    9798
    98       Assert.AreEqual("'x1' * 'x2'",formatter.Format(parser.Parse("\"x1\"*\"x2\"")));
    99       Assert.AreEqual("'x1' * 'x2' + 'x3' * 'x4'",formatter.Format(parser.Parse("\"x1\"*\"x2\"+\"x3\"*\"x4\"")));
     99      Assert.AreEqual("'x1' * 'x2'", formatter.Format(parser.Parse("\"x1\"*\"x2\"")));
     100      Assert.AreEqual("'x1' * 'x2' + 'x3' * 'x4'", formatter.Format(parser.Parse("\"x1\"*\"x2\"+\"x3\"*\"x4\"")));
    100101      Assert.AreEqual("'x1' * 'x2' + 'x3' * 'x4'", formatter.Format(parser.Parse("x1*x2+x3*x4")));
    101102
    102103
    103       Assert.AreEqual("3 ^ 2",formatter.Format(parser.Parse("POW(3, 2)")));
    104       Assert.AreEqual("3.1 ^ 2.1",formatter.Format(parser.Parse("POW(3.1, 2.1)")));
    105       Assert.AreEqual("3.1 ^ 2.1",formatter.Format(parser.Parse("POW(3.1 , 2.1)")));
    106       Assert.AreEqual("3.1 ^ 2.1",formatter.Format(parser.Parse("POW(3.1 ,2.1)")));
    107       Assert.AreEqual("-3.1 ^ -2.1",formatter.Format(parser.Parse("POW(-3.1 , - 2.1)")));
    108       Assert.AreEqual("ROOT(3, 2)",formatter.Format(parser.Parse("ROOT(3, 2)")));
    109       Assert.AreEqual("ROOT(3.1, 2.1)",formatter.Format(parser.Parse("ROOT(3.1, 2.1)")));
    110       Assert.AreEqual("ROOT(3.1, 2.1)",formatter.Format(parser.Parse("ROOT(3.1 , 2.1)")));
    111       Assert.AreEqual("ROOT(3.1, 2.1)",formatter.Format(parser.Parse("ROOT(3.1 ,2.1)")));
     104      Assert.AreEqual("3 ^ 2", formatter.Format(parser.Parse("POW(3, 2)")));
     105      Assert.AreEqual("3.1 ^ 2.1", formatter.Format(parser.Parse("POW(3.1, 2.1)")));
     106      Assert.AreEqual("3.1 ^ 2.1", formatter.Format(parser.Parse("POW(3.1 , 2.1)")));
     107      Assert.AreEqual("3.1 ^ 2.1", formatter.Format(parser.Parse("POW(3.1 ,2.1)")));
     108      Assert.AreEqual("-3.1 ^ -2.1", formatter.Format(parser.Parse("POW(-3.1 , - 2.1)")));
     109      Assert.AreEqual("ROOT(3, 2)", formatter.Format(parser.Parse("ROOT(3, 2)")));
     110      Assert.AreEqual("ROOT(3.1, 2.1)", formatter.Format(parser.Parse("ROOT(3.1, 2.1)")));
     111      Assert.AreEqual("ROOT(3.1, 2.1)", formatter.Format(parser.Parse("ROOT(3.1 , 2.1)")));
     112      Assert.AreEqual("ROOT(3.1, 2.1)", formatter.Format(parser.Parse("ROOT(3.1 ,2.1)")));
    112113      Assert.AreEqual("ROOT(-3.1, -2.1)", formatter.Format(parser.Parse("ROOT(-3.1 , -2.1)")));
    113114
    114       Assert.AreEqual("IF(GT(0, 1), 1, 0)",formatter.Format(parser.Parse("IF(GT( 0, 1), 1, 0)")));
    115       Assert.AreEqual("IF(LT(0, 1), 1, 0)",formatter.Format(parser.Parse("IF(LT(0,1), 1 , 0)")));
    116       Assert.AreEqual("LAG('x', 1)",formatter.Format(parser.Parse("LAG(x, 1)")));
    117       Assert.AreEqual("LAG('x', -1)",formatter.Format(parser.Parse("LAG(x, -1)")));
    118       Assert.AreEqual("LAG('x', 1)",formatter.Format(parser.Parse("LAG(x, +1)")));
     115      Assert.AreEqual("IF(GT(0, 1), 1, 0)", formatter.Format(parser.Parse("IF(GT( 0, 1), 1, 0)")));
     116      Assert.AreEqual("IF(LT(0, 1), 1, 0)", formatter.Format(parser.Parse("IF(LT(0,1), 1 , 0)")));
     117      Assert.AreEqual("LAG('x', 1)", formatter.Format(parser.Parse("LAG(x, 1)")));
     118      Assert.AreEqual("LAG('x', -1)", formatter.Format(parser.Parse("LAG(x, -1)")));
     119      Assert.AreEqual("LAG('x', 1)", formatter.Format(parser.Parse("LAG(x, +1)")));
    119120      Assert.AreEqual("'x' * LAG('x', 1)", formatter.Format(parser.Parse("x * LAG('x', +1)")));
    120121
    121122      // factor variables
    122       Assert.AreEqual("'x'[1] * 'y'",formatter.Format(parser.Parse("x [1.0] * y")));
    123       Assert.AreEqual("'x'[1, 2] * 'y'[1, 2]",formatter.Format(parser.Parse("x [1.0, 2.0] * y [1.0, 2.0]")));
    124       Assert.AreEqual("'x'[1] * 'y'",formatter.Format(parser.Parse("x[1] * y")));
    125       Assert.AreEqual("'x'[1, 2] * 'y'[1, 2]",formatter.Format(parser.Parse("x[1, 2] * y [1, 2]")));
    126       Assert.AreEqual("'x'[1] * 'y'",formatter.Format(parser.Parse("x [+1.0] * y")));
    127       Assert.AreEqual("'x'[-1] * 'y'",formatter.Format(parser.Parse("x [-1.0] * y")));
     123      Assert.AreEqual("'x'[1] * 'y'", formatter.Format(parser.Parse("x [1.0] * y")));
     124      Assert.AreEqual("'x'[1, 2] * 'y'[1, 2]", formatter.Format(parser.Parse("x [1.0, 2.0] * y [1.0, 2.0]")));
     125      Assert.AreEqual("'x'[1] * 'y'", formatter.Format(parser.Parse("x[1] * y")));
     126      Assert.AreEqual("'x'[1, 2] * 'y'[1, 2]", formatter.Format(parser.Parse("x[1, 2] * y [1, 2]")));
     127      Assert.AreEqual("'x'[1] * 'y'", formatter.Format(parser.Parse("x [+1.0] * y")));
     128      Assert.AreEqual("'x'[-1] * 'y'", formatter.Format(parser.Parse("x [-1.0] * y")));
    128129      Assert.AreEqual("'x'[-1, -2] * 'y'[1, 2]", formatter.Format(parser.Parse("x [-1.0, -2.0] * y [+1.0, +2.0]")));
    129130
    130131      // one-hot for factor
    131       Assert.AreEqual("'x' = 'val' * 'y'",formatter.Format(parser.Parse("x='val' * y")));
    132       Assert.AreEqual("'x' = 'val'",formatter.Format(parser.Parse("x = 'val'")));
    133       Assert.AreEqual("'x' = 'val'",formatter.Format(parser.Parse("x = \"val\"")));
    134       Assert.AreEqual("1 * 'x' = 'val'",formatter.Format(parser.Parse("1.0 * x = val")));
    135       Assert.AreEqual("-1 * 'x' = 'val'",formatter.Format(parser.Parse("-1.0 * x = val")));
     132      Assert.AreEqual("'x' = 'val' * 'y'", formatter.Format(parser.Parse("x='val' * y")));
     133      Assert.AreEqual("'x' = 'val'", formatter.Format(parser.Parse("x = 'val'")));
     134      Assert.AreEqual("'x' = 'val'", formatter.Format(parser.Parse("x = \"val\"")));
     135      Assert.AreEqual("1 * 'x' = 'val'", formatter.Format(parser.Parse("1.0 * x = val")));
     136      Assert.AreEqual("-1 * 'x' = 'val'", formatter.Format(parser.Parse("-1.0 * x = val")));
    136137      Assert.AreEqual("1 * 'x' = 'val1' + 'y' = 'val2'", formatter.Format(parser.Parse("+1.0 * \"x\" = val1 + y = \"val2\"")));
    137138
     
    143144      Assert.AreEqual("-1", formatter.Format(parser.Parse("< num =-1.0>")));
    144145      Assert.AreEqual("-1", formatter.Format(parser.Parse("< num = - 1.0>")));
    145      
     146
    146147      // numeric parameter with sign
    147148      Assert.AreEqual("1", formatter.Format(parser.Parse("-<num=-1.0>")));
     
    151152
    152153      {
    153         // a tree with single-arity multiplication and addition
     154        // a tree with single-argument multiplication and addition
    154155        //   ...
    155156        //    *
     
    173174        var t = new SymbolicExpressionTree(root);
    174175
    175         Assert.AreEqual("('x1' + 'x2')", formatter.Format(t)); // TODO parenthesis not strictly required here
     176        Assert.AreEqual("'x1' + 'x2'", formatter.Format(t));
    176177      }
    177178      {
     
    230231        var t = new SymbolicExpressionTree(root);
    231232
    232         Assert.AreEqual("SIN(('x1' + 'x2'))", formatter.Format(t)); // TODO would be better to prevent double parenthesis here
     233        Assert.AreEqual("SIN('x1' + 'x2')", formatter.Format(t));
     234      }
     235      {
     236        // single-argument subtraction
     237        //    -
     238        //    |
     239        //    v1
     240        var root = new ProgramRootSymbol().CreateTreeNode();
     241        var start = new StartSymbol().CreateTreeNode();
     242        var sub = new Subtraction().CreateTreeNode();
     243        var var1 = (VariableTreeNode)new Variable().CreateTreeNode(); var1.VariableName = "x1"; var1.Weight = -1.0;
     244        sub.AddSubtree(var1);
     245        start.AddSubtree(sub);
     246        root.AddSubtree(start);
     247        var t = new SymbolicExpressionTree(root);
     248
     249        Assert.AreEqual("-(-1 * 'x1')", formatter.Format(t));       // TODO: same as --1 * 'x1' and just 'x1'
     250      }
     251      {
     252        // single-argument subtraction
     253        //    -
     254        //    |
     255        //    +
     256        //   / \
     257        //  v1 v2
     258        var root = new ProgramRootSymbol().CreateTreeNode();
     259        var start = new StartSymbol().CreateTreeNode();
     260        var sub = new Subtraction().CreateTreeNode();
     261        var add = new Addition().CreateTreeNode();
     262        var var1 = (VariableTreeNode)new Variable().CreateTreeNode(); var1.VariableName = "x1"; var1.Weight = 1.0;
     263        var var2 = (VariableTreeNode)new Variable().CreateTreeNode(); var2.VariableName = "x2"; var2.Weight = 1.0;
     264        add.AddSubtree(var1);
     265        add.AddSubtree(var2);
     266        sub.AddSubtree(add);
     267        start.AddSubtree(sub);
     268        root.AddSubtree(start);
     269        var t = new SymbolicExpressionTree(root);
     270
     271        Assert.AreEqual("-('x1' + 'x2')", formatter.Format(t));
     272      }
     273      {
     274        //     ^
     275        //    / \
     276        //    *  v3
     277        //   / \
     278        //  v1 v2
     279        var root = new ProgramRootSymbol().CreateTreeNode();
     280        var start = new StartSymbol().CreateTreeNode();
     281        var pow = new Power().CreateTreeNode();
     282        var mul = new Multiplication().CreateTreeNode();
     283        var var1 = (VariableTreeNode)new Variable().CreateTreeNode(); var1.VariableName = "x1"; var1.Weight = 1.0;
     284        var var2 = (VariableTreeNode)new Variable().CreateTreeNode(); var2.VariableName = "x2"; var2.Weight = 1.0;
     285        var var3 = (VariableTreeNode)new Variable().CreateTreeNode(); var3.VariableName = "x3"; var3.Weight = 1.0;
     286        mul.AddSubtree(var1);
     287        mul.AddSubtree(var2);
     288        pow.AddSubtree(mul);
     289        pow.AddSubtree(var3);
     290        start.AddSubtree(pow);
     291        root.AddSubtree(start);
     292        var t = new SymbolicExpressionTree(root);
     293
     294        Assert.AreEqual("('x1' * 'x2') ^ 'x3'", formatter.Format(t));
     295      }
     296      {
     297        //       ^
     298        //     /   \
     299        //    *     *
     300        //   / \   / \
     301        //  v1 v2 v3 v4
     302        var root = new ProgramRootSymbol().CreateTreeNode();
     303        var start = new StartSymbol().CreateTreeNode();
     304        var pow = new Power().CreateTreeNode();
     305        var mul1 = new Multiplication().CreateTreeNode();
     306        var mul2 = new Multiplication().CreateTreeNode();
     307        var var1 = (VariableTreeNode)new Variable().CreateTreeNode(); var1.VariableName = "x1"; var1.Weight = 1.0;
     308        var var2 = (VariableTreeNode)new Variable().CreateTreeNode(); var2.VariableName = "x2"; var2.Weight = 1.0;
     309        var var3 = (VariableTreeNode)new Variable().CreateTreeNode(); var3.VariableName = "x3"; var3.Weight = 1.0;
     310        var var4 = (VariableTreeNode)new Variable().CreateTreeNode(); var4.VariableName = "x4"; var4.Weight = 1.0;
     311        mul1.AddSubtree(var1);
     312        mul1.AddSubtree(var2);
     313        mul2.AddSubtree(var3);
     314        mul2.AddSubtree(var4);
     315        pow.AddSubtree(mul1);
     316        pow.AddSubtree(mul2);
     317        start.AddSubtree(pow);
     318        root.AddSubtree(start);
     319        var t = new SymbolicExpressionTree(root);
     320
     321        Assert.AreEqual("('x1' * 'x2') ^ ('x3' * 'x4')", formatter.Format(t));
     322      }
     323      {
     324        //       *
     325        //     /   \
     326        //    *     /
     327        //   / \   / \
     328        //  v1 v2 v3 v4
     329        var root = new ProgramRootSymbol().CreateTreeNode();
     330        var start = new StartSymbol().CreateTreeNode();
     331        var mul1 = new Multiplication().CreateTreeNode();
     332        var mul2 = new Multiplication().CreateTreeNode();
     333        var div = new Division().CreateTreeNode();
     334        var var1 = (VariableTreeNode)new Variable().CreateTreeNode(); var1.VariableName = "x1"; var1.Weight = 1.0;
     335        var var2 = (VariableTreeNode)new Variable().CreateTreeNode(); var2.VariableName = "x2"; var2.Weight = 1.0;
     336        var var3 = (VariableTreeNode)new Variable().CreateTreeNode(); var3.VariableName = "x3"; var3.Weight = 1.0;
     337        var var4 = (VariableTreeNode)new Variable().CreateTreeNode(); var4.VariableName = "x4"; var4.Weight = 1.0;
     338        mul2.AddSubtree(var1);
     339        mul2.AddSubtree(var2);
     340        div.AddSubtree(var3);
     341        div.AddSubtree(var4);
     342        mul1.AddSubtree(mul2);
     343        mul1.AddSubtree(div);
     344        start.AddSubtree(mul1);
     345        root.AddSubtree(start);
     346        var t = new SymbolicExpressionTree(root);
     347
     348        Assert.AreEqual("'x1' * 'x2' * 'x3' / 'x4'", formatter.Format(t));     // same as x1 * x2 * (x3 / x4)
     349      }
     350      {
     351        //       *
     352        //     /   \
     353        //    div  div
     354        //   / \   / \
     355        //  v1 v2 v3 v4
     356        var root = new ProgramRootSymbol().CreateTreeNode();
     357        var start = new StartSymbol().CreateTreeNode();
     358        var mul = new Multiplication().CreateTreeNode();
     359        var div1 = new Division().CreateTreeNode();
     360        var div2 = new Division().CreateTreeNode();
     361        var var1 = (VariableTreeNode)new Variable().CreateTreeNode(); var1.VariableName = "x1"; var1.Weight = 1.0;
     362        var var2 = (VariableTreeNode)new Variable().CreateTreeNode(); var2.VariableName = "x2"; var2.Weight = 1.0;
     363        var var3 = (VariableTreeNode)new Variable().CreateTreeNode(); var3.VariableName = "x3"; var3.Weight = 1.0;
     364        var var4 = (VariableTreeNode)new Variable().CreateTreeNode(); var4.VariableName = "x4"; var4.Weight = 1.0;
     365        div1.AddSubtree(var1);
     366        div1.AddSubtree(var2);
     367        div2.AddSubtree(var3);
     368        div2.AddSubtree(var4);
     369        mul.AddSubtree(div1);
     370        mul.AddSubtree(div2);
     371        start.AddSubtree(mul);
     372        root.AddSubtree(start);
     373        var t = new SymbolicExpressionTree(root);
     374
     375        Assert.AreEqual("'x1' / 'x2' * 'x3' / 'x4'", formatter.Format(t)); // same as x1 / x2 * (x3 / x4)
     376      }
     377      {
     378        //      div
     379        //     /   \
     380        //    div   *
     381        //   / \   / \
     382        //  v1 v2 v3 v4
     383        //         
     384        var root = new ProgramRootSymbol().CreateTreeNode();
     385        var start = new StartSymbol().CreateTreeNode();
     386        var mul = new Multiplication().CreateTreeNode();
     387        var div2 = new Division().CreateTreeNode();
     388        var div1 = new Division().CreateTreeNode();
     389        var var1 = (VariableTreeNode)new Variable().CreateTreeNode(); var1.VariableName = "x1"; var1.Weight = 1.0;
     390        var var2 = (VariableTreeNode)new Variable().CreateTreeNode(); var2.VariableName = "x2"; var2.Weight = 1.0;
     391        var var3 = (VariableTreeNode)new Variable().CreateTreeNode(); var3.VariableName = "x3"; var3.Weight = 1.0;
     392        var var4 = (VariableTreeNode)new Variable().CreateTreeNode(); var4.VariableName = "x4"; var4.Weight = 1.0;
     393        div2.AddSubtree(var1);
     394        div2.AddSubtree(var2);
     395        mul.AddSubtree(var3);
     396        mul.AddSubtree(var4);
     397        div1.AddSubtree(div2);
     398        div1.AddSubtree(mul);
     399        start.AddSubtree(div1);
     400        root.AddSubtree(start);
     401        var t = new SymbolicExpressionTree(root);
     402
     403        Assert.AreEqual("'x1' / 'x2' / ('x3' * 'x4')", formatter.Format(t));
     404      }
     405
     406      {
     407        // check random trees after formatting & parsing again
     408
     409        var rand = new Random.MersenneTwister(1234);
     410        var interpreter = new SymbolicDataAnalysisExpressionTreeInterpreter(); // use an interpreter that supports boolean functions
     411        var xy = new double[100, 4];
     412        for (int i = 0; i < xy.GetLength(0); i++)
     413          for (int j = 0; j < xy.GetLength(1); j++)
     414            xy[i, j] = rand.NextDouble();
     415
     416        var ds = new Dataset(new string[] { "a", "b", "c", "y" }, xy);
     417        var rows = Enumerable.Range(0, xy.GetLength(1)).ToArray();
     418        var grammar = new TypeCoherentExpressionGrammar();
     419        grammar.ConfigureAsDefaultRegressionGrammar();
     420        grammar.Symbols.OfType<Logarithm>().First().Enabled = true; // enable a function
     421        grammar.Symbols.OfType<Exponential>().First().Enabled = true; // enable a function
     422        grammar.Symbols.OfType<AnalyticQuotient>().First().Enabled = true; // enable a function with two arguments
     423        grammar.Symbols.OfType<Power>().First().Enabled = true; // another function with two arguments
     424        grammar.Symbols.OfType<And>().First().Enabled = true; // enable Boolean operators
     425        grammar.Symbols.OfType<Or>().First().Enabled = true;
     426        grammar.Symbols.OfType<Xor>().First().Enabled = true;
     427        grammar.Symbols.OfType<Not>().First().Enabled = true;
     428        grammar.Symbols.OfType<IfThenElse>().First().Enabled = true; // enable if-then-else function
     429        // test multi-argument versions of operators
     430        grammar.SetSubtreeCount(grammar.Symbols.OfType<Division>().First(), 1, 4);
     431        grammar.SetSubtreeCount(grammar.Symbols.OfType<Subtraction>().First(), 1, 2);
     432        grammar.SetSubtreeCount(grammar.Symbols.OfType<Addition>().First(), 1, 4);
     433        grammar.SetSubtreeCount(grammar.Symbols.OfType<Multiplication>().First(), 1, 4);
     434        grammar.SetSubtreeCount(grammar.Symbols.OfType<And>().First(), 1, 4);
     435        grammar.SetSubtreeCount(grammar.Symbols.OfType<Or>().First(), 1, 4);
     436        grammar.SetSubtreeCount(grammar.Symbols.OfType<Xor>().First(), 1, 4);
     437        grammar.ConfigureVariableSymbols(new RegressionProblemData(ds, new string[] { "a", "b", "c" }, "y"));
     438        var fmt = new SymbolicExpressionTreeStringFormatter();
     439        for (int i = 0; i < 100000; i++) {
     440          var t = ProbabilisticTreeCreator.Create(rand, grammar, 15, 8);
     441          var p1 = interpreter.GetSymbolicExpressionTreeValues(t, ds, rows).ToArray();
     442          var p2 = interpreter.GetSymbolicExpressionTreeValues(parser.Parse(formatter.Format(t)), ds, rows).ToArray(); // test formatter, and that parser can read the expression again, and that the evaluation is the same
     443          for (int j = 0; j < p1.Length; j++) {
     444            if (double.IsNaN(p1[j]) && double.IsNaN(p2[j])) continue;
     445            Assert.AreEqual(p1[j], p2[j], Math.Abs(p1[j] * 1e-6), $"Problem in formatted expression:\n{formatter.Format(t)}\n{fmt.Format(t)}");
     446          }
     447        }
    233448      }
    234449    }
Note: See TracChangeset for help on using the changeset viewer.