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) |
| `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) |
| `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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ UnicodeMath and LaTeX, and `samples/SharpEquations.Avalonia.Browser.Demo` the sa
(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), with
formulas, a unit converter), 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 @@ -9,8 +9,8 @@ namespace SharpEquations.Avalonia.MathTools
using SharpEquations;

/// <summary>
/// The math tools, a page each: the scientific keypad around the equation editor, and the library of formulas. The
/// desktop window and the WebAssembly page show the same view.
/// The math tools, a page each: the scientific keypad around the equation editor, the library of formulas and the
/// unit converter. The desktop window and the WebAssembly page show the same view.
/// </summary>
public sealed class MathToolsView : UserControl
{
Expand All @@ -20,6 +20,7 @@ public MathToolsView()
{
Ui.AddBrushes(Resources);
Calculator = new CalculatorPage();
Converter = new UnitsPage();
Tabs = new TabControl { Margin = new Thickness(20, 12, 20, 0) };
FormulaLibrary = new FormulasPage(equation =>
{
Expand All @@ -31,6 +32,7 @@ public MathToolsView()
{
Tab("Calculator", Calculator),
Tab("Formulas", FormulaLibrary),
Tab("Units", Converter),
};

var root = new DockPanel { Children = { Ui.Docked(Header(), Dock.Top), Tabs } };
Expand All @@ -44,6 +46,9 @@ public MathToolsView()
/// <summary>The page of the library of formulas.</summary>
public FormulasPage FormulaLibrary { get; }

/// <summary>The page of the unit converter.</summary>
public UnitsPage Converter { get; }

/// <summary>The tabs of the pages.</summary>
public TabControl Tabs { get; }

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

/// <summary>A unit of a quantity: a value in it is <c>value × Factor + Offset</c> in the base unit of the quantity.</summary>
public sealed class Unit
{
public Unit(string name, string symbol, double factor, double offset = 0)
{
Name = name;
Symbol = symbol;
Factor = factor;
Offset = offset;
}

public string Name { get; }

public string Symbol { get; }

public double Factor { get; }

public double Offset { get; }

public override string ToString() => $"{Name} ({Symbol})";
}

/// <summary>A quantity and its units, the first of which is the base unit.</summary>
public sealed class Quantity
{
public Quantity(string name, params Unit[] units)
{
Name = name;
Units = units;
}

public string Name { get; }

public IReadOnlyList<Unit> Units { get; }

public Unit this[string symbol] => Units.Single(u => u.Symbol == symbol);

public override string ToString() => Name;
}

/// <summary>The quantities of the converter, and the conversion and the formatting of the values.</summary>
public static class Units
{
private const double Inch = 0.0254;
private const double Pound = 0.45359237;
private const double Gallon = 3.785411784e-3;

public static IReadOnlyList<Quantity> All { get; } = new[]
{
new Quantity("Length",
new Unit("metre", "m", 1), new Unit("kilometre", "km", 1e3), new Unit("centimetre", "cm", 1e-2), new Unit("millimetre", "mm", 1e-3),
new Unit("micrometre", "μm", 1e-6), new Unit("nanometre", "nm", 1e-9), new Unit("inch", "in", Inch), new Unit("foot", "ft", 12 * Inch),
new Unit("yard", "yd", 36 * Inch), new Unit("mile", "mi", 63360 * Inch), new Unit("nautical mile", "nmi", 1852),
new Unit("astronomical unit", "au", 149597870700), new Unit("light-year", "ly", 9460730472580800)),
new Quantity("Area",
new Unit("square metre", "m²", 1), new Unit("square kilometre", "km²", 1e6), new Unit("square centimetre", "cm²", 1e-4),
new Unit("hectare", "ha", 1e4), new Unit("are", "a", 100), new Unit("square inch", "in²", Inch * Inch),
new Unit("square foot", "ft²", 144 * Inch * Inch), new Unit("acre", "ac", 4046.8564224), new Unit("square mile", "mi²", 63360 * Inch * 63360 * Inch)),
new Quantity("Volume",
new Unit("cubic metre", "m³", 1), new Unit("litre", "L", 1e-3), new Unit("millilitre", "mL", 1e-6), new Unit("cubic centimetre", "cm³", 1e-6),
new Unit("US gallon", "gal", Gallon), new Unit("US quart", "qt", Gallon / 4), new Unit("US pint", "pt", Gallon / 8),
new Unit("US cup", "cup", Gallon / 16), new Unit("US fluid ounce", "fl oz", Gallon / 128), new Unit("imperial gallon", "imp gal", 4.54609e-3),
new Unit("cubic foot", "ft³", 1728 * Inch * Inch * Inch), new Unit("cubic inch", "in³", Inch * Inch * Inch)),
new Quantity("Mass",
new Unit("kilogram", "kg", 1), new Unit("gram", "g", 1e-3), new Unit("milligram", "mg", 1e-6), new Unit("tonne", "t", 1e3),
new Unit("pound", "lb", Pound), new Unit("ounce", "oz", Pound / 16), new Unit("stone", "st", 14 * Pound),
new Unit("short ton", "ton", 2000 * Pound), new Unit("carat", "ct", 2e-4)),
new Quantity("Time",
new Unit("second", "s", 1), new Unit("millisecond", "ms", 1e-3), new Unit("microsecond", "μs", 1e-6), new Unit("minute", "min", 60),
new Unit("hour", "h", 3600), new Unit("day", "d", 86400), new Unit("week", "wk", 604800), new Unit("year (365.25 days)", "yr", 31557600)),
new Quantity("Speed",
new Unit("metre per second", "m/s", 1), new Unit("kilometre per hour", "km/h", 1 / 3.6), new Unit("mile per hour", "mph", 63360 * Inch / 3600),
new Unit("foot per second", "ft/s", 12 * Inch), new Unit("knot", "kn", 1852 / 3600.0), new Unit("speed of light", "c", 299792458)),
new Quantity("Temperature",
new Unit("kelvin", "K", 1), new Unit("degree Celsius", "°C", 1, 273.15), new Unit("degree Fahrenheit", "°F", 5 / 9.0, 459.67 * 5 / 9.0),
new Unit("degree Rankine", "°R", 5 / 9.0)),
new Quantity("Energy",
new Unit("joule", "J", 1), new Unit("kilojoule", "kJ", 1e3), new Unit("calorie", "cal", 4.184), new Unit("kilocalorie", "kcal", 4184),
new Unit("watt-hour", "Wh", 3600), new Unit("kilowatt-hour", "kWh", 3.6e6), new Unit("electronvolt", "eV", 1.602176634e-19),
new Unit("British thermal unit", "BTU", 1055.05585262)),
new Quantity("Pressure",
new Unit("pascal", "Pa", 1), new Unit("kilopascal", "kPa", 1e3), new Unit("bar", "bar", 1e5), new Unit("atmosphere", "atm", 101325),
new Unit("millimetre of mercury", "mmHg", 133.322387415), new Unit("pound per square inch", "psi", Pound * 9.80665 / (Inch * Inch))),
new Quantity("Angle",
new Unit("radian", "rad", 1), new Unit("degree", "°", Math.PI / 180), new Unit("gradian", "grad", Math.PI / 200),
new Unit("arcminute", "′", Math.PI / 10800), new Unit("arcsecond", "″", Math.PI / 648000), new Unit("turn", "tr", 2 * Math.PI)),
new Quantity("Data",
new Unit("byte", "B", 1), new Unit("bit", "bit", 0.125), new Unit("kilobyte", "kB", 1e3), new Unit("megabyte", "MB", 1e6),
new Unit("gigabyte", "GB", 1e9), new Unit("terabyte", "TB", 1e12), new Unit("kibibyte", "KiB", 1024), new Unit("mebibyte", "MiB", 1 << 20),
new Unit("gibibyte", "GiB", 1 << 30), new Unit("tebibyte", "TiB", 1099511627776)),
};

public static Quantity Get(string name) => All.Single(q => q.Name == name);

/// <summary>A value in a unit, in another unit of the same quantity.</summary>
public static double Convert(double value, Unit from, Unit to) => (value * from.Factor + from.Offset - to.Offset) / to.Factor;

/// <summary>A number typed by the user: a point or a comma before the decimals, and an exponent (<c>1.5e3</c>).</summary>
public static bool TryParse(string? text, out double value)
{
value = 0;
if (string.IsNullOrWhiteSpace(text))
return false;
var normalized = text!.Trim().Replace(" ", string.Empty).Replace('−', '-').Replace(',', '.');
return double.TryParse(normalized, NumberStyles.Float, CultureInfo.InvariantCulture, out value) && !double.IsInfinity(value);
}

/// <summary>
/// A value in UnicodeMath, with six significant digits: in scientific notation (<c>1.5×10^6</c>) when it is very
/// large or very small. What follows it is separated by a space, which ends the exponent.
/// </summary>
public static string Format(double value)
{
if (double.IsNaN(value) || double.IsInfinity(value))
return "\"—\"";
var magnitude = Math.Abs(value);
if (magnitude != 0 && (magnitude >= 1e6 || magnitude < 1e-4))
{
var exponent = (int)Math.Floor(Math.Log10(magnitude));
var mantissa = Math.Round(value / Math.Pow(10, exponent), 5);
if (Math.Abs(mantissa) >= 10)
{
mantissa /= 10;
exponent++;
}
return Plain(mantissa) + "×10^" + (exponent < 0 ? "(-" + -exponent + ")" : exponent.ToString(CultureInfo.InvariantCulture));
}
return Plain(value);
}

private static string Plain(double value)
{
var text = value.ToString("G6", CultureInfo.InvariantCulture);
return text == "-0" ? "0" : text;
}
}
}
141 changes: 141 additions & 0 deletions samples/SharpEquations.Avalonia.MathTools.Demo/UnitsPage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
namespace SharpEquations.Avalonia.MathTools
{
using System.Collections.Generic;
using System.Linq;
using global::Avalonia;
using global::Avalonia.Controls;
using global::Avalonia.Layout;
using global::Avalonia.Media;
using SharpEquations;

/// <summary>
/// The unit converter: a value in a unit of a quantity, in another unit and in all of them, the results written in
/// UnicodeMath and drawn by a <see cref="MathView"/>.
/// </summary>
public sealed class UnitsPage : UserControl
{
private readonly MathView result = new MathView { FontSize = 30, HorizontalAlignment = HorizontalAlignment.Left };
private readonly StackPanel table = new StackPanel { Spacing = 2 };
private bool updating;

public UnitsPage()
{
Quantity = new ComboBox { ItemsSource = Units.All, SelectedIndex = 0, MinWidth = 160 };
Value = new TextBox { Text = "1", Width = 150, PlaceholderText = "A number" };
From = new ComboBox { MinWidth = 220 };
To = new ComboBox { MinWidth = 220 };
var swap = new Button { Content = "⇄", FontSize = 17, Padding = new Thickness(12, 2), CornerRadius = new CornerRadius(8) };
ToolTip.SetTip(swap, "Swap the units");

Quantity.SelectionChanged += (_, _) => ShowQuantity();
Value.PropertyChanged += (_, e) =>
{
if (e.Property == TextBox.TextProperty)
Refresh();
};
From.SelectionChanged += (_, _) => Refresh();
To.SelectionChanged += (_, _) => Refresh();
swap.Click += (_, _) => Swap();

var resultFrame = new Border { CornerRadius = new CornerRadius(12), Padding = new Thickness(18, 14), MinHeight = 76, Child = result };
resultFrame.Bind(Border.BackgroundProperty, resultFrame.GetResourceObservable("CodeBrush"));

Content = new StackPanel
{
Spacing = 20,
Children =
{
Ui.Card("Units", "A value in a unit, in another: length, area, volume, mass, time, speed, temperature, energy, pressure, angle and data", new StackPanel
{
Spacing = 16,
Children =
{
Ui.Row(Field("Quantity", Quantity), Field("Value", Value), Field("From", From), swap, Field("To", To)),
resultFrame,
},
}),
Ui.Card("In every unit", "The value in all the units of the quantity", table),
},
};
ShowQuantity();
}

public ComboBox Quantity { get; }

public TextBox Value { get; }

public ComboBox From { get; }

public ComboBox To { get; }

/// <summary>The result, as UnicodeMath (<c>1 " km"=0.621371 " mi"</c>, with no-break spaces in the text), or null when the value is not a number.</summary>
public string? Result { get; private set; }

/// <summary>Swaps the units, as the ⇄ button does.</summary>
public void Swap()
{
var from = From.SelectedIndex;
updating = true;
From.SelectedIndex = To.SelectedIndex;
updating = false;
To.SelectedIndex = from;
}

private Quantity Selected => (Quantity)Quantity.SelectedItem!;

private void ShowQuantity()
{
updating = true;
From.ItemsSource = Selected.Units;
To.ItemsSource = Selected.Units;
From.SelectedIndex = 0;
To.SelectedIndex = 1;
updating = false;
Refresh();
}

private void Refresh()
{
if (updating || From.SelectedItem is not Unit from || To.SelectedItem is not Unit to)
return;

table.Children.Clear();
if (!Units.TryParse(Value.Text, out var value))
{
Result = null;
result.Equation = null;
table.Children.Add(Ui.Muted("Type a number, such as 12.5 or 1.5e3."));
return;
}

Result = Units.Format(value) + " " + Text(from.Symbol) + "=" + Units.Format(Units.Convert(value, from, to)) + " " + Text(to.Symbol);
result.Equation = Equation.Parse(Result, EquationFormat.UnicodeMath, new EquationOptions { Display = true });
foreach (var unit in Selected.Units)
table.Children.Add(Line(unit, Units.Convert(value, from, unit), unit == from || unit == to));
}

private static Grid Line(Unit unit, double value, bool highlighted)
{
var name = new TextBlock { Text = unit.Name, FontSize = 13.5, VerticalAlignment = VerticalAlignment.Center };
if (highlighted)
name.FontWeight = FontWeight.SemiBold;
var math = Ui.Math(Units.Format(value) + " " + Text(unit.Symbol), 17);
math.HorizontalAlignment = HorizontalAlignment.Left;
math.VerticalAlignment = VerticalAlignment.Center;
Grid.SetColumn(math, 1);
return new Grid { ColumnDefinitions = new ColumnDefinitions("220,*"), MinHeight = 32, Children = { name, math } };
}

// a symbol as text of UnicodeMath, upright and kept as it is (a quote in it is not in any symbol), after a
// no-break space that separates it from the number (the space of UnicodeMath before it ends an exponent, and is
// not drawn)
private static string Text(string symbol) => "\"\u00A0" + symbol + "\"";

private static StackPanel Field(string label, Control control)
{
var text = Ui.Muted(label, 12.5);
text.VerticalAlignment = VerticalAlignment.Center;
return new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8, Children = { text, control } };
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,40 @@ public async Task FormulaOpensInTheCalculator()
await Assert.That(equation).IsEqualTo(Equation.Parse("c^2=a^2+b^2-2ab cos⁡γ", EquationFormat.UnicodeMath, new EquationOptions { Display = true }));
await Assert.That(tab).IsEqualTo(0);
}

[Test]
public async Task UnitsConvert()
{
var (miles, swapped, celsius, invalid) = await Headless.Run(() =>
{
var window = new MainWindow();
window.Show();
var view = window.GetVisualDescendants().OfType<MathToolsView>().Single();
view.Tabs.SelectedIndex = 2;
var page = view.Converter;
page.Value.Text = "42.195";
page.From.SelectedItem = Units.Get("Length")["km"];
page.To.SelectedItem = Units.Get("Length")["mi"];
window.UpdateLayout();
var first = page.Result;
Save(window, "math-tools-units");
page.Swap();
var second = page.Result;
page.Quantity.SelectedItem = Units.Get("Temperature");
page.Value.Text = "98.6";
page.From.SelectedItem = Units.Get("Temperature")["°F"];
page.To.SelectedItem = Units.Get("Temperature")["°C"];
var third = page.Result;
page.Value.Text = "abc";
var fourth = page.Result;
window.Close();
return (first, second, third, fourth);
});
await Assert.That(miles).IsEqualTo("42.195 \"\u00A0km\"=26.2188 \"\u00A0mi\"");
await Assert.That(swapped).IsEqualTo("42.195 \"\u00A0mi\"=67.9063 \"\u00A0km\"");
await Assert.That(celsius).IsEqualTo("98.6 \"\u00A0°F\"=37 \"\u00A0°C\"");
await Assert.That(invalid).IsNull();
}
}
}
#endif
Loading
Loading