Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ generated parser, on netstandard2.0): keep it so, and keep containers and other
| `src/SharpEquations.Avalonia/` | The controls for Avalonia: `MathEditBox` (the editor), `MathView`; embeds Libertinus Math |
| `samples/SharpEquations.Avalonia.Demo/` | A window with the editor (`DemoView`, shared with the web demo) |
| `samples/SharpEquations.Avalonia.Browser.Demo/` | The same demo on WebAssembly (needs the `wasm-tools` workload, so not in the solution; `.github/workflows/demo.yml` publishes it to GitHub Pages) |
| `samples/SharpEquations.Avalonia.MathTools.Demo/` | A second app, math tools around the editor (`MathToolsView`, a page each: the scientific keypad, the library of formulas, the unit converter, the solver of triangles) |
| `samples/SharpEquations.Avalonia.MathTools.Demo/` | A second app, math tools around the editor (`MathToolsView`, a page each: the scientific keypad, the library of formulas, the unit converter, the solver of triangles; the calculator computes with AngouriMath, through `MathReader`, which reads what the normalized presentation MathML means) |
| `samples/SharpEquations.Avalonia.Browser.MathTools.Demo/` | The math tools on WebAssembly (as the web demo, not in the solution build; `demo.yml` publishes them under `math/`) |
| `benchmarks/SharpEquations.Benchmarks/` | BenchmarkDotNet benchmarks of reading, writing and converting the corpora |
| `tests/SharpEquations.Tests/` | TUnit tests, in folders by subject (PublicApi, MathML, Omml, Mtef, UnicodeMath, LaTeX, Property, Regression) |
Expand Down
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<PackageVersion Include="Avalonia.Headless" Version="12.1.4" />
<PackageVersion Include="Avalonia.Skia" Version="12.1.4" />
<PackageVersion Include="Avalonia.Themes.Fluent" Version="12.1.4" />
<PackageVersion Include="AngouriMath" Version="2.6.0" />
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.401" PrivateAssets="All" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,9 @@ three projects, so that the core keeps no dependency: `SharpEquations.Editing` (
UnicodeMath and LaTeX, and `samples/SharpEquations.Avalonia.Browser.Demo` the same demo in the browser, on WebAssembly
(published to GitHub Pages from `main`; to run it locally: `dotnet workload install wasm-tools`, then
`dotnet run --project samples/SharpEquations.Avalonia.Browser.Demo`). `samples/SharpEquations.Avalonia.MathTools.Demo` is a
second app around the editor, a set of math tools (a scientific keypad that types into the editor, a library of
formulas, a unit converter, a solver of triangles), with
second app around the editor, a set of math tools (a scientific keypad that types into the editor and computes what is
typed with [AngouriMath](https://github.com/asc-community/AngouriMath), a library of formulas, a unit converter, a
solver of triangles), with
`samples/SharpEquations.Avalonia.Browser.MathTools.Demo` in the browser (published under `math/` of the same site).

## Architecture
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<PackageReference Include="Avalonia.Browser" />
<PackageReference Include="Avalonia.Fonts.Inter" />
<PackageReference Include="Avalonia.Themes.Fluent" />
<PackageReference Include="AngouriMath" />
</ItemGroup>

<ItemGroup>
Expand Down
118 changes: 118 additions & 0 deletions samples/SharpEquations.Avalonia.MathTools.Demo/Algebra.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
namespace SharpEquations.Avalonia.MathTools
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using AngouriMath;
using SharpEquations;

/// <summary>A result of <see cref="Algebra"/>: what was done, and the result as an equation (null if it cannot be drawn) and as text.</summary>
public sealed record AlgebraResult(string Action, Equation? Math, string Text);

/// <summary>
/// What the calculator computes with AngouriMath: the value of an expression (exact, and as a decimal number),
/// the solutions of an equation, simplification, expansion, factoring, derivatives and integrals. The results come
/// back through LaTeX, which AngouriMath writes and SharpEquations reads.
/// </summary>
public static class Algebra
{
public const string Evaluate = "Evaluate";
public const string Solve = "Solve";
public const string Simplify = "Simplify";
public const string Expand = "Expand";
public const string Factor = "Factor";
public const string Differentiate = "Differentiate";
public const string Integrate = "Integrate";

/// <summary>The actions, in the order of the buttons.</summary>
public static readonly IReadOnlyList<string> Actions = new[] { Evaluate, Solve, Simplify, Expand, Factor, Differentiate, Integrate };

/// <summary>The letters of an expression, x first when there is one.</summary>
public static IReadOnlyList<string> Variables(Entity expression) =>
expression.Vars.Select(v => v.Name).Distinct().OrderBy(v => v == "x" ? 0 : 1).ThenBy(v => v, StringComparer.Ordinal).ToList();

/// <summary>Does an action to an expression read by <see cref="MathReader"/>; <paramref name="variable"/> is the letter to solve for, or to differentiate and integrate by.</summary>
public static AlgebraResult Run(string action, Entity expression, string? variable)
{
var letters = Variables(expression);
var x = MathS.Var(variable ?? (letters.Count > 0 ? letters[0] : "x"));
switch (action)
{
case Evaluate:
return Value(expression);
case Solve:
var equation = expression is Entity.Equalsf ? expression : expression.Equalizes(0);
var solutions = equation.Solve(x).Simplify();
return Result(Solve, Latex(x) + @"\in " + Latex(solutions), x.ToString() + " ∈ " + solutions.ToString());
case Simplify:
return Result(Simplify, expression.Simplify());
case Expand:
return Result(Expand, expression.Expand());
case Factor:
return Result(Factor, expression.Factorize());
case Differentiate:
return Result(Differentiate, expression.Differentiate(x).Simplify());
case Integrate:
return Result(Integrate, expression.Integrate(x).Simplify());
default:
throw new ArgumentOutOfRangeException(nameof(action), action, "Not an action of the calculator.");
}
}

// the exact value, and the decimal one when it differs: 1/3 ≈ 0.333333, √2 ≈ 1.41421
private static AlgebraResult Value(Entity expression)
{
if (expression is Entity.Equalsf || expression.Vars.Any())
return Result(Evaluate, expression.Simplify());

var exact = expression.Simplify();
var approximate = Decimal(exact.EvalNumerical());
var exactLatex = Latex(exact);
var exactText = exact.ToString();
if (approximate == null || approximate == exactText)
return Result(Evaluate, exactLatex, exactText);
return Result(Evaluate, exactLatex + @"\approx " + approximate, exactText + " ≈ " + approximate);
}

/// <summary>A number of AngouriMath as the calculator shows it: ten significant digits, <c>a+bi</c> for a complex one; null for what is not a number.</summary>
public static string? Decimal(Entity value)
{
switch (value)
{
case Entity.Number.Real real:
return Format(real.EDecimal.ToDouble());
case Entity.Number.Complex complex:
var re = complex.RealPart.EDecimal.ToDouble();
var im = complex.ImaginaryPart.EDecimal.ToDouble();
if (Math.Abs(im) < 1e-12 * Math.Max(1, Math.Abs(re)))
return Format(re);
var imaginary = (Math.Abs(im) == 1 ? "" : Format(Math.Abs(im))) + "i";
return Math.Abs(re) < 1e-12 * Math.Abs(im) ? (im < 0 ? "-" : "") + imaginary : Format(re) + (im < 0 ? "-" : "+") + imaginary;
default:
return null;
}
}

private static string Format(double value) =>
double.IsInfinity(value) ? (value > 0 ? "∞" : "-∞") : value.ToString("G10", CultureInfo.InvariantCulture);

private static AlgebraResult Result(string action, Entity value) => Result(action, Latex(value), value.ToString());

private static AlgebraResult Result(string action, string latex, string text)
{
var options = new EquationOptions { Display = true };
var math = Equation.TryParse(latex, EquationFormat.LaTeX, options, out var parsed)
&& !parsed!.ToMathML().Descendants().Any(e => e.Name.LocalName == "merror")
? parsed
: null;
return new AlgebraResult(action, math, text);
}

// the LaTeX of AngouriMath, with its constants as the LaTeX reader knows them
private static string Latex(Entity value) => value.Latexize()
.Replace(@"\mathrm{\pi}", @"\pi", StringComparison.Ordinal)
.Replace(@"\mathrm{e}", "e", StringComparison.Ordinal)
.Replace(@"\mathrm{i}", "i", StringComparison.Ordinal);
}
}
233 changes: 233 additions & 0 deletions samples/SharpEquations.Avalonia.MathTools.Demo/AlgebraPanel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
namespace SharpEquations.Avalonia.MathTools
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AngouriMath;
using global::Avalonia;
using global::Avalonia.Controls;
using global::Avalonia.Layout;
using global::Avalonia.Threading;
using SharpEquations;

/// <summary>
/// What the calculator computes from the equation of the editor, with AngouriMath: its value as it is typed, and on a
/// click its solutions, its simplified, expanded or factored form, its derivative and its integral, each result
/// drawn and ready to go back to the editor.
/// </summary>
public sealed class AlgebraPanel : UserControl
{
private static readonly TimeSpan Delay = TimeSpan.FromMilliseconds(300);
private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(10);

private static readonly (string Action, string Face, string Tip)[] Buttons =
{
(Algebra.Solve, "Solve", "The solutions of the equation, or where the expression is 0"),
(Algebra.Simplify, "Simplify", "The expression in a simpler form"),
(Algebra.Expand, "Expand", "Products and powers multiplied out"),
(Algebra.Factor, "Factor", "The expression as a product"),
(Algebra.Differentiate, "d/dx", "The derivative by the letter chosen"),
(Algebra.Integrate, "∫ dx", "An antiderivative by the letter chosen"),
};

private readonly MathEditBox editor;
private readonly MathView answer = new MathView { FontSize = 26, HorizontalAlignment = HorizontalAlignment.Left };
private readonly TextBlock answerNote = Ui.Muted("", 12.5);
private readonly StackPanel results = new StackPanel { Spacing = 12 };
private readonly DispatcherTimer timer;
private int version;

public AlgebraPanel(MathEditBox editor)
{
this.editor = editor;
Letter = new ComboBox { MinWidth = 70 };
timer = new DispatcherTimer { Interval = Delay };
timer.Tick += async (_, _) =>
{
timer.Stop();
await RefreshAnswer().ConfigureAwait(true);
};
editor.EquationChanged += (_, _) =>
{
timer.Stop();
timer.Start();
};

var buttons = new List<Control>();
foreach (var (action, face, tip) in Buttons)
{
var button = new Button { Content = face, CornerRadius = new CornerRadius(8), Padding = new Thickness(12, 5) };
ToolTip.SetTip(button, tip);
button.Click += async (_, _) => await RunAsync(action).ConfigureAwait(true);
buttons.Add(button);
}
var forLabel = Ui.Muted("by", 12.5);
buttons.Add(forLabel);
buttons.Add(Letter);

var answerFrame = new Border
{
CornerRadius = new CornerRadius(12),
Padding = new Thickness(18, 12),
MinHeight = 60,
Child = new StackPanel { Spacing = 4, Children = { answer, answerNote } },
};
answerFrame.Bind(Border.BackgroundProperty, answerFrame.GetResourceObservable("CodeBrush"));

Content = new StackPanel { Spacing = 14, Children = { answerFrame, Ui.Row(buttons.ToArray()), results } };
ShowAnswer(null, "The value of what is typed shows here.");
}

/// <summary>The letter to solve for, and to differentiate and integrate by.</summary>
public ComboBox Letter { get; }

/// <summary>The value of the equation of the editor, as it was last computed.</summary>
public AlgebraResult? Answer { get; private set; }

/// <summary>The results of the actions, the last first.</summary>
public IReadOnlyList<AlgebraResult> Results => history;

private readonly List<AlgebraResult> history = new List<AlgebraResult>();

/// <summary>Computes the value of the equation of the editor now, as typing does after a pause.</summary>
public async Task RefreshAnswer()
{
timer.Stop();
var current = ++version;
if (!TryRead(out var expression, out var error))
{
UpdateLetters(null);
ShowAnswer(null, error);
return;
}
UpdateLetters(expression);
var (result, message) = await Compute(Algebra.Evaluate, expression!, null).ConfigureAwait(true);
if (current == version)
ShowAnswer(result, message);
}

/// <summary>Does an action to the equation of the editor, as its button does, and adds the result to the list.</summary>
public async Task<AlgebraResult?> RunAsync(string action)
{
if (!TryRead(out var expression, out var error))
{
AddMessage(error!);
return null;
}
var letter = Letter.SelectedItem as string;
var (result, message) = await Compute(action, expression!, letter).ConfigureAwait(true);
if (result == null)
{
AddMessage(message!);
return null;
}
history.Insert(0, result);
results.Children.Insert(0, ResultCard(result, Label(action, letter ?? "x")));
while (results.Children.Count > 8)
results.Children.RemoveAt(results.Children.Count - 1);
return result;
}

private bool TryRead(out Entity? expression, out string? error)
{
expression = null;
error = null;
var equation = editor.Equation;
if (equation == null)
{
error = "The value of what is typed shows here.";
return false;
}
if (equation.ToMathML().Descendants().Any(e => e.Name.LocalName == "merror"))
{
error = "Not built up yet: finish the argument, or press Build up (Enter).";
return false;
}
try
{
expression = MathReader.Read(equation);
return true;
}
catch (NotSupportedException e)
{
error = e.Message;
return false;
}
}

// on another thread, since AngouriMath can take long (it cannot be stopped: a result too late is dropped)
private static async Task<(AlgebraResult? Result, string? Message)> Compute(string action, Entity expression, string? letter)
{
var work = Task.Run(() => Algebra.Run(action, expression, letter));
if (await Task.WhenAny(work, Task.Delay(Timeout)).ConfigureAwait(true) != work)
return (null, "It takes too long: AngouriMath found no result in " + Timeout.TotalSeconds + " seconds.");
try
{
return (await work.ConfigureAwait(true), null);
}
#pragma warning disable CA1031 // what AngouriMath cannot do, it says with exceptions of many kinds
catch (Exception e)
#pragma warning restore CA1031
{
return (null, "AngouriMath cannot do it: " + e.Message);
}
}

private void UpdateLetters(Entity? expression)
{
var letters = expression == null ? Array.Empty<string>() : Algebra.Variables(expression);
var selected = Letter.SelectedItem as string;
if (Letter.ItemsSource is IReadOnlyList<string> old && old.SequenceEqual(letters))
return;
Letter.ItemsSource = letters;
Letter.SelectedItem = selected != null && letters.Contains(selected) ? selected : letters.Count > 0 ? letters[0] : null;
}

private void ShowAnswer(AlgebraResult? result, string? message)
{
Answer = result;
answer.Equation = result?.Math;
answer.IsVisible = result?.Math != null;
answerNote.Text = result == null ? message : result.Math == null ? result.Text : null;
answerNote.IsVisible = !string.IsNullOrEmpty(answerNote.Text);
}

private void AddMessage(string message)
{
results.Children.Insert(0, Ui.Muted(message, 13));
while (results.Children.Count > 8)
results.Children.RemoveAt(results.Children.Count - 1);
}

private Border ResultCard(AlgebraResult result, string label)
{
Control math = result.Math != null
? new MathView { FontSize = 22, Equation = result.Math, HorizontalAlignment = HorizontalAlignment.Left }
: new SelectableTextBlock { Text = result.Text, FontSize = 14, TextWrapping = global::Avalonia.Media.TextWrapping.Wrap };
var use = new Button { Content = "To the editor", CornerRadius = new CornerRadius(8), Padding = new Thickness(10, 3), IsEnabled = result.Math != null };
use.Click += (_, _) =>
{
editor.Equation = result.Math;
editor.Focus();
};
var top = new DockPanel { Children = { Ui.Docked(use, Dock.Right), Ui.Small(label) } };
var card = new Border
{
CornerRadius = new CornerRadius(12),
Padding = new Thickness(16, 10),
Child = new StackPanel { Spacing = 8, Children = { top, math } },
};
card.Bind(Border.BackgroundProperty, card.GetResourceObservable("CodeBrush"));
return card;
}

private static string Label(string action, string letter) => action switch
{
Algebra.Solve => "Solve for " + letter,
Algebra.Differentiate => "Derivative by " + letter,
Algebra.Integrate => "Antiderivative by " + letter,
_ => action,
};
}
}
Loading
Loading