Skip to content

Commit ae6d1b5

Browse files
Move to AngouriMath 2.0, which renamed Latexise to Latexize (#262)
* Move to AngouriMath 2.0, which renamed Latexise to Latexize AngouriMath 2.0 renames ILatexiseable.Latexise to ILatexizeable.Latexize. MathItem implements the new interface explicitly, so CSharpMath's own public surface does not change: MathItem.Latexise stays the name callers know, and PublicAPI.Unshipped.txt is untouched. The public-API analyzer confirms it -- no RS warnings. Only two call sites are on AngouriMath's side of the boundary and move: Content.Latexize() on an AngouriMath.Entity, and the ILatexizeable parameter in Interpret. One test expectation changes. 2.0 splits a radical over a positive factor, so 1+\sqrt{2x} expands to 1+\sqrt{2}\sqrt{x}. sqrt(2x) = sqrt(2)sqrt(x) holds for all complex x when the factored constant is positive, so the new output is sound; it is a rendering change rather than a correctness one. 955 of 955 tests pass. * Read the LaTeX AngouriMath 2.2.0 emits, and pin it with a sweep Evaluation.Visualize throws InvalidCodePathException on any LaTeX it cannot read, and says why in its own source -- "CSharpMath must handle all LaTeX coming from AngouriMath or a bug is present!" -- but nothing checked it. AngouriMathLatexSweepTests now does: every concrete Entity type AngouriMath can print, Latexized and fed back through LaTeXParser. On 2.2.0 that is 76 nodes, of which two forms had no reading here. \bmod, AngouriMath's modulo node. Added as a BinaryOperator whose nucleus is the word "mod": binary spacing, and upright because only Variable and Number go through the italicising font changer. It binds like multiplication and division, matching AngouriMath's Priority.Mul, so x+y \bmod z is x+(y mod z) on both sides. \operatorname{\varphi}, Euler's totient. The name was read with ReadString, which takes ASCII letters only, so both the command and -- since the name is written back out as the letter -- this method's own output \operatorname{φ} were rejected. It now reads char.IsLetter and resolves commands that stand for a letter, refusing the rest; \operatorname{a|} stays the error it was. Both directions are covered: parsing, serialising back, evaluating to an Entity, and laying out. Layout is measured rather than compared against a baseline image, since the new shapes there are a word-length BinaryOperator and a non-ASCII operator name, and glyph coverage is already asserted by TestCommandDisplay. Four recorded answers changed with the version and are updated, each because AngouriMath became more careful rather than less: sgn(|x|) was 1; it is 1 away from zero but 0 at zero arccos(cos x) was x; that holds only on [0, pi] x <= x was T; now T for x in RR, the domain <= needs cos(-x) now folds to cos(x), by evenness Core 1515, Evaluation 962, Rendering 1226, Rendering.Text 237 all pass.
1 parent ddd8b98 commit ae6d1b5

9 files changed

Lines changed: 244 additions & 14 deletions

File tree

CSharpMath.Core.Tests/Atom/LaTeXParserTest.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1300,6 +1300,35 @@ public void TestOperatorName(string operatorname, string output) {
13001300
Assert.Equal(output, LaTeXParser.MathListToLaTeX(list).ToString());
13011301
}
13021302

1303+
/// <summary>
1304+
/// A letter in an operator name may be written as a command. AngouriMath emits
1305+
/// <c>\operatorname{\varphi}</c> for Euler's totient, which is the reason this exists.
1306+
/// </summary>
1307+
[Theory]
1308+
[InlineData(@"\varphi", "φ")]
1309+
[InlineData(@"\Gamma", "Γ")]
1310+
[InlineData(@"ma\chi ", "maχ")]
1311+
public void TestOperatorNameWithCommands(string operatorname, string name) {
1312+
var list = ParseLaTeX(@$"\operatorname{{{operatorname}}}");
1313+
Assert.Collection(list, CheckAtom<LargeOperator>(name));
1314+
var output = LaTeXParser.MathListToLaTeX(list).ToString();
1315+
Assert.Equal(@$"\operatorname{{{name}}} ", output);
1316+
// The name comes back out as the letter rather than as the command, so reading that is the
1317+
// round trip that matters -- and it is why the name is read with char.IsLetter.
1318+
Assert.Collection(ParseLaTeX(output), CheckAtom<LargeOperator>(name));
1319+
}
1320+
1321+
/// <summary><c>\bmod</c> is a binary operator whose nucleus is the word "mod".</summary>
1322+
[Fact]
1323+
public void TestModulo() {
1324+
var list = ParseLaTeX(@"x\bmod y");
1325+
Assert.Collection(list,
1326+
CheckAtom<Variable>("x"),
1327+
CheckAtom<BinaryOperator>("mod"),
1328+
CheckAtom<Variable>("y"));
1329+
Assert.Equal(@"x\bmod y", LaTeXParser.MathListToLaTeX(list).ToString());
1330+
}
1331+
13031332
[Theory]
13041333
[InlineData(@"\TeX")]
13051334
[InlineData(@"\left.\mathrm{T\! \raisebox{-4.5mu}{E}\mkern-2.25muX}\right.")]
@@ -1561,6 +1590,9 @@ public void TestHelpfulErrorMessage(string input, int index, string expected) {
15611590
InlineData(@"\operatorname {a|}", @"Error: Expected }
15621591
\operatorname {a|}
15631592
↑ (pos 16)"),
1593+
InlineData(@"\operatorname{\pm}", @"Error: Invalid command \pm in an operator name
1594+
\operatorname{\pm}
1595+
↑ (pos 17)"),
15641596
]
15651597
public void TestErrors(string badInput, string expected) {
15661598
var (list, actual) = LaTeXParser.MathListFromLaTeX(badInput);
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Reflection;
5+
using AngouriMath;
6+
using AngouriMath.Extensions;
7+
using Xunit;
8+
9+
namespace CSharpMath.EvaluationTests {
10+
using Atom;
11+
12+
/// <summary>
13+
/// <see cref="Evaluation.Visualize"/> throws <c>InvalidCodePathException</c> on any LaTeX it
14+
/// cannot read, and says why in its own source: "CSharpMath must handle all LaTeX coming from
15+
/// AngouriMath or a bug is present!". Nothing checked that, on either side — AngouriMath's own
16+
/// <c>Docs/Usage/Syntax.md</c> says the LaTeX round trip "is checked in someone else's
17+
/// repository", meaning this one. So this sweeps every node AngouriMath can print and asserts
18+
/// the LaTeX comes back through the parser. Each failure here is a crash waiting for a user.
19+
/// </summary>
20+
public class AngouriMathLatexSweepTests {
21+
/// <param name="Strict">
22+
/// False for nodes built by reflection: filling every argument with <c>x</c> can produce a node
23+
/// that is not well-formed, so a throw out of <c>Latexize</c> is not evidence of a defect. The
24+
/// hand-built shapes below are strict, because those are known-good expressions.
25+
/// </param>
26+
record Case(string Name, Entity Node, bool Strict);
27+
28+
static IEnumerable<Case> Nodes() {
29+
var x = MathS.Var("x");
30+
var y = MathS.Var("y");
31+
foreach (var t in typeof(Entity).Assembly.GetTypes()
32+
.Where(t => typeof(Entity).IsAssignableFrom(t) && !t.IsAbstract && !t.IsGenericTypeDefinition)
33+
.OrderBy(t => t.Name, StringComparer.Ordinal)) {
34+
Entity? built = null;
35+
foreach (var ctor in t.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
36+
.OrderBy(c => c.GetParameters().Length)) {
37+
var ps = ctor.GetParameters();
38+
if (ps.Length is 0 or > 4) continue;
39+
if (!ps.All(p => typeof(Entity).IsAssignableFrom(p.ParameterType))) continue;
40+
try { built = (Entity)ctor.Invoke(ps.Select(_ => (object)x).ToArray()); break; } catch { }
41+
}
42+
if (built is not null) yield return new(t.Name, built, false);
43+
}
44+
// Shapes reflection cannot reach, listed by hand because they are exactly the ones whose
45+
// LaTeX is unusual.
46+
Case Strict(string name, Entity node) => new(name, node, true);
47+
yield return Strict("Matrix", MathS.Vector(1, 2, 3));
48+
yield return Strict("Matrix2x2", MathS.Matrix(new Entity[,] { { 1, 2 }, { 3, 4 } }));
49+
yield return Strict("Piecewise", MathS.Piecewise((x, x > 0), (y, y > 0)));
50+
yield return Strict("Integral", MathS.Integral(x, x));
51+
yield return Strict("IntegralRanged", MathS.Integral(x, x, 0, 1));
52+
yield return Strict("Derivative", MathS.Derivative(x, x));
53+
yield return Strict("Limit", MathS.Limit(x, x, 0));
54+
yield return Strict("Interval", new Entity.Set.Interval(0, true, 1, true));
55+
yield return Strict("FiniteSet", new Entity.Set.FiniteSet(1, 2, 3));
56+
yield return Strict("ConditionalSet", "{ x : x > 0 }".ToEntity());
57+
yield return Strict("Integers", "ZZ".ToEntity());
58+
yield return Strict("Reals", "RR".ToEntity());
59+
yield return Strict("Complexes", "CC".ToEntity());
60+
yield return Strict("Rationals", "QQ".ToEntity());
61+
yield return Strict("Booleans", "BB".ToEntity());
62+
yield return Strict("Rational", "3/2".ToEntity());
63+
yield return Strict("ComplexNumber", MathS.Numbers.Create(1, 2));
64+
yield return Strict("Factorial", MathS.Factorial(x));
65+
yield return Strict("Union", MathS.Union("A".ToEntity(), "B".ToEntity()));
66+
yield return Strict("Intersection", MathS.Intersection("A".ToEntity(), "B".ToEntity()));
67+
yield return Strict("SetMinus", MathS.SetSubtraction("A".ToEntity(), "B".ToEntity()));
68+
yield return Strict("In", "x in RR".ToEntity());
69+
yield return Strict("Provided", "x provided x > 0".ToEntity());
70+
yield return Strict("Apply", "apply(f, x)".ToEntity());
71+
yield return Strict("Lambda", "lambda(x, x^2)".ToEntity());
72+
yield return Strict("Domain", "domain(x, RR)".ToEntity());
73+
yield return Strict("Signum", MathS.Signum(x));
74+
yield return Strict("Abs", MathS.Abs(x));
75+
yield return Strict("Modulo", MathS.Mod(x, y));
76+
yield return Strict("EulerTotient", MathS.NumberTheory.Phi(x));
77+
}
78+
79+
[Fact]
80+
public void EveryAngouriMathNodeLatexizesIntoSomethingCSharpMathCanParse() {
81+
var failures = new List<string>();
82+
var checkedCount = 0;
83+
foreach (var (name, node, strict) in Nodes()) {
84+
string latex;
85+
try {
86+
latex = node.Latexize();
87+
} catch (Exception e) {
88+
if (strict) failures.Add($"{name}: Latexize threw {e.GetType().Name}: {e.Message}");
89+
continue;
90+
}
91+
checkedCount++;
92+
LaTeXParser.MathListFromLaTeX(latex)
93+
.Match(_ => { }, err => failures.Add($"{name}: {latex}{Environment.NewLine} -> {err}"));
94+
}
95+
// A guard that checks nothing would also report no failures.
96+
Assert.True(checkedCount > 50, $"only {checkedCount} nodes reached the parser");
97+
Assert.True(failures.Count == 0,
98+
$"checked {checkedCount} nodes, {failures.Count} unparseable:{Environment.NewLine} "
99+
+ string.Join(Environment.NewLine + " ", failures));
100+
}
101+
}
102+
}

CSharpMath.Evaluation.Tests/EvaluationTests.cs

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,20 @@ public void Numbers(string input, string converted, string output) =>
179179
[InlineData(@"a / bc / d", @"\frac{\frac{a}{bc}}{d}", @"\frac{a}{bcd}")]
180180
[InlineData(@"-2/\sin x/y", @"\frac{\frac{-2}{\sin \left( x\right) }}{y}", @"\frac{-2}{\sin \left( x\right) \cdot y}")]
181181
public void BinaryOperators(string latex, string converted, string result) => Test(latex, converted, result);
182+
/// <summary>
183+
/// The two forms AngouriMath emits that had no reading here: <c>\bmod</c> for its modulo node
184+
/// and <c>\operatorname{\varphi}</c> for Euler's totient. <c>\bmod</c> binds like multiplication
185+
/// and division, which is AngouriMath's <c>Priority.Mul</c>, so the grouping cases below are
186+
/// the point rather than decoration.
187+
/// </summary>
188+
[Theory]
189+
[InlineData(@"x\bmod y", @"x\bmod y", @"x\bmod y")]
190+
[InlineData(@"7\bmod 3", @"7\bmod 3", @"1")]
191+
[InlineData(@"x+y\bmod z", @"x+y\bmod z", @"x+y\bmod z")]
192+
[InlineData(@"x\bmod y+z", @"x\bmod y+z", @"x\bmod y+z")]
193+
[InlineData(@"\operatorname{\varphi}(10)", @"\operatorname{φ} \left( 10\right) ", @"4")]
194+
[InlineData(@"\operatorname{\varphi}(x)", @"\operatorname{φ} \left( x\right) ", @"\operatorname{φ} \left( x\right) ")]
195+
public void AngouriMathOnlyForms(string latex, string converted, string result) => Test(latex, converted, result);
182196
[Theory]
183197
[InlineData(@"+i", @"\mathrm{i}", @"\mathrm{i}")]
184198
[InlineData(@"-i", @"-\mathrm{i}", @"-\mathrm{i}")]
@@ -341,7 +355,7 @@ public void Numbers(string input, string converted, string output) =>
341355
[InlineData(@"\sin \frac\pi2", @"\sin \left( \frac{\mathrm{\pi }}{2}\right) ", @"1")]
342356
[InlineData(@"\sin \frac\pi2+1", @"\sin \left( \frac{\mathrm{\pi }}{2}\right) +1", @"2")]
343357
[InlineData(@"\cos +x", @"\cos \left( x\right) ", @"\cos \left( x\right) ")]
344-
[InlineData(@"\cos -x", @"\cos \left( -x\right) ", @"\cos \left( -x\right) ")]
358+
[InlineData(@"\cos -x", @"\cos \left( -x\right) ", @"\cos \left( x\right) ")] // 2.2.0 uses evenness
345359
[InlineData(@"\tan x\%", @"\tan \left( \frac{x}{100}\right) ", @"\tan \left( \frac{x}{100}\right) ")]
346360
[InlineData(@"\tan x\%^2", @"\tan \left( \left( \frac{x}{100}\right) ^2\right) ", @"\tan \left( \left( \frac{x}{100}\right) ^2\right) ")]
347361
[InlineData(@"\cot x\times y", @"\cot \left( x\right) \cdot y", @"\cot \left( x\right) \cdot y")]
@@ -391,7 +405,8 @@ public void Numbers(string input, string converted, string output) =>
391405
[InlineData(@"\tan^{-1} x\%^2", @"\arctan \left( \left( \frac{x}{100}\right) ^2\right) ", @"\arctan \left( \left( \frac{x}{100}\right) ^2\right) ")]
392406
[InlineData(@"\cot^{-1} x\times y", @"\arccot \left( x\right) \cdot y", @"\arccot \left( x\right) \cdot y")]
393407
[InlineData(@"\cot^{-1} x/y", @"\frac{\arccot \left( x\right) }{y}", @"\frac{\arccot \left( x\right) }{y}")]
394-
[InlineData(@"\cos^{-1} \arccos^{-1} x", @"\arccos \left( \cos \left( x\right) \right) ", @"x")]
408+
// arccos(cos x) is x only on [0, pi]; AngouriMath 2.2.0 no longer claims it in general.
409+
[InlineData(@"\cos^{-1} \arccos^{-1} x", @"\arccos \left( \cos \left( x\right) \right) ", @"\arccos \left( \cos \left( x\right) \right) ")]
395410
[InlineData(@"\sin^1 x", @"\sin \left( x\right) ^1", @"\sin \left( x\right) ")]
396411
[InlineData(@"\sin^{+1} x", @"\sin \left( x\right) ^1", @"\sin \left( x\right) ")]
397412
[InlineData(@"\sin^{+-1} x", @"\sin \left( x\right) ^{-1}", @"\csc \left( x\right) ")]
@@ -869,9 +884,10 @@ public void SimpleArithmeticSyntax(string simpleSyntax, string latex) =>
869884
[InlineData(@"\top\nleftrightarrow\bot", @"\top \veebar \bot ", @"\top ")]
870885
[InlineData(@"\bot\nleftrightarrow\bot", @"\bot \veebar \bot ", @"\bot ")]
871886
[InlineData(@"x=x", @"x=x", @"\top ")]
872-
[InlineData(@"x\le x", @"x\leq x", @"\top ")]
873-
[InlineData(@"x\leq x", @"x\leq x", @"\top ")]
874-
[InlineData(@"x\leqslant x", @"x\leq x", @"\top ")]
887+
// AngouriMath 2.2.0 carries the domain <= needs rather than asserting the tautology outright.
888+
[InlineData(@"x\le x", @"x\leq x", @"\top \quad \mathrm{for}\quad x\in \mathbb{R}")]
889+
[InlineData(@"x\leq x", @"x\leq x", @"\top \quad \mathrm{for}\quad x\in \mathbb{R}")]
890+
[InlineData(@"x\leqslant x", @"x\leq x", @"\top \quad \mathrm{for}\quad x\in \mathbb{R}")]
875891
[InlineData(@"x\neq y", @"x\neq y", @"x\neq y")] // Cannot simplify without knowing x and y
876892
[InlineData(@"1<2", @"1<2", @"\top ")]
877893
[InlineData(@"2<1", @"2<1", @"\bot ")]
@@ -951,7 +967,8 @@ public void ChainedComparisons(string latex, string converted, string result, st
951967
[InlineData(@"-\operatorname{abs}(-1)", @"-\left| -1\right| ", @"-1")]
952968
[InlineData(@"-\operatorname{abs}\left|-1\right|", @"-\left| \left| -1\right| \right| ", @"-1")]
953969
[InlineData(@"-\left|1\right|^2", @"-\left| 1\right| ^2", @"-1")]
954-
[InlineData(@"\operatorname{sgn}\operatorname{abs} x", @"\operatorname{sgn} \left( \left| x\right| \right) ", @"1")]
970+
// AngouriMath 2.2.0 no longer answers 1 here: sgn(|x|) is 1 away from zero but 0 at zero.
971+
[InlineData(@"\operatorname{sgn}\operatorname{abs} x", @"\operatorname{sgn} \left( \left| x\right| \right) ", @"\operatorname{sgn} \left( \left| x\right| \right) ")]
955972
public void Abs(string latex, string converted, string result) => Test(latex, converted, result);
956973
[Theory]
957974
[InlineData(@"\lim_{x\to2}x+1", @"\lim _{x\rightarrow 2}x+1", @"3")]

CSharpMath.Evaluation.Tests/InterpretTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public class InterpretTests {
1010
[InlineData(@"1+2", @"\underline\mathrm{Input}\\1+2\\\\\underline\mathrm{Simplified}\\3\\\\\underline\mathrm{Value\ (100\ digits)}\\3")]
1111
[InlineData(@"1+\sqrt", @"\color{red}\text{Missing radicand}")]
1212
[InlineData(@"1+\sqrt2", @"\underline\mathrm{Input}\\1+\sqrt{2}\\\\\underline\mathrm{Simplified}\\1+\sqrt{2}\\\\\underline\mathrm{Value\ (100\ digits)}\\2.414213562373095048801688724209698078569671875376948073176679737990732478462107038850387534327641573")]
13-
[InlineData(@"1+\sqrt{2x}", @"\underline\mathrm{Input}\\1+\sqrt{2 x}\\\\\underline\mathrm{Simplified}\\1+\sqrt{2 x}\\\\\underline\mathrm{Expanded}\\1+\sqrt{2 x}\\\\\underline\mathrm{Factorized}\\1+\sqrt{2 x}")]
13+
[InlineData(@"1+\sqrt{2x}", @"\underline\mathrm{Input}\\1+\sqrt{2 x}\\\\\underline\mathrm{Simplified}\\1+\sqrt{2 x}\\\\\underline\mathrm{Expanded}\\1+\sqrt{2} \sqrt{x}\\\\\underline\mathrm{Factorized}\\1+\sqrt{2 x}")]
1414
[InlineData(@"1+\sqrt{2xy}", @"\underline\mathrm{Input}\\1+\sqrt{2 x y}\\\\\underline\mathrm{Simplified}\\1+\sqrt{2 x y}\\\\\underline\mathrm{Expanded}\\1+\sqrt{2 x y}\\\\\underline\mathrm{Factorized}\\1+\sqrt{2 x y}")]
1515
[InlineData(@"=1+\sqrt{2xy}", @"\color{red}\text{Missing left side of equation}")]
1616
[InlineData(@"1+\sqrt{2xy}=", @"\color{red}\text{Missing right side of equation}")]

CSharpMath.Evaluation/CSharpMath.Evaluation.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
<ItemGroup>
1515
<ProjectReference Include="..\CSharpMath\CSharpMath.csproj" />
16-
<PackageReference Include="AngouriMath" Version="1.4.0" />
16+
<PackageReference Include="AngouriMath" Version="2.2.0" />
1717
</ItemGroup>
1818

1919
</Project>

CSharpMath.Evaluation/Evaluation.cs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,20 @@ enum Precedence {
4242
Postfix
4343
// Highest
4444
}
45-
public abstract record MathItem : ILatexiseable {
45+
public abstract record MathItem : ILatexizeable {
4646
private protected MathItem() { }
4747
public abstract string Latexise();
48+
// AngouriMath 2.0 renamed ILatexiseable.Latexise to ILatexizeable.Latexize. Implementing
49+
// it explicitly keeps MathItem.Latexise as the public name here, so this package's own
50+
// surface is unchanged by their rename.
51+
string ILatexizeable.Latexize() => Latexise();
4852
public static implicit operator MathItem(AngouriMath.Entity content) => new Entity(content);
4953
public static explicit operator AngouriMath.Entity(MathItem item) => ((Entity)item).Content;
5054
/// <summary>A real number, complex number, variable, function call, vector, matrix, higher-dimensional tensor, or set</summary>
5155
public sealed record Entity : MathItem {
5256
public Entity(AngouriMath.Entity content) => Content = content;
5357
public AngouriMath.Entity Content { get; }
54-
public override string Latexise() => Content.Latexise();
58+
public override string Latexise() => Content.Latexize();
5559
}
5660
/// <summary>A linked list of comma-delimited items</summary>
5761
public sealed record Comma : MathItem, IEnumerable<MathItem> {
@@ -494,6 +498,12 @@ string GreekToLaTeXCommandName(string n) =>
494498
handleFunction = MathS.Signum;
495499
handleFunctionInverse = arg => MathS.NaN;
496500
goto handleFunction;
501+
// Euler's totient, which AngouriMath writes \operatorname{\varphi}. It is not injective
502+
// (φ(1) = φ(2) = 1), so there is no inverse to offer -- as for abs and sgn above.
503+
case Atoms.LargeOperator { Nucleus: "φ" }:
504+
handleFunction = MathS.NumberTheory.Phi;
505+
handleFunctionInverse = arg => MathS.NaN;
506+
goto handleFunction;
497507
case Atoms.LargeOperator { Nucleus: "lim", Subscript: var limitSubscript }:
498508
Entity limitVariable, limitTarget;
499509
int limitSubscriptIndex = 0;
@@ -563,6 +573,12 @@ string GreekToLaTeXCommandName(string n) =>
563573
handlePrecedence = Precedence.MultiplicationDivision;
564574
handleBinary = (a, b) => a / b;
565575
goto handleBinary;
576+
// \bmod, which AngouriMath emits for its modulo node. It binds like multiplication and
577+
// division there too (Priority.Mul), so a+b \bmod c is a+(b mod c) on both sides.
578+
case Atoms.BinaryOperator { Nucleus: "mod" }:
579+
handlePrecedence = Precedence.MultiplicationDivision;
580+
handleBinary = MathS.Mod;
581+
goto handleBinary;
566582
case Atoms.Ordinary { Nucleus: "%" }:
567583
handlePostfix = x => x / 100;
568584
goto handlePostfix;

CSharpMath.Evaluation/Interpret.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33

44
namespace CSharpMath {
55
static partial class Evaluation {
6-
static StringBuilder AppendLaTeX(this StringBuilder sb, AngouriMath.Core.ILatexiseable latex) =>
7-
sb.Append(latex.Latexise());
6+
static StringBuilder AppendLaTeX(this StringBuilder sb, AngouriMath.Core.ILatexizeable latex) =>
7+
sb.Append(latex.Latexize());
88
static StringBuilder AppendLaTeXHeader(this StringBuilder sb, string header, bool includeNewlineBefore = true) {
99
if (includeNewlineBefore) sb.Append(@"\\\\");
1010
return sb.Append(@"\underline\mathrm{").Append(header).Append(@"}\\");
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using Xunit;
2+
3+
namespace CSharpMath.Rendering.Tests {
4+
/// <summary>
5+
/// <c>\bmod</c> is the first binary operator here whose nucleus is a word rather than a symbol,
6+
/// and <c>\operatorname{φ}</c> the first operator name written with a non-ASCII letter. Both come
7+
/// from AngouriMath. Glyph coverage is already asserted by
8+
/// <see cref="TestCommandDisplay.CommandsAreDisplayable"/>; what is new is the layout, so this
9+
/// measures rather than comparing against a baseline image.
10+
/// </summary>
11+
public class TestAngouriMathForms {
12+
static System.Drawing.RectangleF Measure(string latex) {
13+
var painter = new SkiaSharp.MathPainter { LaTeX = latex };
14+
Assert.Null(painter.ErrorMessage);
15+
return painter.Measure(FrontEnd.TextPainter<global::SkiaSharp.SKCanvas, global::SkiaSharp.SKColor>.DefaultCanvasWidth);
16+
}
17+
18+
[Theory]
19+
[InlineData(@"x\bmod y")]
20+
[InlineData(@"\operatorname{φ}\left( x\right) ")]
21+
[InlineData(@"\operatorname{φ}\left( x\bmod y\right) ")]
22+
public void TheyLayOut(string latex) {
23+
var measured = Measure(latex);
24+
Assert.True(measured.Width > 0, $"zero width for {latex}");
25+
Assert.True(measured.Height > 0, $"zero height for {latex}");
26+
}
27+
28+
/// <summary>
29+
/// The three letters of "mod" and the spacing around a binary operator are actually laid out,
30+
/// rather than the atom occupying no room -- which a zero-width check alone would not catch,
31+
/// since x and y are there either way.
32+
/// </summary>
33+
[Fact]
34+
public void ModuloTakesUpRoom() =>
35+
Assert.True(Measure(@"x\bmod y").Width > Measure(@"xy").Width * 2);
36+
}
37+
}

0 commit comments

Comments
 (0)