From 613712d1970a7ec199c9a48bcc99b6b7ccce0e0b Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Fri, 30 Oct 2026 18:36:57 +0000 Subject: [PATCH] A negation of a signed term is printed in brackets A negation prints as a bare - in front of what it negates. A negative integer was bracketed by its priority, which is a sum's, but a negative rational has a quotient's, and a negation and +oo print with their own sign: -(-2/3) printed as --2/3, which reads back as -(-2) / 3, and sinh(+oo) as (e ^ +oo - e ^ (-+oo)) / 2. A negated term whose print starts with a sign is now bracketed. NegationOfASignedTermPrintsTest prints six negations and reads them back to their values; three of them, and sinh(-5/4)'s double sign, fail on master. Found by work/roundtrip: over 5000 random expressions no print failed to parse and none read back to another value. Fixes #2264. Co-Authored-By: Claude Opus 5.5 (1M context) --- BREAKING-CHANGES.md | 12 +++++ Sources/.editorconfig | 3 ++ .../ToString/ToString.Arithmetics.Classes.cs | 14 +++-- .../NegationOfASignedTermPrintsTest.cs | 51 +++++++++++++++++++ 4 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 Sources/Tests/UnitTests/Convenience/NegationOfASignedTermPrintsTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index a53b2ebec..486497a5c 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -194,6 +194,18 @@ and `Simplify` passed that on as `not x = 0`, though at `x = 0` the input is `-y | `"sgn(NaN)".ToEntity().Evaled` | `1` | `NaN` | | `"sgn(-1 provided false)".ToEntity().Evaled` | `1` | `NaN` | +### A negation of a signed term is printed in brackets + +**Silent: `-(-2/3)` prints as `-(-2/3)`, where it printed as `--2/3`.** A negation prints as a bare `-` in front of what +it negates, and a negative integer was bracketed by its priority, but a negative rational, a negation, and `+oo` were +not. `--5/4` reads back as `-(-5) / 4`, which has the same value as a different expression, and is easy to misread +([#2264](https://github.com/asc-community/AngouriMath/issues/2264)). + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `"sinh(-5/4)".ToEntity().ToString()` | `(e ^ (-5/4) - e ^ (--5/4)) / 2` | `(e ^ (-5/4) - e ^ (-(-5/4))) / 2` | +| `new Entity.Mulf(-1, new Entity.Mulf(-1, x)).ToString()` | `--x` | `-(-x)` | + ### A matrix is divided, raised to a power and compared as a matrix **Silent: wrong answers about matrices become right, or stay written.** Where an operation had no answer of its own diff --git a/Sources/.editorconfig b/Sources/.editorconfig index 0033226ad..faa5f1930 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -246,6 +246,9 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed [Tests/UnitTests/Convenience/RealAndImaginaryPartTest.cs] file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n +[Tests/UnitTests/Convenience/NegationOfASignedTermPrintsTest.cs] +file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n + [Tests/UnitTests/Convenience/DoubleFactorialTest.cs] file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n diff --git a/Sources/AngouriMath/Functions/Output/ToString/ToString.Arithmetics.Classes.cs b/Sources/AngouriMath/Functions/Output/ToString/ToString.Arithmetics.Classes.cs index de663adce..338fd142c 100644 --- a/Sources/AngouriMath/Functions/Output/ToString/ToString.Arithmetics.Classes.cs +++ b/Sources/AngouriMath/Functions/Output/ToString/ToString.Arithmetics.Classes.cs @@ -34,10 +34,16 @@ public partial record Mulf // 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. - private protected override string StringizeNode() => - (Multiplier is Integer(-1) && !MathS.Diagnostic.OutputExplicit ? "-" - : Multiplier.Stringize(Multiplier.Priority < Priority) + " * ") - + Multiplicand.Stringize(Multiplicand.Priority < Priority || Multiplicand is Modf); + private protected override string StringizeNode() + { + var multiplicand = Multiplicand.Stringize(Multiplicand.Priority < Priority || Multiplicand is Modf); + if (Multiplier is not Integer(-1) || MathS.Diagnostic.OutputExplicit) + return Multiplier.Stringize(Multiplier.Priority < Priority) + " * " + multiplicand; + // A negation of something printed with a sign of its own is bracketed: -(-2/3), not --2/3, which is + // read back as -(-2) / 3; and -(+oo), not -+oo. A negative rational is not bracketed by its priority, + // which is a quotient's, as a negative integer is. https://github.com/asc-community/AngouriMath/issues/2264 + return multiplicand.StartsWith("-") || multiplicand.StartsWith("+") ? "-(" + multiplicand + ")" : "-" + multiplicand; + } /// public override string ToString() => Stringize(); } diff --git a/Sources/Tests/UnitTests/Convenience/NegationOfASignedTermPrintsTest.cs b/Sources/Tests/UnitTests/Convenience/NegationOfASignedTermPrintsTest.cs new file mode 100644 index 000000000..9b586bf23 --- /dev/null +++ b/Sources/Tests/UnitTests/Convenience/NegationOfASignedTermPrintsTest.cs @@ -0,0 +1,51 @@ +// +// Copyright (c) 2019-2026 Angouri. +// AngouriMath is licensed under MIT. +// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md. +// Website: https://am.angouri.org. +// + +using AngouriMath; +using AngouriMath.Extensions; +using Xunit; +using static AngouriMath.Entity; +using static AngouriMath.Entity.Number; + +namespace AngouriMath.Tests.Convenience +{ + /// + /// A negation of something printed with a sign of its own is bracketed, so it reads back as itself: -(-2/3) + /// printed as --2/3, read back as -(-2) / 3, and -(+oo) as -+oo. + /// #2264 + /// + [Trait("Area", "Convenience")] + public sealed class NegationOfASignedTermPrintsTest + { + private static Entity Negated(Entity e) => new Mulf(Integer.Create(-1), e); + + public static TheoryData Negations => new() + { + { Negated(Rational.Create(-2, 3)), "-(-2/3)" }, + { Negated(Real.PositiveInfinity), "-(+oo)" }, + { Negated(Negated(MathS.Var("x"))), "-(-x)" }, + { Negated(Integer.Create(-3)), "-(-3)" }, + { Negated(MathS.Var("x")), "-x" }, + { Negated(Rational.Create(2, 3)), "-2/3" }, + }; + + [Theory] + [MemberData(nameof(Negations))] + public void ItPrintsWithOneSignAndReadsBackToItsValue(Entity negation, string printed) + { + Assert.Equal(printed, negation.Stringize()); + // The parser folds a negated number into the number, so -(-2/3) reads back as 2/3: the same value, and for + // a symbol the same tree. + var x = "7/10".ToEntity(); + Assert.Equal(negation.Substitute("x", x).Evaled, negation.Stringize().ToEntity().Substitute("x", x).Evaled); + } + + [Fact] + public void AHyperbolicSineOfANegativeRationalHasNoDoubleSign() + => Assert.DoesNotContain("--", "sinh(-5/4)".ToEntity().Stringize()); + } +}