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