diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md
index c194d9725..b139f4ba3 100644
--- a/BREAKING-CHANGES.md
+++ b/BREAKING-CHANGES.md
@@ -15,6 +15,87 @@ read first.
---
+## Unreleased — since 2.3.0
+
+### At a glance
+
+| Silent? | What | Was | Is |
+|---|---|---|---|
+| **Silent** | `"a implies (b implies c)".ToEntity().Stringize()` | `a implies b implies c`, which reads back as `(a implies b) implies c` | `a implies (b implies c)` |
+| | `"(a implies b) implies c".ToEntity().Stringize()` | `(a implies b) implies c` | `a implies b implies c` |
+| **Silent** | `@"A \ (B \ C)".ToEntity().Stringize()`, and `Latexize` | `A \ B \ C` / `A \setminus B \setminus C` | `A \ (B \ C)` / `A \setminus \left(B \setminus C\right)` |
+| **Silent** | `@"A \ (B \/ C)".ToEntity().Stringize()`, and `Latexize` | `A \ B \/ C` / `A \setminus B \cup C` | `A \ (B \/ C)` / `A \setminus \left(B \cup C\right)` |
+| **Silent** | `@"A \/ (B \ C)".ToEntity().Stringize()`, and `Latexize` | `A \/ B \ C` / `A \cup B \setminus C` | `A \/ (B \ C)` / `A \cup \left(B \setminus C\right)` |
+| **Silent** | `"(x provided p) provided q".ToEntity().Stringize()`, and `Latexize` | `x provided p provided q`, which reads back as `x provided (p provided q)` | `(x provided p) provided q` |
+| **Silent** | `"a in (b in c)".ToEntity().Stringize()`, and `Latexize` | `a in b in c` / `a \in b \in c` | `a in (b in c)` / `a \in \left(b \in c\right)` |
+| **Silent** | `"x * (y mod z)".ToEntity().Stringize()`, and `Latexize` | `x * y mod z` / `x y \bmod z` | `x * (y mod z)` / `x \left(y \bmod z\right)` |
+| **Silent** | `"-1 * (y mod z)".ToEntity().Stringize()` | `-y mod z` | `-(y mod z)` |
+
+### A printed operator that is not associative keeps its brackets
+
+`Stringize` is the library's own input format: parsing what it prints has to give back the
+expression it printed, and that is what [`StringizeRoundTripTest`](Sources/Tests/UnitTests/Convenience/StringizeRoundTripTest.cs)
+enforces. Six operators broke it, five of them in the same way — the printer left the **right**
+operand unbracketed at its own precedence level, while the grammar folds that level to the left, so
+the printed text came back re-associated. The sixth, `provided`, is the mirror: it is the one infix
+operator the grammar folds to the **right**, so there it is the *left* operand that mis-associates.
+
+Four of the six are not associative, so the re-association changed the answer and not merely
+the shape. Measured on a build of 2.3.0 and a build of this branch:
+
+| input | 2.3.0 printed | 2.3.0 read that back as | value moved |
+|---|---|---|---|
+| `false implies (true implies false)` | `False implies True implies False` | `(False implies True) implies False` | `True` → `False` |
+| `{ 1, 2, 3 } \ ({ 2, 3 } \ { 3 })` | `{ 1, 2, 3 } \ { 2, 3 } \ { 3 }` | `({ 1, 2, 3 } \ { 2, 3 }) \ { 3 }` | `{ 1, 3 }` → `{ 1 }` |
+| `{ 1, 2 } \/ ({ 3 } \ { 1, 2 })` | `{ 1, 2 } \/ { 3 } \ { 1, 2 }` | `({ 1, 2 } \/ { 3 }) \ { 1, 2 }` | `{ 1, 2, 3 }` → `{ 3 }` |
+| `{ 1, 2 } \ ({ 2 } \/ { 3 })` | `{ 1, 2 } \ { 2 } \/ { 3 }` | `({ 1, 2 } \ { 2 }) \/ { 3 }` | `{ 1 }` → `{ 1, 3 }` |
+| `2 * (3 mod 2)` | `2 * 3 mod 2` | `(2 * 3) mod 2` | `2` → `0` |
+
+On this branch each of those prints its brackets and reads back as itself, so the value column
+no longer moves.
+
+`implies` also changes in the other direction, because its printer had the rule the wrong way
+round rather than missing: it bracketed the **assumption** — the operand that never needs it under
+a left fold — and not the conclusion. So `(a implies b) implies c`, which used to print as
+`(a implies b) implies c`, now prints flat as `a implies b implies c`. Both read back as the same
+expression; the brackets were redundant, and printing them is what made the genuinely ambiguous
+case look no different.
+
+`\/` and `mod` are bracketed only against the operator that makes them ambiguous, because both
+share a precedence level with an operator that is not associative while being associative
+themselves:
+
+- `A \/ (B \/ C)` still prints `A \/ B \/ C`; `A \/ (B \ C)` now prints `A \/ (B \ C)`.
+- `x * (y * z)` still prints `x * y * z` and `x * (y / z)` still prints `x * y / z`;
+ `x * (y mod z)` now prints `x * (y mod z)`.
+- `-1 * (y mod z)` printed `-y mod z` and now prints `-(y mod z)`.
+
+`provided` is bracketed on the other side, and it is the case where the value is *not* the test.
+`(x provided p) provided q` and `x provided (p provided q)` are both `x` exactly when `p` and `q`
+hold, so no answer moves — and they are different expressions, which is what the round trip is
+about. It printed flat and read back as the right-nested one.
+
+**What has not changed, deliberately.** An operator that *is* associative still prints flat, so
+`x + (y + z)` still prints `x + y + z`, and likewise for `*`, `and`, `or`, `xor`, `unite` and
+`intersect`. Those come back as a different `Entity` — a left-nested tree instead of
+a right-nested one — and as the same number, because the bracketing carries no mathematics. The
+alternative would print every expanded polynomial as a right-nested pile of parentheses:
+`"(a + b + c + d) ^ 2".ToEntity().Expand()` prints, on both versions,
+`d ^ 2 + 2 * c * d + c ^ 2 + 2 * b * d + 2 * b * c + b ^ 2 + 2 * a * d + 2 * a * c + 2 * a * b + a ^ 2`,
+and its sum is right-nested throughout. `StringizeRoundTripTest` pins that decision so that it
+stays one.
+
+**LaTeX.** `Latexize` moves for the four set and `mod` cases above and **not** for `implies`.
+[CSharpMath.Evaluation](https://github.com/verybadcat/CSharpMath/blob/master/CSharpMath.Evaluation/Evaluation.cs),
+which reads our LaTeX back, folds `\cup`, `\setminus`, `\in`, `\cdot` and `\bmod` to the left at
+the same precedences this grammar uses — so those needed the same brackets — but folds `\to` to the
+**right**, which is the usual convention for implication and is what `Latexize` was already
+bracketing for. The change only ever adds `\left(`/`\right)` groups, which CSharpMath already
+parses, so nothing downstream needs a matching change
+([#822](https://github.com/asc-community/AngouriMath/issues/822)).
+
+---
+
## 2.3.0 — since 2.2.0
### At a glance
diff --git a/Sources/AngouriMath/Docs/Usage/Syntax.md b/Sources/AngouriMath/Docs/Usage/Syntax.md
index af2d04094..dfbbadbcd 100644
--- a/Sources/AngouriMath/Docs/Usage/Syntax.md
+++ b/Sources/AngouriMath/Docs/Usage/Syntax.md
@@ -1,4 +1,4 @@
-# Expression syntax
+# Expression syntax
What `MathS.FromString` accepts, and what `Entity.Stringize` produces. The authority is
[`Core/Antlr/AngouriMath.g`](../../Core/Antlr/AngouriMath.g); this page is a reading of it. To
@@ -22,12 +22,17 @@ will say so ([#822](https://github.com/asc-community/AngouriMath/issues/822)).
## Operators, loosest first
-Everything on one line has the same precedence and groups to the left, except `^`, which groups
-to the right.
+Everything on one line has the same precedence and groups to the left, except `^` and `provided`,
+which group to the right.
+
+Grouping is not decoration: an operator that is not associative prints the brackets it has, so
+`a implies (b implies c)` prints with them and `(a implies b) implies c` prints without. Where the
+operator *is* associative the brackets carry no mathematics and are not printed, so `x + (y + z)`
+prints as `x + y + z` and reads back as `(x + y) + z` — the same number, a differently shaped tree.
| | operators | notes |
|---|---|---|
-| 1 | `provided` | |
+| 1 | `provided` | **groups to the right**: `a provided b provided c` is `a provided (b provided c)` |
| 2 | `implies` | |
| 3 | `or` `\|` | |
| 4 | `xor` | |
@@ -35,10 +40,10 @@ to the right.
| 6 | `not` | prefix |
| 7 | `=` `<>` `>` `>=` `<` `<=` | chained: `a < b < c` means `a < b and b < c` |
| 8 | `in` | |
-| 9 | `unite` `\/`, `setsubtract` `\` | |
+| 9 | `unite` `\/`, `setsubtract` `\` | one level, so `A \/ B \ C` is `(A \/ B) \ C` |
| 10 | `intersect` `/\` | |
| 11 | `+` `-` | |
-| 12 | `*` `/` `mod` | |
+| 12 | `*` `/` `mod` | one level, so `x * y mod z` is `(x * y) mod z` |
| 13 | `+` `-` | prefix |
| 14 | `^` | **groups to the right**: `a ^ b ^ c` is `a ^ (b ^ c)` |
| 15 | `!` | postfix factorial |
diff --git a/Sources/AngouriMath/Functions/Output/Latex/Latex.Arithmetics.Classes.cs b/Sources/AngouriMath/Functions/Output/Latex/Latex.Arithmetics.Classes.cs
index 7e0ccc40e..2257437bd 100644
--- a/Sources/AngouriMath/Functions/Output/Latex/Latex.Arithmetics.Classes.cs
+++ b/Sources/AngouriMath/Functions/Output/Latex/Latex.Arithmetics.Classes.cs
@@ -49,10 +49,13 @@ public override string Latexize()
};
case 1:
if (longArray[index - 1] is Integer(-1))
- return $"-{currIn.Latexize(currIn.LatexPriority < LatexPriority)}"; // display "-1 * x * y" as "-x \cdot y", only for the first -1
+ return $"-{currIn.Latexize(currIn.LatexPriority < LatexPriority || currIn is Modf)}"; // display "-1 * x * y" as "-x \cdot y", only for the first -1
break;
}
- var currOut = currIn.Latexize(currIn.LatexPriority < LatexPriority);
+ // A factor other than the first that is a `mod` needs brackets: \bmod sits
+ // at this precedence level and is not associative, so "x \cdot y \bmod z"
+ // reads as "(x y) mod z". 2 * (3 mod 2) is 2 and (2 * 3) mod 2 is 0.
+ var currOut = currIn.Latexize(currIn.LatexPriority < LatexPriority || currIn is Modf);
return (longArray[index - 1], currIn) switch // whether we use juxtaposition and omit \cdot
{
diff --git a/Sources/AngouriMath/Functions/Output/Latex/Latex.Omni.Classes.cs b/Sources/AngouriMath/Functions/Output/Latex/Latex.Omni.Classes.cs
index ad97a0428..00a9f5f9f 100644
--- a/Sources/AngouriMath/Functions/Output/Latex/Latex.Omni.Classes.cs
+++ b/Sources/AngouriMath/Functions/Output/Latex/Latex.Omni.Classes.cs
@@ -50,8 +50,12 @@ public override string Latexize()
partial record Unionf
{
///
+ // CSharpMath.Evaluation reads \cup and \setminus at one precedence level and folds
+ // them to the left, exactly as the grammar here does, so a \setminus on the right
+ // needs bracketing or it comes back as the outer operator. Union with union is
+ // associative and stays flat.
public override string Latexize()
- => $@"{Left.Latexize(Left.LatexPriority < LatexPriority)} \cup {Right.Latexize(Right.LatexPriority < LatexPriority)}";
+ => $@"{Left.Latexize(Left.LatexPriority < LatexPriority)} \cup {Right.Latexize(Right.LatexPriority < LatexPriority || Right is SetMinusf)}";
}
partial record Intersectionf
@@ -64,22 +68,27 @@ public override string Latexize()
partial record SetMinusf
{
///
+ // Not associative, and sharing a precedence level with union: anything at that
+ // level on the right is bracketed, the same rule \frac's text form follows.
public override string Latexize()
- => $@"{Left.Latexize(Left.LatexPriority < LatexPriority)} \setminus {Right.Latexize(Right.LatexPriority < LatexPriority)}";
+ => $@"{Left.Latexize(Left.LatexPriority < LatexPriority)} \setminus {Right.Latexize(Right.LatexPriority <= LatexPriority)}";
}
partial record Inf
{
///
+ // \in is left-folded by CSharpMath too, and membership is not associative.
public override string Latexize()
- => $@"{Element.Latexize(Element.LatexPriority < LatexPriority)} \in {SupSet.Latexize(SupSet.LatexPriority < LatexPriority)}";
+ => $@"{Element.Latexize(Element.LatexPriority < LatexPriority)} \in {SupSet.Latexize(SupSet.LatexPriority <= LatexPriority)}";
}
}
partial record Providedf
{
///
- public override string Latexize() => $@"{Expression.Latexize(Expression.LatexPriority < LatexPriority)} \quad \text{{for}} \quad {Predicate.Latexize(Predicate.LatexPriority < LatexPriority)}";
+ // Right-folding, so the left operand is the one that needs bracketing -- see the
+ // remark on Stringize.
+ public override string Latexize() => $@"{Expression.Latexize(Expression.LatexPriority <= LatexPriority)} \quad \text{{for}} \quad {Predicate.Latexize(Predicate.LatexPriority < LatexPriority)}";
}
partial record Piecewise
diff --git a/Sources/AngouriMath/Functions/Output/ToString/ToString.Arithmetics.Classes.cs b/Sources/AngouriMath/Functions/Output/ToString/ToString.Arithmetics.Classes.cs
index be2782930..f549f2769 100644
--- a/Sources/AngouriMath/Functions/Output/ToString/ToString.Arithmetics.Classes.cs
+++ b/Sources/AngouriMath/Functions/Output/ToString/ToString.Arithmetics.Classes.cs
@@ -30,10 +30,14 @@ public override string Stringize() =>
public partial record Mulf
{
///
+ // Multiplication is associative, so a product or a quotient on the right may stay
+ // unbracketed -- but `mod` shares this precedence level and is not associative, so a
+ // `mod` on the right must be bracketed or it is re-read as the outer operator:
+ // `2 * (3 mod 2)` is 2 and `(2 * 3) mod 2` is 0.
public override string Stringize() =>
(Multiplier is Integer(-1) && !MathS.Diagnostic.OutputExplicit ? "-"
: Multiplier.Stringize(Multiplier.Priority < Priority) + " * ")
- + Multiplicand.Stringize(Multiplicand.Priority < Priority);
+ + Multiplicand.Stringize(Multiplicand.Priority < Priority || Multiplicand is Modf);
///
public override string ToString() => Stringize();
}
diff --git a/Sources/AngouriMath/Functions/Output/ToString/ToString.Discrete.Classes.cs b/Sources/AngouriMath/Functions/Output/ToString/ToString.Discrete.Classes.cs
index 6c952b0dc..9f305e7bf 100644
--- a/Sources/AngouriMath/Functions/Output/ToString/ToString.Discrete.Classes.cs
+++ b/Sources/AngouriMath/Functions/Output/ToString/ToString.Discrete.Classes.cs
@@ -55,8 +55,13 @@ public override string Stringize()
partial record Impliesf
{
///
+ // The grammar folds `implies` to the left, like everything but `^` and `provided`, so
+ // it is the *conclusion* that needs bracketing when it is an implication of its own.
+ // The two are not interchangeable: `false implies (true implies false)` is true, and
+ // `(false implies true) implies false` is false, so printing the first without its
+ // brackets said the opposite of what it meant.
public override string Stringize()
- => $"{Assumption.Stringize(Assumption.Priority <= Priority)} implies {Conclusion.Stringize(Conclusion.Priority < Priority)}";
+ => $"{Assumption.Stringize(Assumption.Priority < Priority)} implies {Conclusion.Stringize(Conclusion.Priority <= Priority)}";
///
public override string ToString() => Stringize();
}
diff --git a/Sources/AngouriMath/Functions/Output/ToString/ToString.Omni.Classes.cs b/Sources/AngouriMath/Functions/Output/ToString/ToString.Omni.Classes.cs
index a69393263..8cd273c52 100644
--- a/Sources/AngouriMath/Functions/Output/ToString/ToString.Omni.Classes.cs
+++ b/Sources/AngouriMath/Functions/Output/ToString/ToString.Omni.Classes.cs
@@ -88,8 +88,13 @@ partial record Complexes
partial record Unionf
{
///
+ // Union is associative, so a union on the right may stay unbracketed -- but `\/`
+ // and `\` share one precedence level and are folded by one loop in the grammar, so
+ // a *set difference* on the right must be bracketed or it is re-read as the outer
+ // operator: `{ 1, 2 } \/ ({ 3 } \ { 1, 2 })` is { 1, 2, 3 } and
+ // `({ 1, 2 } \/ { 3 }) \ { 1, 2 }` is { 3 }.
public override string Stringize()
- => $@"{Left.Stringize(Left.Priority < Priority)} \/ {Right.Stringize(Right.Priority < Priority)}";
+ => $@"{Left.Stringize(Left.Priority < Priority)} \/ {Right.Stringize(Right.Priority < Priority || Right is SetMinusf)}";
///
public override string ToString() => Stringize();
}
@@ -106,8 +111,12 @@ public override string Stringize()
partial record SetMinusf
{
///
+ // Set difference is not associative, and it shares its precedence level with
+ // union, so anything at that level on the right needs bracketing -- the same rule
+ // `-` and `/` follow. `{1,2,3} \ ({2,3} \ {3})` is { 1, 3 } where
+ // `({1,2,3} \ {2,3}) \ {3}` is { 1 }.
public override string Stringize()
- => $@"{Left.Stringize(Left.Priority < Priority)} \ {Right.Stringize(Right.Priority < Priority)}";
+ => $@"{Left.Stringize(Left.Priority < Priority)} \ {Right.Stringize(Right.Priority <= Priority)}";
///
public override string ToString() => Stringize();
}
@@ -115,8 +124,10 @@ public override string Stringize()
partial record Inf
{
///
+ // `in` is folded to the left and is not associative: `(a in b) in c` asks whether
+ // a truth value is an element of c, `a in (b in c)` whether a is an element of one.
public override string Stringize()
- => $@"{Element.Stringize(Element.Priority < Priority)} in {SupSet.Stringize(SupSet.Priority < Priority)}";
+ => $@"{Element.Stringize(Element.Priority < Priority)} in {SupSet.Stringize(SupSet.Priority <= Priority)}";
///
public override string ToString() => Stringize();
}
@@ -127,7 +138,14 @@ public override string Stringize()
partial record Providedf
{
///
- public override string Stringize() => $@"{Expression.Stringize(Expression.Priority < Priority.Provided)} provided {Predicate.Stringize(Predicate.Priority < Priority.Provided)}";
+ // `provided` is the one infix operator the grammar folds to the *right*, so this is
+ // the mirror of the `implies` rule: the flat form is read as
+ // `a provided (b provided c)`, and it is the left operand that has to say when it is
+ // an attached condition of its own. `(x provided p) provided q` printed flat came
+ // back as `x provided (p provided q)` -- the same value, since both are `x` exactly
+ // when `p` and `q` hold, and a different expression, which is what the round trip is
+ // about.
+ public override string Stringize() => $@"{Expression.Stringize(Expression.Priority <= Priority.Provided)} provided {Predicate.Stringize(Predicate.Priority < Priority.Provided)}";
///
public override string ToString() => Stringize();
}
diff --git a/Sources/Tests/UnitTests/Convenience/FromStringTest.cs b/Sources/Tests/UnitTests/Convenience/FromStringTest.cs
index 08f80817f..ad8d5a331 100644
--- a/Sources/Tests/UnitTests/Convenience/FromStringTest.cs
+++ b/Sources/Tests/UnitTests/Convenience/FromStringTest.cs
@@ -216,7 +216,20 @@ public void TestFormula8()
[Fact] public void TestProvided1() => Assert.Equal(MathS.Provided("a", "b"), FromString("a provided b"));
[Fact] public void TestProvided2() => Assert.Equal(MathS.Provided("a", MathS.Provided("b", "c")), FromString("a provided b provided c"));
[Fact] public void TestProvided3() => Assert.Equal(MathS.Provided(MathS.Provided("a", "b"), "c"), FromString("(a provided b) provided c"));
- [Fact] public void TestProvided4() => Assert.Equal(FromString("a provided (b provided c)").Stringize(), FromString("(a provided b) provided c").Stringize());
+ // These two are different expressions -- `provided` folds to the right, so only the first
+ // is what the flat spelling means -- and this used to assert that they print alike. That
+ // is the round trip failing, written down as an expectation: the printed form of the
+ // second came back as the first. They must print differently, and each must read back as
+ // itself.
+ [Fact] public void TestProvided4()
+ {
+ var rightNested = FromString("a provided (b provided c)");
+ var leftNested = FromString("(a provided b) provided c");
+ Assert.NotEqual(rightNested, leftNested);
+ Assert.NotEqual(rightNested.Stringize(), leftNested.Stringize());
+ Assert.Equal(rightNested, FromString(rightNested.Stringize()));
+ Assert.Equal(leftNested, FromString(leftNested.Stringize()));
+ }
[Theory]
[InlineData("sh", "Sinh")]
diff --git a/Sources/Tests/UnitTests/Convenience/LatexTest.cs b/Sources/Tests/UnitTests/Convenience/LatexTest.cs
index 8179e86ad..fd9b43aed 100644
--- a/Sources/Tests/UnitTests/Convenience/LatexTest.cs
+++ b/Sources/Tests/UnitTests/Convenience/LatexTest.cs
@@ -1,4 +1,4 @@
-//
+//
// Copyright (c) 2019-2022 Angouri.
// AngouriMath is licensed under MIT.
// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.
@@ -312,10 +312,13 @@ [Fact] public void Provided1()
=> Test(@"x \quad \text{for} \quad y", MathS.Provided("x", "y"));
[Fact] public void Provided2()
=> Test(@"x+1 \quad \text{for} \quad y > 0", MathS.Provided("x + 1", "y > 0"));
+ // Both of these are left-nested, and `provided` folds to the right -- so the flat form
+ // these used to expect is read back as the *right*-nested tree, which is a different
+ // expression. The grouping is the assertion; the two used to pin its absence.
[Fact] public void Provided3()
- => Test(@"a \quad \text{for} \quad b \quad \text{for} \quad c", MathS.Provided(MathS.Provided("a", "b"), "c"));
+ => Test(@"\left(a \quad \text{for} \quad b\right) \quad \text{for} \quad c", MathS.Provided(MathS.Provided("a", "b"), "c"));
[Fact] public void Provided4()
- => Test(@"\left(a \quad \text{for} \quad b \quad \text{for} \quad c \quad \text{for} \quad d\right) \to \top ", MathS.Provided(MathS.Provided("a", "b"), MathS.Provided("c", "d")).Implies(Entity.Boolean.True));
+ => Test(@"\left(\left(a \quad \text{for} \quad b\right) \quad \text{for} \quad c \quad \text{for} \quad d\right) \to \top ", MathS.Provided(MathS.Provided("a", "b"), MathS.Provided("c", "d")).Implies(Entity.Boolean.True));
// Juxtaposition tests
[Fact] public void M1InTheMiddle() => Test(@"x \left(-1\right) \cdot x", (x * (-1)) * x);
[Fact] public void MultiplyNumberWithPower() => Test(@"2 \cdot {3}^{4}", 2 * ((Entity)3).Pow(4));
@@ -532,6 +535,37 @@ [Fact] public void SetMinus()
=> Test(@"\left\{ 1, 2, 3 \right\} \setminus \left\{ 2 \right\}", MathS.Sets.Finite(1, 2, 3).SetSubtract(MathS.Sets.Finite(2)));
[Fact] public void SetIn()
=> Test(@"x \in \left\{ 1, 2, 3 \right\}", x.In(MathS.Sets.Finite(1, 2, 3)));
+
+ // Associativity, which LaTeX has to express too: CSharpMath.Evaluation reads the LaTeX
+ // back, and it folds \cup, \setminus and \in to the left at the precedences the grammar
+ // here uses, so a right operand at the same level must be bracketed or it comes back as
+ // the outer operator. \to is the exception -- CSharpMath folds it to the *right*, the
+ // usual convention for implication, so it is the assumption that gets the brackets.
+ private static readonly Entity A = MathS.Sets.Finite(1, 2), B = MathS.Sets.Finite(2, 3), C = MathS.Sets.Finite(3);
+ private static readonly string As = @"\left\{ 1, 2 \right\}", Bs = @"\left\{ 2, 3 \right\}", Cs = @"\left\{ 3 \right\}";
+
+ [Fact] public void SetMinusOfSetMinusOnTheRight()
+ => Test($@"{As} \setminus \left({Bs} \setminus {Cs}\right)", A.SetSubtract(B.SetSubtract(C)));
+ [Fact] public void SetMinusOfSetMinusOnTheLeft()
+ => Test($@"{As} \setminus {Bs} \setminus {Cs}", A.SetSubtract(B).SetSubtract(C));
+ [Fact] public void SetMinusOfUnionOnTheRight()
+ => Test($@"{As} \setminus \left({Bs} \cup {Cs}\right)", A.SetSubtract(B.Unite(C)));
+ [Fact] public void UnionOfSetMinusOnTheRight()
+ => Test($@"{As} \cup \left({Bs} \setminus {Cs}\right)", A.Unite(B.SetSubtract(C)));
+ [Fact] public void UnionOfUnionOnTheRightStaysFlat()
+ => Test($@"{As} \cup {Bs} \cup {Cs}", A.Unite(B.Unite(C)));
+ [Fact] public void IntersectionOfIntersectionOnTheRightStaysFlat()
+ => Test($@"{As} \cap {Bs} \cap {Cs}", A.Intersect(B.Intersect(C)));
+ [Fact] public void InOfInOnTheRight()
+ => Test(@"x \in \left(y \in z\right)", x.In(MathS.Var("y").In(MathS.Var("z"))));
+ [Fact] public void ImpliesOfImpliesOnTheRightStaysFlat()
+ => Test(@"x \to y \to z", x.Implies(MathS.Var("y").Implies(MathS.Var("z"))));
+ [Fact] public void ImpliesOfImpliesOnTheLeft()
+ => Test(@"\left(x \to y\right) \to z", x.Implies(MathS.Var("y")).Implies(MathS.Var("z")));
+ [Fact] public void ModAsANonLeadingFactor()
+ => Test(@"x \left(y \bmod z\right)", x * (MathS.Var("y") % MathS.Var("z")));
+ [Fact] public void ModAsTheLeadingFactorStaysFlat()
+ => Test(@"x \bmod y \cdot z", (x % MathS.Var("y")) * MathS.Var("z"));
// Special sets
[Fact] public void SetBooleans()
diff --git a/Sources/Tests/UnitTests/Convenience/PriorityTest.cs b/Sources/Tests/UnitTests/Convenience/PriorityTest.cs
index eff56bb0b..c2797df6b 100644
--- a/Sources/Tests/UnitTests/Convenience/PriorityTest.cs
+++ b/Sources/Tests/UnitTests/Convenience/PriorityTest.cs
@@ -49,6 +49,20 @@ public class PriorityTest
[InlineData("not a = b", "a <> b")]
[InlineData("a = b and not b = c and c < d", "a = b <> c < d")]
[InlineData("not a = b and not b = c", "a <> b <> c")]
+ // The right operand of a left-folded operator that is not associative has to keep its
+ // brackets, or the normalized form is a different expression.
+ [InlineData("a implies (b implies c)", "a implies (b implies c)")]
+ [InlineData("a implies b implies c", "(a implies b) implies c")]
+ [InlineData("a implies (b implies c) implies d", "(a implies (b implies c)) implies d")]
+ [InlineData(@"A \ (B \ C)", @"A \ (B \ C)")]
+ [InlineData(@"A \ B \ C", @"(A \ B) \ C")]
+ [InlineData(@"A \/ (B \ C)", @"A \/ (B \ C)")]
+ [InlineData(@"A \ (B \/ C)", @"A \ (B \/ C)")]
+ [InlineData(@"A \/ B \ C", @"(A \/ B) \ C")]
+ [InlineData("a in (b in c)", "a in (b in c)")]
+ [InlineData("a in b in c", "(a in b) in c")]
+ [InlineData("a * (b mod c)", "a * (b mod c)")]
+ [InlineData("a * b mod c", "(a * b) mod c")]
public void Test(string normalizedForm, string alternateForm)
{
Assert.Equal(alternateForm.ToEntity(), normalizedForm.ToEntity());
diff --git a/Sources/Tests/UnitTests/Convenience/StringizeRoundTripTest.cs b/Sources/Tests/UnitTests/Convenience/StringizeRoundTripTest.cs
index f2507b5ec..25de960a1 100644
--- a/Sources/Tests/UnitTests/Convenience/StringizeRoundTripTest.cs
+++ b/Sources/Tests/UnitTests/Convenience/StringizeRoundTripTest.cs
@@ -1,4 +1,4 @@
-//
+//
// Copyright (c) 2019-2022 Angouri.
// AngouriMath is licensed under MIT.
// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.
@@ -136,6 +136,92 @@ public void TheNodesWithoutAnOperatorSpellingPrintAsTheirFunction(string source)
[InlineData("x provided y")]
public void BooleansRoundTrip(string source) => AssertRoundTrip(source);
+ ///
+ /// An operator that is not associative has to print the bracketing it has, because
+ /// the flat form is read the way the grammar folds -- to the left for everything but
+ /// ^ and provided. Printing a implies (b implies c) as
+ /// a implies b implies c did not merely look wrong: it comes back as
+ /// (a implies b) implies c, and the two do not agree
+ /// (false implies (true implies false) is true, the other is false).
+ ///
+ ///
+ /// \/ and \ share one precedence level and are folded by the same loop, so a
+ /// \ on the right of a \/ mis-associates too even though union on its own is
+ /// associative; and mod shares a level with * and /, so a mod on
+ /// the right of a * does the same.
+ ///
+ [Theory]
+ [InlineData("a implies (b implies c)")]
+ [InlineData("(a implies b) implies c")]
+ [InlineData("a implies b implies c")]
+ [InlineData("a implies (b implies (c implies d))")]
+ [InlineData("(a implies (b implies c)) implies d")]
+ [InlineData("a implies (b or c)")]
+ [InlineData(@"A \ (B \ C)")]
+ [InlineData(@"(A \ B) \ C")]
+ [InlineData(@"A \/ (B \ C)")]
+ [InlineData(@"A \ (B \/ C)")]
+ [InlineData(@"(A \/ B) \ C")]
+ [InlineData(@"A \ (B /\ C)")]
+ [InlineData(@"A /\ (B \ C)")]
+ [InlineData("a in (b in c)")]
+ [InlineData("(a in b) in c")]
+ [InlineData("x * (y mod z)")]
+ [InlineData("(x * y) mod z")]
+ [InlineData("x mod (y * z)")]
+ [InlineData("x / (y mod z)")]
+ [InlineData("x mod (y mod z)")]
+ // `provided` is the mirror case: it folds to the *right*, so it is the left operand that
+ // mis-associates. Its value survives either reading -- `(x provided p) provided q` and
+ // `x provided (p provided q)` are both `x` exactly when `p` and `q` hold -- which is why
+ // a check on the value alone passes it and a check on the expression does not.
+ [InlineData("(x provided p) provided q")]
+ [InlineData("x provided (p provided q)")]
+ [InlineData("x provided p provided q")]
+ [InlineData("((x provided p) provided q) provided r")]
+ public void ANonAssociativeOperatorKeepsItsGrouping(string source) => AssertRoundTrip(source);
+
+ ///
+ /// The same defect stated in values rather than in trees: each of these is a case where
+ /// reading the printed form back the way the grammar folds gives a different answer.
+ ///
+ [Theory]
+ [InlineData("false implies (true implies false)")]
+ [InlineData(@"{ 1, 2, 3 } \ ({ 2, 3 } \ { 3 })")]
+ [InlineData(@"{ 1, 2 } \ ({ 2 } \/ { 3 })")]
+ [InlineData(@"{ 1, 2 } \/ ({ 3 } \ { 1, 2 })")]
+ [InlineData("2 * (3 mod 2)")]
+ [InlineData("(3 in { 3 }) in BB")]
+ public void AMisreadGroupingWouldChangeTheValue(string source)
+ {
+ var original = source.ToEntity();
+ Assert.Equal(original.Evaled, original.Stringize().ToEntity().Evaled);
+ }
+
+ ///
+ /// The other half of the rule, pinned so that it is a decision rather than an oversight:
+ /// an operator that is associative prints flat, because the bracketing then
+ /// carries no mathematics. x + (y + z) comes back as (x + y) + z, which is a
+ /// different and the same number -- and bracketing it instead would
+ /// turn every expanded polynomial into a right-nested pile of parentheses.
+ ///
+ [Theory]
+ [InlineData("1 + (2 + 3)", "1 + 2 + 3")]
+ [InlineData("1 + (2 - 3)", "1 + 2 - 3")]
+ [InlineData("2 * (3 * 4)", "2 * 3 * 4")]
+ [InlineData("2 * (6 / 3)", "2 * 6 / 3")]
+ [InlineData("true and (false and true)", "True and False and True")]
+ [InlineData("true or (false or true)", "True or False or True")]
+ [InlineData("true xor (false xor true)", "True xor False xor True")]
+ [InlineData(@"{ 1 } \/ ({ 2 } \/ { 3 })", @"{ 1 } \/ { 2 } \/ { 3 }")]
+ [InlineData(@"{ 1, 2 } /\ ({ 2, 3 } /\ { 2 })", @"{ 1, 2 } /\ { 2, 3 } /\ { 2 }")]
+ public void AnAssociativeOperatorPrintsFlatAndKeepsItsValue(string source, string expectedFlat)
+ {
+ var original = source.ToEntity();
+ Assert.Equal(expectedFlat, original.Stringize());
+ Assert.Equal(original.Evaled, original.Stringize().ToEntity().Evaled);
+ }
+
[Theory]
[InlineData("{ 1, 2, 3 }")]
[InlineData("[1; 2]")]