From 5d1189ccc240b5267dd5a699c4043a84b0ee7bae Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 00:54:30 +0000 Subject: [PATCH] [no-new-version] Math tools: a unit converter The third page of the math tools: a value in a unit of length, area, volume, mass, time, speed, temperature (with an offset), energy, pressure, angle or data, in another unit and in all of them, written as UnicodeMath (six significant digits, scientific notation for very large and very small values) and drawn by a MathView. Tests: known conversions and their inverses, the formatting, the numbers typed, the symbols, and the page in the window. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SyVio8afJsGwHW9i5v5HDs --- AGENTS.md | 2 +- README.md | 2 +- .../MathToolsView.cs | 9 +- .../Units.cs | 144 ++++++++++++++++++ .../UnitsPage.cs | 141 +++++++++++++++++ .../MathTools/MathToolsWindowTests.cs | 34 +++++ .../MathTools/UnitsTests.cs | 95 ++++++++++++ 7 files changed, 423 insertions(+), 4 deletions(-) create mode 100644 samples/SharpEquations.Avalonia.MathTools.Demo/Units.cs create mode 100644 samples/SharpEquations.Avalonia.MathTools.Demo/UnitsPage.cs create mode 100644 tests/SharpEquations.Avalonia.Tests/MathTools/UnitsTests.cs diff --git a/AGENTS.md b/AGENTS.md index 60c3fa4..baa5468 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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) | diff --git a/README.md b/README.md index 780ce40..08a6a42 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/samples/SharpEquations.Avalonia.MathTools.Demo/MathToolsView.cs b/samples/SharpEquations.Avalonia.MathTools.Demo/MathToolsView.cs index ae66ffb..72aff30 100644 --- a/samples/SharpEquations.Avalonia.MathTools.Demo/MathToolsView.cs +++ b/samples/SharpEquations.Avalonia.MathTools.Demo/MathToolsView.cs @@ -9,8 +9,8 @@ namespace SharpEquations.Avalonia.MathTools using SharpEquations; /// - /// 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. /// public sealed class MathToolsView : UserControl { @@ -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 => { @@ -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 } }; @@ -44,6 +46,9 @@ public MathToolsView() /// The page of the library of formulas. public FormulasPage FormulaLibrary { get; } + /// The page of the unit converter. + public UnitsPage Converter { get; } + /// The tabs of the pages. public TabControl Tabs { get; } diff --git a/samples/SharpEquations.Avalonia.MathTools.Demo/Units.cs b/samples/SharpEquations.Avalonia.MathTools.Demo/Units.cs new file mode 100644 index 0000000..4c609a3 --- /dev/null +++ b/samples/SharpEquations.Avalonia.MathTools.Demo/Units.cs @@ -0,0 +1,144 @@ +namespace SharpEquations.Avalonia.MathTools +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + + /// A unit of a quantity: a value in it is value × Factor + Offset in the base unit of the quantity. + 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})"; + } + + /// A quantity and its units, the first of which is the base unit. + public sealed class Quantity + { + public Quantity(string name, params Unit[] units) + { + Name = name; + Units = units; + } + + public string Name { get; } + + public IReadOnlyList Units { get; } + + public Unit this[string symbol] => Units.Single(u => u.Symbol == symbol); + + public override string ToString() => Name; + } + + /// The quantities of the converter, and the conversion and the formatting of the values. + 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 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); + + /// A value in a unit, in another unit of the same quantity. + public static double Convert(double value, Unit from, Unit to) => (value * from.Factor + from.Offset - to.Offset) / to.Factor; + + /// A number typed by the user: a point or a comma before the decimals, and an exponent (1.5e3). + 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); + } + + /// + /// A value in UnicodeMath, with six significant digits: in scientific notation (1.5×10^6) when it is very + /// large or very small. What follows it is separated by a space, which ends the exponent. + /// + 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; + } + } +} diff --git a/samples/SharpEquations.Avalonia.MathTools.Demo/UnitsPage.cs b/samples/SharpEquations.Avalonia.MathTools.Demo/UnitsPage.cs new file mode 100644 index 0000000..9b683c9 --- /dev/null +++ b/samples/SharpEquations.Avalonia.MathTools.Demo/UnitsPage.cs @@ -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; + + /// + /// 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 . + /// + 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; } + + /// The result, as UnicodeMath (1 " km"=0.621371 " mi", with no-break spaces in the text), or null when the value is not a number. + public string? Result { get; private set; } + + /// Swaps the units, as the ⇄ button does. + 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 } }; + } + } +} diff --git a/tests/SharpEquations.Avalonia.Tests/MathTools/MathToolsWindowTests.cs b/tests/SharpEquations.Avalonia.Tests/MathTools/MathToolsWindowTests.cs index c4b2dbc..095fe49 100644 --- a/tests/SharpEquations.Avalonia.Tests/MathTools/MathToolsWindowTests.cs +++ b/tests/SharpEquations.Avalonia.Tests/MathTools/MathToolsWindowTests.cs @@ -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().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 diff --git a/tests/SharpEquations.Avalonia.Tests/MathTools/UnitsTests.cs b/tests/SharpEquations.Avalonia.Tests/MathTools/UnitsTests.cs new file mode 100644 index 0000000..222fb02 --- /dev/null +++ b/tests/SharpEquations.Avalonia.Tests/MathTools/UnitsTests.cs @@ -0,0 +1,95 @@ +#if NET10_0_OR_GREATER +namespace SharpEquations.Avalonia.Tests.MathTools +{ + using System; + using System.Linq; + using System.Threading.Tasks; + using SharpEquations; + using SharpEquations.Avalonia.MathTools; + + /// The conversions of the unit converter, and the values written as UnicodeMath. + public class UnitsTests + { + [Test] + [Arguments("Length", 1, "mi", "km", 1.609344)] + [Arguments("Length", 1, "ft", "in", 12)] + [Arguments("Area", 1, "ha", "m²", 10000)] + [Arguments("Area", 1, "mi²", "ac", 640)] + [Arguments("Volume", 1, "gal", "L", 3.785411784)] + [Arguments("Mass", 1, "lb", "oz", 16)] + [Arguments("Mass", 1, "kg", "lb", 2.20462262185)] + [Arguments("Time", 2, "h", "min", 120)] + [Arguments("Speed", 100, "km/h", "m/s", 27.7777777778)] + [Arguments("Temperature", 100, "°C", "°F", 212)] + [Arguments("Temperature", -40, "°F", "°C", -40)] + [Arguments("Temperature", 0, "K", "°C", -273.15)] + [Arguments("Temperature", 491.67, "°R", "°F", 32)] + [Arguments("Energy", 1, "kWh", "kcal", 860.420650096)] + [Arguments("Pressure", 1, "atm", "psi", 14.6959487755)] + [Arguments("Angle", 180, "°", "rad", Math.PI)] + [Arguments("Data", 1, "GiB", "MB", 1073.741824)] + public async Task Converts(string quantity, double value, string from, string to, double expected) + { + var q = Units.Get(quantity); + await Assert.That(Units.Convert(value, q[from], q[to])).IsEqualTo(expected).Within(Math.Abs(expected) * 1e-9 + 1e-9); + await Assert.That(Units.Convert(Units.Convert(value, q[from], q[to]), q[to], q[from])).IsEqualTo(value).Within(Math.Abs(value) * 1e-9 + 1e-9); + } + + [Test] + [Arguments(1.0, "1")] + [Arguments(0.621371192237, "0.621371")] + [Arguments(-273.15, "-273.15")] + [Arguments(123456.7, "123457")] + [Arguments(9460730472580800.0, "9.46073×10^15")] + [Arguments(1.602176634e-19, "1.60218×10^(-19)")] + [Arguments(9.999996e9, "1×10^10")] + [Arguments(0.0, "0")] + public async Task FormatsAsUnicodeMath(double value, string expected) + { + await Assert.That(Units.Format(value)).IsEqualTo(expected); + await Assert.That(Equation.TryParse(expected, EquationFormat.UnicodeMath, out _)).IsTrue(); + } + + /// The unit after a value in scientific notation is not in its exponent. + [Test] + public async Task UnitIsNotInTheExponent() + { + var math = Equation.Parse(Units.Format(4.2195e6) + " \" cm\"", EquationFormat.UnicodeMath).ToMathML(); + var power = math.Descendants().Single(e => e.Name.LocalName == "msup"); + await Assert.That(power.Descendants().Any(e => e.Name.LocalName == "mtext")).IsFalse(); + } + + [Test] + [Arguments("12.5", 12.5)] + [Arguments(" 12,5 ", 12.5)] + [Arguments("−3", -3.0)] + [Arguments("1.5e3", 1500.0)] + public async Task ParsesWhatIsTyped(string text, double expected) + { + await Assert.That(Units.TryParse(text, out var value)).IsTrue(); + await Assert.That(value).IsEqualTo(expected); + } + + [Test] + public async Task RejectsWhatIsNotANumber() + { + foreach (var text in new[] { "", " ", "abc", "1e999" }) + await Assert.That(Units.TryParse(text, out _)).IsFalse().Because(text); + } + + [Test] + public async Task EverySymbolIsUniqueAndParses() + { + foreach (var quantity in Units.All) + { + await Assert.That(quantity.Units.Select(u => u.Symbol).Distinct().Count()).IsEqualTo(quantity.Units.Count).Because(quantity.Name); + foreach (var unit in quantity.Units) + { + var equation = Equation.Parse("1 \"" + unit.Symbol + "\"", EquationFormat.UnicodeMath); + await Assert.That(equation.ToMathML().Descendants().Any(e => e.Name.LocalName == "merror")).IsFalse().Because(unit.Symbol); + } + } + } + } +} +#endif