From e92743bc73e4521d2f115026be88be1751a95b55 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sun, 18 Oct 2026 12:11:45 +0000 Subject: [PATCH] Move to AngouriMath 2.6.0, and read the congruence it writes with \pmod AngouriMath 2.6.0 adds a congruence node, written a \equiv b \pmod{m}, and AngouriMathLatexSweepTests found \pmod was an invalid command here: the only one of the 109 nodes it checks. \pmod{m} is now LaTeX's "(mod m)" after a quad, built from atoms that already lay out, as \Bra and \Ket are. Evaluation reads it as the modulus of the \equiv before it, so 15625 \equiv 1 \pmod{7} evaluates to true. \equiv without \pmod, and \pmod without \equiv, are errors. The other 33 changed rows are AngouriMath 2.6.0 printing or answering differently: - a variable now sorts before a function in a product (y \cot(x)); - a--a prints as a+a; - [7, 8]/9 evaluates to [7/9, 8/9]; - d/d(x^2) of x is 1/(2x), where 2.2.0 answered 0, so (dx/d(x^2)) x y + 1 is 1 + y/2 for x != 0. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- CSharpMath.Core.Tests/Atom/LaTeXParserTest.cs | 24 ++++++ .../EvaluationTests.cs | 78 +++++++++++-------- .../CSharpMath.Evaluation.csproj | 2 +- CSharpMath.Evaluation/Evaluation.cs | 41 ++++++++++ .../TestAngouriMathForms.cs | 8 ++ CSharpMath/Atom/LaTeXSettings.cs | 10 +++ 6 files changed, 129 insertions(+), 34 deletions(-) diff --git a/CSharpMath.Core.Tests/Atom/LaTeXParserTest.cs b/CSharpMath.Core.Tests/Atom/LaTeXParserTest.cs index dfacb74a..1f197bae 100644 --- a/CSharpMath.Core.Tests/Atom/LaTeXParserTest.cs +++ b/CSharpMath.Core.Tests/Atom/LaTeXParserTest.cs @@ -1469,6 +1469,30 @@ public void TestModulo() { Assert.Equal(@"x\bmod y", LaTeXParser.MathListToLaTeX(list).ToString()); } + /// + /// \pmod{m} is a quad, then "(mod m)": the operator name and the modulus inside parentheses. + /// + [Fact] + public void TestParenthesisedModulo() { + var list = ParseLaTeX(@"x\equiv y\pmod{z}"); + Assert.Collection(list, + CheckAtom("x"), + CheckAtom("≡"), + CheckAtom("y"), + CheckAtom("", space => { + Assert.Equal(Length.EmWidth.Amount, space.Length); + Assert.Equal(Length.EmWidth.IsMu, space.IsMu); + }), + CheckAtom("", inner => { + Assert.Equal(new Boundary("("), inner.LeftBoundary); + Assert.Equal(new Boundary(")"), inner.RightBoundary); + Assert.Collection(inner.InnerList, + CheckAtom("mod"), + CheckAtom("z")); + })); + Assert.Equal(@"x\equiv y\quad \left( \operatorname{mod} z\right) ", LaTeXParser.MathListToLaTeX(list).ToString()); + } + [Theory] [InlineData(@"\TeX")] [InlineData(@"\left.\mathrm{T\! \raisebox{-4.5mu}{E}\mkern-2.25muX}\right.")] diff --git a/CSharpMath.Evaluation.Tests/EvaluationTests.cs b/CSharpMath.Evaluation.Tests/EvaluationTests.cs index 687e0d77..77d0cb1f 100644 --- a/CSharpMath.Evaluation.Tests/EvaluationTests.cs +++ b/CSharpMath.Evaluation.Tests/EvaluationTests.cs @@ -137,8 +137,8 @@ public void Numbers(string input, string converted, string output) => [InlineData(@"a \cdot b / c", @"\frac{ab}{c}", @"\frac{ab}{c}")] [InlineData(@"a / b + c", @"\frac{a}{b}+c", @"\frac{a}{b}+c")] [InlineData(@"a / b - c", @"\frac{a}{b}-c", @"\frac{a}{b}-c")] - [InlineData(@"a / b \times c", @"\frac{a}{b}\cdot c", @"\frac{a}{b}\cdot c")] - [InlineData(@"a / b \cdot c", @"\frac{a}{b}\cdot c", @"\frac{a}{b}\cdot c")] + [InlineData(@"a / b \times c", @"\frac{a}{b}\cdot c", @"c\frac{a}{b}")] + [InlineData(@"a / b \cdot c", @"\frac{a}{b}\cdot c", @"c\frac{a}{b}")] [InlineData(@"a / b / c", @"\frac{\frac{a}{b}}{c}", @"\frac{a}{bc}")] [InlineData(@"2+\frac ab", @"2+\frac{a}{b}", @"2+\frac{a}{b}")] [InlineData(@"\frac ab+2", @"\frac{a}{b}+2", @"\frac{a}{b}+2")] @@ -146,8 +146,8 @@ public void Numbers(string input, string converted, string output) => [InlineData(@"\frac ab-2", @"\frac{a}{b}-2", @"\frac{a}{b}-2")] [InlineData(@"2\times\frac ab", @"2\cdot \frac{a}{b}", @"2\cdot \frac{a}{b}")] [InlineData(@"2\cdot\frac ab", @"2\cdot \frac{a}{b}", @"2\cdot \frac{a}{b}")] - [InlineData(@"\frac ab\times 2", @"\frac{a}{b}\cdot 2", @"\frac{a}{b}\cdot 2")] - [InlineData(@"\frac ab\cdot 2", @"\frac{a}{b}\cdot 2", @"\frac{a}{b}\cdot 2")] + [InlineData(@"\frac ab\times 2", @"\frac{a}{b}\cdot 2", @"2\cdot \frac{a}{b}")] + [InlineData(@"\frac ab\cdot 2", @"\frac{a}{b}\cdot 2", @"2\cdot \frac{a}{b}")] [InlineData(@"2/\frac ab", @"\frac{2}{\frac{a}{b}}", @"\frac{2b}{a}")] [InlineData(@"\frac ab/2", @"\frac{\frac{a}{b}}{2}", @"\frac{a}{2b}")] [InlineData(@"1+\mathrm{i}", @"1+\mathrm{i}", @"1+\mathrm{i}")] @@ -177,7 +177,7 @@ public void Numbers(string input, string converted, string output) => [InlineData(@"\frac{-\infty}{\infty}", @"\frac{-\infty }{\infty }", @"\mathrm{undefined}")] [InlineData(@"a / bc", @"\frac{a}{bc}", @"\frac{a}{bc}")] [InlineData(@"a / bc / d", @"\frac{\frac{a}{bc}}{d}", @"\frac{a}{bcd}")] - [InlineData(@"-2/\sin x/y", @"\frac{\frac{-2}{\sin \left( x\right) }}{y}", @"\frac{-2}{\sin \left( x\right) \cdot y}")] + [InlineData(@"-2/\sin x/y", @"\frac{\frac{-2}{\sin \left( x\right) }}{y}", @"\frac{-2}{y\sin \left( x\right) }")] public void BinaryOperators(string latex, string converted, string result) => Test(latex, converted, result); /// /// The two forms AngouriMath emits that had no reading here: \bmod for its modulo node @@ -193,6 +193,15 @@ public void Numbers(string input, string converted, string output) => [InlineData(@"\operatorname{\varphi}(10)", @"\operatorname{φ} \left( 10\right) ", @"4")] [InlineData(@"\operatorname{\varphi}(x)", @"\operatorname{φ} \left( x\right) ", @"\operatorname{φ} \left( x\right) ")] public void AngouriMathOnlyForms(string latex, string converted, string result) => Test(latex, converted, result); + /// + /// AngouriMath's congruence, which it writes a \equiv b \pmod{m}. \pmod reads as + /// "(mod m)" after a quad, and is taken as the modulus of the \equiv before it. + /// + [Theory] + [InlineData(@"x\equiv y\pmod{z}", @"x\equiv y\quad \left( \operatorname{mod} z\right) ", @"x\equiv y\quad \left( \operatorname{mod} z\right) ")] + [InlineData(@"15625\equiv 1\pmod{7}", @"15625\equiv 1\quad \left( \operatorname{mod} 7\right) ", @"\top ")] + [InlineData(@"x+1\equiv y\pmod{z+1}", @"x+1\equiv y\quad \left( \operatorname{mod} z+1\right) ", @"1+x\equiv y\quad \left( \operatorname{mod} 1+z\right) ")] + public void Congruence(string latex, string converted, string result) => Test(latex, converted, result); [Theory] [InlineData(@"+i", @"\mathrm{i}", @"\mathrm{i}")] [InlineData(@"-i", @"-\mathrm{i}", @"-\mathrm{i}")] @@ -207,9 +216,9 @@ public void Numbers(string input, string converted, string output) => [InlineData("a++a", "a+a", @"2a")] [InlineData("a+-a", "a-a", "0")] [InlineData("a-+a", "a-a", "0")] - [InlineData("a--a", @"a--a", @"2a")] + [InlineData("a--a", @"a+a", @"2a")] [InlineData("a+++a", "a+a", @"2a")] - [InlineData("a---a", @"a--\left( -1\right) \cdot a", "0")] + [InlineData("a---a", @"a+\left( -1\right) \cdot a", @"0")] [InlineData(@"a\times+a", @"aa", "a^2")] [InlineData(@"a\times-a", @"a\left( -1\right) \cdot a", "-a^2")] [InlineData(@"+a\times+a", @"aa", "a^2")] @@ -219,7 +228,7 @@ public void Numbers(string input, string converted, string output) => [InlineData("+a/+a", @"\frac{a}{a}", @"1\quad \mathrm{for}\quad a\neq 0")] [InlineData("-a/-a", @"\frac{-a}{-a}", @"1\quad \mathrm{for}\quad a\neq 0")] [InlineData(@"-2+-2+-2", @"-2-2-2", "-6")] - [InlineData(@"-2--2--2", @"-2--2--2", "2")] + [InlineData(@"-2--2--2", @"-2+2+2", @"2")] [InlineData(@"-2\times -2\times -2", @"-2\left( -1\right) \cdot 2\left( -1\right) \cdot 2", "-8")] [InlineData(@"-2/-2/-2", @"\frac{\frac{-2}{-2}}{-2}", @"\frac{-1}{2}")] public void UnaryOperators(string latex, string converted, string result) => Test(latex, converted, result); @@ -277,7 +286,7 @@ public void Numbers(string input, string converted, string output) => [InlineData(@"2\times3^4", @"2\cdot 3^4", "162")] [InlineData(@"1/x", @"\frac{1}{x}", @"\frac{1}{x}")] [InlineData(@"2/x", @"\frac{2}{x}", @"\frac{2}{x}")] - [InlineData(@"0^x", @"0^x", @"0\quad \mathrm{for}\quad \left( 1+\frac{1}{\operatorname{sgn} \left( x\right) ^2}\right) \cdot x>0")] + [InlineData(@"0^x", @"0^x", @"0\quad \mathrm{for}\quad x\left( 1+\frac{1}{\operatorname{sgn} \left( x\right) ^2}\right) >0")] [InlineData(@"1^x", @"1^x", @"1")] [InlineData(@"x^0", @"x^0", @"1\quad \mathrm{for}\quad x\neq 0")] [InlineData(@"x^{-1}", @"x^{-1}", @"\frac{1}{x}")] @@ -358,7 +367,7 @@ public void Numbers(string input, string converted, string output) => [InlineData(@"\cos -x", @"\cos \left( -x\right) ", @"\cos \left( x\right) ")] // 2.2.0 uses evenness [InlineData(@"\tan x\%", @"\tan \left( \frac{x}{100}\right) ", @"\tan \left( \frac{x}{100}\right) ")] [InlineData(@"\tan x\%^2", @"\tan \left( \left( \frac{x}{100}\right) ^2\right) ", @"\tan \left( \left( \frac{x}{100}\right) ^2\right) ")] - [InlineData(@"\cot x\times y", @"\cot \left( x\right) \cdot y", @"\cot \left( x\right) \cdot y")] + [InlineData(@"\cot x\times y", @"\cot \left( x\right) \cdot y", @"y\cot \left( x\right) ")] [InlineData(@"\cot x/y", @"\frac{\cot \left( x\right) }{y}", @"\frac{\cot \left( x\right) }{y}")] [InlineData(@"\cos \arccos x", @"\cos \left( \arccos \left( x\right) \right) ", @"x")] [InlineData(@"\sin^2 x", @"\sin \left( x\right) ^2", @"\sin \left( x\right) ^2")] @@ -403,7 +412,7 @@ public void Numbers(string input, string converted, string output) => [InlineData(@"\cos^{-1} -x", @"\arccos \left( -x\right) ", @"\arccos \left( -x\right) ")] [InlineData(@"\tan^{-1} x\%", @"\arctan \left( \frac{x}{100}\right) ", @"\arctan \left( \frac{x}{100}\right) ")] [InlineData(@"\tan^{-1} x\%^2", @"\arctan \left( \left( \frac{x}{100}\right) ^2\right) ", @"\arctan \left( \left( \frac{x}{100}\right) ^2\right) ")] - [InlineData(@"\cot^{-1} x\times y", @"\arccot \left( x\right) \cdot y", @"\arccot \left( x\right) \cdot y")] + [InlineData(@"\cot^{-1} x\times y", @"\arccot \left( x\right) \cdot y", @"y\arccot \left( x\right) ")] [InlineData(@"\cot^{-1} x/y", @"\frac{\arccot \left( x\right) }{y}", @"\frac{\arccot \left( x\right) }{y}")] // arccos(cos x) is x only on [0, pi]; AngouriMath 2.2.0 no longer claims it in general. [InlineData(@"\cos^{-1} \arccos^{-1} x", @"\arccos \left( \cos \left( x\right) \right) ", @"\arccos \left( \cos \left( x\right) \right) ")] @@ -458,7 +467,7 @@ public void Numbers(string input, string converted, string output) => [InlineData(@"\sin((x+1)+2)", @"\sin \left( x+1+2\right) ", @"\sin \left( 3+x\right) ")] [InlineData(@"\sin(x)2", @"\sin \left( x\right) \cdot 2", @"2\sin \left( x\right) ")] [InlineData(@"\sin(x)(x+1)", @"\sin \left( x\right) \left( x+1\right) ", @"\sin \left( x\right) \left( 1+x\right) ")] - [InlineData(@"\sin(x)(x+1)(x)", @"\sin \left( x\right) \left( x+1\right) \cdot x", @"\sin \left( x\right) \left( 1+x\right) \cdot x")] + [InlineData(@"\sin(x)(x+1)(x)", @"\sin \left( x\right) \left( x+1\right) \cdot x", @"x\sin \left( x\right) \left( 1+x\right) ")] [InlineData(@"\sin(x^2)", @"\sin \left( x^2\right) ", @"\sin \left( x^2\right) ")] [InlineData(@"\sin\ (x^2)", @"\sin \left( x^2\right) ", @"\sin \left( x^2\right) ")] [InlineData(@"\sin\; (x^2)", @"\sin \left( x^2\right) ", @"\sin \left( x^2\right) ")] @@ -492,14 +501,14 @@ public void Numbers(string input, string converted, string output) => [InlineData(@"\sin^a\ (x)^2", @"\sin \left( x\right) ^{a\cdot 2}", @"\sin \left( x\right) ^{2a}")] [InlineData(@"\sin^a\; (x)^2", @"\sin \left( x\right) ^{a\cdot 2}", @"\sin \left( x\right) ^{2a}")] [InlineData(@"\sin^a\ \; (x)^2", @"\sin \left( x\right) ^{a\cdot 2}", @"\sin \left( x\right) ^{2a}")] - [InlineData(@"\sin(x)^2(x)", @"\sin \left( x\right) ^2\cdot x", @"\sin \left( x\right) ^2\cdot x")] - [InlineData(@"\sin\ (x)^2(x)", @"\sin \left( x\right) ^2\cdot x", @"\sin \left( x\right) ^2\cdot x")] - [InlineData(@"\sin\; (x)^2(x)", @"\sin \left( x\right) ^2\cdot x", @"\sin \left( x\right) ^2\cdot x")] - [InlineData(@"\sin\ \; (x)^2(x)", @"\sin \left( x\right) ^2\cdot x", @"\sin \left( x\right) ^2\cdot x")] - [InlineData(@"\sin^a(x)^2(x)", @"\sin \left( x\right) ^{a\cdot 2}\cdot x", @"\sin \left( x\right) ^{2a}\cdot x")] - [InlineData(@"\sin^a\ (x)^2(x)", @"\sin \left( x\right) ^{a\cdot 2}\cdot x", @"\sin \left( x\right) ^{2a}\cdot x")] - [InlineData(@"\sin^a\; (x)^2(x)", @"\sin \left( x\right) ^{a\cdot 2}\cdot x", @"\sin \left( x\right) ^{2a}\cdot x")] - [InlineData(@"\sin^a\ \; (x)^2(x)", @"\sin \left( x\right) ^{a\cdot 2}\cdot x", @"\sin \left( x\right) ^{2a}\cdot x")] + [InlineData(@"\sin(x)^2(x)", @"\sin \left( x\right) ^2\cdot x", @"x\sin \left( x\right) ^2")] + [InlineData(@"\sin\ (x)^2(x)", @"\sin \left( x\right) ^2\cdot x", @"x\sin \left( x\right) ^2")] + [InlineData(@"\sin\; (x)^2(x)", @"\sin \left( x\right) ^2\cdot x", @"x\sin \left( x\right) ^2")] + [InlineData(@"\sin\ \; (x)^2(x)", @"\sin \left( x\right) ^2\cdot x", @"x\sin \left( x\right) ^2")] + [InlineData(@"\sin^a(x)^2(x)", @"\sin \left( x\right) ^{a\cdot 2}\cdot x", @"x\sin \left( x\right) ^{2a}")] + [InlineData(@"\sin^a\ (x)^2(x)", @"\sin \left( x\right) ^{a\cdot 2}\cdot x", @"x\sin \left( x\right) ^{2a}")] + [InlineData(@"\sin^a\; (x)^2(x)", @"\sin \left( x\right) ^{a\cdot 2}\cdot x", @"x\sin \left( x\right) ^{2a}")] + [InlineData(@"\sin^a\ \; (x)^2(x)", @"\sin \left( x\right) ^{a\cdot 2}\cdot x", @"x\sin \left( x\right) ^{2a}")] [InlineData(@"\sin (\frac\pi2)", @"\sin \left( \frac{\mathrm{\pi }}{2}\right) ", @"1")] [InlineData(@"\sin (\frac\pi2)+1", @"\sin \left( \frac{\mathrm{\pi }}{2}\right) +1", @"2")] public void Parentheses(string latex, string converted, string result) { @@ -576,7 +585,7 @@ public void Matrices(string latex, string converted, string result) { [InlineData(@"(xy,xy]", @"\emptyset ", @"\emptyset ")] [InlineData(@"[xy,xy)", @"\emptyset ", @"\emptyset ")] [InlineData(@"(xy,xy)", @"\emptyset ", @"\emptyset ")] - [InlineData(@"\frac{[7, 8]}{9}", @"\frac{\left[ 7,8\right] }{9}", @"\frac{\left[ 7,8\right] }{9}")] + [InlineData(@"\frac{[7, 8]}{9}", @"\frac{\left[ 7,8\right] }{9}", @"\left[ \frac{7}{9},\frac{8}{9}\right] ")] [InlineData(@"+(3,4]", @"\left( 3,4\right] ", @"\left( 3,4\right] ")] public void Intervals(string latex, string converted, string result) { Test(latex, converted, result); @@ -818,6 +827,9 @@ public void Intervals(string latex, string converted, string result) { [InlineData(@"\begin{cases}1&\end{cases}", "Missing case predicate")] [InlineData(@"\begin{cases}1&\mathrm{for}^23\end{cases}", "for operator cannot have a superscript")] [InlineData(@"\begin{cases}1&\mathrm{for}_23\end{cases}", "for operator cannot have a subscript")] + [InlineData(@"x\equiv y", @"≡ is read as a congruence, so its right side must be followed by \pmod")] + [InlineData(@"\equiv y\pmod{z}", "Missing left side for ≡")] + [InlineData(@"x\pmod{y}", @"\pmod is read only after the right side of ≡")] [InlineData(@"\left\{ \, \begin{array}{ll}\textstyle x+1&\textstyle \mathrm{otherwise}&ERROR\\ \textstyle x-1&\textstyle \mathrm{otherwise}\end{array}\right. ", "The cases environment must have 1 to 2 columns per row")] public void Error(string badLaTeX, string error) => Evaluation.Evaluate(ParseLaTeX(badLaTeX)) @@ -1007,30 +1019,30 @@ public void ChainedComparisons(string latex, string converted, string result, st [InlineData(@"\frac{\mathrm{d}}{\mathrm{d}(x)}(x)y+1", @"\frac{\mathrm{d}}{\mathrm{d}x}xy+1", @"1+y")] [InlineData(@"\frac{\mathrm{d}}{\mathrm{d}x^2}x^2y+1", @"\frac{\mathrm{d}}{\mathrm{d}\left( x^2\right) }x^2\cdot y+1", @"1+y")] [InlineData(@"\frac{\mathrm{d}x^2}{\mathrm{d}x^2}y+1", @"\left( \frac{\mathrm{d}}{\mathrm{d}\left( x^2\right) }x^2\right) \cdot y+1", @"1+y")] - [InlineData(@"\frac{\mathrm{d}x}{\mathrm{d}x^2}xy+1", @"\left( \frac{\mathrm{d}}{\mathrm{d}\left( x^2\right) }x\right) \cdot xy+1", @"1")] - [InlineData(@"\frac{\mathrm{d}x}{\mathrm{d}(x^2)}(x)y+1", @"\left( \frac{\mathrm{d}}{\mathrm{d}\left( x^2\right) }x\right) \cdot xy+1", @"1")] + [InlineData(@"\frac{\mathrm{d}x}{\mathrm{d}x^2}xy+1", @"\left( \frac{\mathrm{d}}{\mathrm{d}\left( x^2\right) }x\right) \cdot xy+1", @"1+\frac{y}{2}\quad \mathrm{for}\quad x\neq 0")] + [InlineData(@"\frac{\mathrm{d}x}{\mathrm{d}(x^2)}(x)y+1", @"\left( \frac{\mathrm{d}}{\mathrm{d}\left( x^2\right) }x\right) \cdot xy+1", @"1+\frac{y}{2}\quad \mathrm{for}\quad x\neq 0")] [InlineData(@"\frac{\mathrm{d}^2}{\mathrm{d}x^2}x^2y+1", @"\frac{\mathrm{d^{\mathnormal{2}}}}{\mathrm{d}x^2}x^2\cdot y+1", @"1+2y")] [InlineData(@"\frac{\mathrm{d}^2}{\mathrm{d}(x^2)^2}x^2y+1", @"\frac{\mathrm{d^{\mathnormal{2}}}}{\mathrm{d}\left( x^2\right) ^2}x^2\cdot y+1", @"1")] [InlineData(@"\frac{\mathrm{d}^2x}{\mathrm{d}x^2}x^2y+1", @"\left( \frac{\mathrm{d^{\mathnormal{2}}}}{\mathrm{d}x^2}x\right) \cdot x^2\cdot y+1", @"1")] - [InlineData(@"\frac{\mathrm{d}^2x}{\mathrm{d}(x^2)^2}x^2y+1", @"\left( \frac{\mathrm{d^{\mathnormal{2}}}}{\mathrm{d}\left( x^2\right) ^2}x\right) \cdot x^2\cdot y+1", @"1")] + [InlineData(@"\frac{\mathrm{d}^2x}{\mathrm{d}(x^2)^2}x^2y+1", @"\left( \frac{\mathrm{d^{\mathnormal{2}}}}{\mathrm{d}\left( x^2\right) ^2}x\right) \cdot x^2\cdot y+1", @"1+\frac{\frac{-1}{4}\cdot y}{x}")] [InlineData(@"\frac{\mathrm{d}}{\mathrm{d}x}\lim_{y\to z}xy+x", @"\frac{\mathrm{d}}{\mathrm{d}x}\lim _{y\rightarrow z}xy+x", @"x+z")] [InlineData(@"\frac{\mathrm{d}^0x}{\mathrm{d}x^0}y+1", @"\left( \frac{\mathrm{d^{\mathnormal{0}}}}{\mathrm{d}x^0}x\right) \cdot y+1", @"1+xy")] - [InlineData(@"\frac{\mathrm{d}^{-1}x}{\mathrm{d}x^{-1}}y+1", @"\left( \int x\, \mathrm{d}x\right) \cdot y+1", @"\left( \frac{x^2}{2}+C\right) \cdot y+1")] + [InlineData(@"\frac{\mathrm{d}^{-1}x}{\mathrm{d}x^{-1}}y+1", @"\left( \int x\, \mathrm{d}x\right) \cdot y+1", @"y\left( \frac{x^2}{2}+C\right) +1")] [InlineData(@"\frac\mathrm{dy}\mathrm{dx}\mathrm{dx}", @"\frac{\mathrm{dy}}{\mathrm{dx}}\cdot \mathrm{dx}", @"\mathrm{dy}\quad \mathrm{for}\quad \mathrm{dx}\neq 0")] // upright letters are interpreted together. [InlineData(@"\frac\mathrm{d\ y}\mathrm{d\ x}", @"\frac{\mathrm{d}}{\mathrm{d}x}y", @"0")] // spaces are required to separate upright letters. [InlineData(@"\frac\mathrm{dx}\mathrm{dx}", @"\frac{\mathrm{dx}}{\mathrm{dx}}", @"1\quad \mathrm{for}\quad \mathrm{dx}\neq 0")] public void Derivative(string latex, string converted, string result) => Test(latex, converted, result); [Theory] [InlineData(@"\int\mathrm{d}x", @"\int 1\, \mathrm{d}x", @"C+x")] - [InlineData(@"\int\int\mathrm{d}x\mathrm{d}y", @"\int \int 1\, \mathrm{d}x\, \mathrm{d}y", @"C_1+\left( C+x\right) \cdot y")] - [InlineData(@"(\int\int\mathrm{d}x\mathrm{d}y)", @"\int \int 1\, \mathrm{d}x\, \mathrm{d}y", @"C_1+\left( C+x\right) \cdot y")] - [InlineData(@"\int\int C\mathrm{d}x\mathrm{d}y", @"\int \int C\, \mathrm{d}x\, \mathrm{d}y", @"\left( C_1+Cx\right) \cdot y+C_2")] + [InlineData(@"\int\int\mathrm{d}x\mathrm{d}y", @"\int \int 1\, \mathrm{d}x\, \mathrm{d}y", @"C_1+y\left( C+x\right) ")] + [InlineData(@"(\int\int\mathrm{d}x\mathrm{d}y)", @"\int \int 1\, \mathrm{d}x\, \mathrm{d}y", @"C_1+y\left( C+x\right) ")] + [InlineData(@"\int\int C\mathrm{d}x\mathrm{d}y", @"\int \int C\, \mathrm{d}x\, \mathrm{d}y", @"y\left( C_1+Cx\right) +C_2")] [InlineData(@"\int\frac\mathrm{d}{\mathrm{d}x}\int C\mathrm{d}x\mathrm{d}y", @"\int \frac{\mathrm{d}}{\mathrm{d}x}\int C\, \mathrm{d}x\, \mathrm{d}y", @"C_1+Cy")] [InlineData(@"\int x\,\mathrm{d}x", @"\int x\, \mathrm{d}x", @"\frac{x^2}{2}+C")] - [InlineData(@"\int x+y\,\mathrm{d}x", @"\int \left( x+y\right) \, \mathrm{d}x", @"\frac{\left( x+y\right) ^2}{2}+C")] - [InlineData(@"2\int x+y\,\mathrm{d}x\cdot 3", @"2\left( \int \left( x+y\right) \, \mathrm{d}x\right) \cdot 3", @"6\left( C+\frac{\left( x+y\right) ^2}{2}\right) ")] - [InlineData(@"2+\int\sin x+y\,\mathrm{d}\sin x+3", @"2+\int \left( \sin \left( x\right) +y\right) \, \mathrm{d}\left( \sin \left( x\right) \right) +3", @"5+\frac{\left( \sin \left( x\right) +y\right) ^2}{2}+C")] - [InlineData(@"2+\int xy+z\,\mathrm{d}xy\cdot 3", @"2+\left( \int \left( xy+z\right) \, \mathrm{d}\left( xy\right) \right) \cdot 3", @"2+\left( \frac{\left( xy+z\right) ^2}{2}+C\right) \cdot 3")] + [InlineData(@"\int x+y\,\mathrm{d}x", @"\int \left( x+y\right) \, \mathrm{d}x", @"\frac{x^2}{2}+yx+C")] + [InlineData(@"2\int x+y\,\mathrm{d}x\cdot 3", @"2\left( \int \left( x+y\right) \, \mathrm{d}x\right) \cdot 3", @"6\left( C+\frac{x^2}{2}+xy\right) ")] + [InlineData(@"2+\int\sin x+y\,\mathrm{d}\sin x+3", @"2+\int \left( \sin \left( x\right) +y\right) \, \mathrm{d}\left( \sin \left( x\right) \right) +3", @"5+\frac{\sin \left( x\right) ^2}{2}+y\sin \left( x\right) +C")] + [InlineData(@"2+\int xy+z\,\mathrm{d}xy\cdot 3", @"2+\left( \int \left( xy+z\right) \, \mathrm{d}\left( xy\right) \right) \cdot 3", @"2+3\left( \frac{\left( xy\right) ^2}{2}+zxy+C\right) ")] [InlineData(@"\int_0^1\mathrm{d}x", @"\int _0^11\, \mathrm{d}x", @"1")] [InlineData(@"\int_{10}^{11}\mathrm{d}x", @"\int _{10}^{11}1\, \mathrm{d}x", @"1")] [InlineData(@"\int_{10}^{11}x\mathrm{d}x", @"\int _{10}^{11}x\, \mathrm{d}x", @"\frac{21}{2}")] @@ -1039,7 +1051,7 @@ public void ChainedComparisons(string latex, string converted, string result, st [Theory] [InlineData(@"x\mathrm{for}y", @"x\quad \mathrm{for}\quad y", @"x\quad \mathrm{for}\quad y")] [InlineData(@"x\quad \mathrm{for}\quad y", @"x\quad \mathrm{for}\quad y", @"x\quad \mathrm{for}\quad y")] - [InlineData(@"(x\mathrm{for}y)\mathrm{for}z", @"x\quad \mathrm{for}\quad y\quad \mathrm{for}\quad z", @"x\quad \mathrm{for}\quad y\wedge z")] + [InlineData(@"(x\mathrm{for}y)\mathrm{for}z", @"\left( x\quad \mathrm{for}\quad y\right) \quad \mathrm{for}\quad z", @"x\quad \mathrm{for}\quad y\wedge z")] [InlineData(@"x\mathrm{for}(y\mathrm{for}z)", @"x\quad \mathrm{for}\quad y\quad \mathrm{for}\quad z", @"x\quad \mathrm{for}\quad y\wedge z")] [InlineData(@"x\mathrm{for}y\mathrm{for}z", @"x\quad \mathrm{for}\quad y\quad \mathrm{for}\quad z", @"x\quad \mathrm{for}\quad y\wedge z")] [InlineData(@"x\mathrm{for}y,y\mathrm{for}z,z\mathrm{for}a\mathrm{for}b", @"x\quad \mathrm{for}\quad y,y\quad \mathrm{for}\quad z,z\quad \mathrm{for}\quad a\quad \mathrm{for}\quad b", null)] diff --git a/CSharpMath.Evaluation/CSharpMath.Evaluation.csproj b/CSharpMath.Evaluation/CSharpMath.Evaluation.csproj index 9b5aa3db..e2b58836 100644 --- a/CSharpMath.Evaluation/CSharpMath.Evaluation.csproj +++ b/CSharpMath.Evaluation/CSharpMath.Evaluation.csproj @@ -13,7 +13,7 @@ - + diff --git a/CSharpMath.Evaluation/Evaluation.cs b/CSharpMath.Evaluation/Evaluation.cs index 7fe4bcdd..67cd33e6 100644 --- a/CSharpMath.Evaluation/Evaluation.cs +++ b/CSharpMath.Evaluation/Evaluation.cs @@ -82,6 +82,17 @@ public static MathList Visualize(MathItem entity) => .Match(list => list, e => throw new InvalidCodePathException(e)); public static Result Evaluate(MathList mathList) => Transform(mathList.Clone(true)).Bind(result => result is { } r ? Result.Ok(r) : Result.Err("There is nothing to evaluate")); + /// Whether is the "(mod m)" that \pmod{m} produces, and m if so. + static bool IsModulus(MathAtom atom, out MathList modulus) { + if (atom is Atoms.Inner { + LeftBoundary.Nucleus: "(", RightBoundary.Nucleus: ")", + InnerList: [Atoms.LargeOperator { Nucleus: "mod" }, ..] inner }) { + modulus = new MathList(inner.Skip(1)); + return true; + } + modulus = new MathList(); + return false; + } static Result Transform(MathList mathList) { int i = 0; return Transform(mathList, ref i, Precedence.DefaultContext); @@ -699,6 +710,30 @@ string GreekToLaTeXCommandName(string n) => handlePrecedence = Precedence.Comparison; handleBinary = (x, y) => MathS.Negation(new Entity.Equalsf(x, y)); goto handleBinary; + // AngouriMath's congruence, a ≡ b (mod m), written a \equiv b \pmod{m}. + case Atoms.Relation { Nucleus: "≡" }: + if (atom.Superscript is not [] || atom.Subscript is not []) + return "Scripts are unsupported for Relation ≡"; + if (prec >= Precedence.Comparison) { + i--; + return prev; + } + if (prev is null) return "Missing left side for ≡"; + i++; + (next, error) = Transform(mathList, ref i, Precedence.Comparison); + if (error != null) return error; + (var congruent, error) = prev.AsEntity("left side of ≡"); + if (error != null) return error; + (var to, error) = next.AsEntity("right side of ≡"); + if (error != null) return error; + if (i + 1 >= mathList.Count || !IsModulus(mathList[i + 1], out var modulusList)) + return @"≡ is read as a congruence, so its right side must be followed by \pmod"; + i++; + (var modulus, error) = Transform(modulusList).Bind(m => m.AsEntity("modulus of ≡")); + if (error != null) return error; + @this = MathS.NumberTheory.Congruent(congruent, to, modulus); + prev = null; // We used up prev, don't keep it + goto handleThis; case Atoms.Relation { Nucleus: "<" }: handlePrecedence = Precedence.Comparison; handleBinary = (x, y) => new Entity.Lessf(x, y); @@ -803,6 +838,12 @@ string GreekToLaTeXCommandName(string n) => if (InnerHandlers.TryGetValue((contextInfo.KnownOpening, rightBracket), out var handler)) return handler(prev).Bind(x => (MathItem?)x); else return $"Unrecognized bracket pair {contextInfo.KnownOpening} {rightBracket}"; + // "(mod m)", which \pmod produces: it ends the right side of a congruence the way a comparison would, + // and the ≡ case below reads it as the modulus rather than letting it multiply onto that side. + case Atoms.Inner pmod when IsModulus(pmod, out _): + if (prec < Precedence.Comma) return @"\pmod is read only after the right side of ≡"; + i--; + return prev; case Atoms.Inner { LeftBoundary.Nucleus: var left, InnerList: var inner, RightBoundary.Nucleus: var right }: (@this, error) = Transform(inner); if (error != null) return error; diff --git a/CSharpMath.Rendering.Tests/TestAngouriMathForms.cs b/CSharpMath.Rendering.Tests/TestAngouriMathForms.cs index 23452f75..6f3cf731 100644 --- a/CSharpMath.Rendering.Tests/TestAngouriMathForms.cs +++ b/CSharpMath.Rendering.Tests/TestAngouriMathForms.cs @@ -19,6 +19,7 @@ static System.Drawing.RectangleF Measure(string latex) { [InlineData(@"x\bmod y")] [InlineData(@"\operatorname{φ}\left( x\right) ")] [InlineData(@"\operatorname{φ}\left( x\bmod y\right) ")] + [InlineData(@"x\equiv y\pmod{z}")] public void TheyLayOut(string latex) { var measured = Measure(latex); Assert.True(measured.Width > 0, $"zero width for {latex}"); @@ -33,5 +34,12 @@ public void TheyLayOut(string latex) { [Fact] public void ModuloTakesUpRoom() => Assert.True(Measure(@"x\bmod y").Width > Measure(@"xy").Width * 2); + + /// + /// \pmod{z} is a quad, the parentheses, "mod" and z, so it is wider than the quad and z alone. + /// + [Fact] + public void ParenthesisedModuloTakesUpRoom() => + Assert.True(Measure(@"y\pmod{z}").Width > Measure(@"y\quad z").Width + Measure(@"xy").Width); } } diff --git a/CSharpMath/Atom/LaTeXSettings.cs b/CSharpMath/Atom/LaTeXSettings.cs index 18ccdddb..c178698b 100644 --- a/CSharpMath/Atom/LaTeXSettings.cs +++ b/CSharpMath/Atom/LaTeXSettings.cs @@ -264,6 +264,16 @@ public static class LaTeXSettings { if (!parser.ReadCharIfAvailable('}')) return "Expected }"; return Ok(new LargeOperator(operatorname.ToString(), null)); } }, + // \pmod{m} is LaTeX's "(mod m)", set off by a quad from what precedes it. AngouriMath emits it + // for its congruence node, `a \equiv b \pmod{m}`. It is built from atoms that already lay out, + // as \Bra and \Ket are: the space, then a parenthesised list opening with the operator name. + { @"\pmod", (parser, accumulate, stopChar) => + parser.ReadArgument().Bind(modulus => { + accumulate.Add(new Space(Length.EmWidth)); + var inner = new MathList(new LargeOperator("mod", null)); + inner.Append(modulus); + return Ok(new Inner(new Boundary("("), inner, new Boundary(")"))); + }) }, // Bra and Ket implementations are derived from Donald Arseneau's braket LaTeX package. // See: https://www.ctan.org/pkg/braket { @"\Bra", (parser, accumulate, stopChar) =>