From 5c5426b9b93a71b455f812a2d7df8d701fee1d1a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 01:50:11 +0000 Subject: [PATCH] feat: declare IParsable, ISpanParsable, and IMinMaxValue interfaces FixedPointNano already had Parse/TryParse methods and MaxValue/MinValue properties. This commit formally declares the three interfaces on the struct so consumers can use generic constraints against them. Also adds 11 tests covering Parse, TryParse, and the new interface contracts via generic helper methods. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/FixedPointNano/FixedPointNano.cs | 5 +- .../FixedPointNanoTests.cs | 126 ++++++++++++++++++ 2 files changed, 130 insertions(+), 1 deletion(-) diff --git a/src/FixedPointNano/FixedPointNano.cs b/src/FixedPointNano/FixedPointNano.cs index 42a5c4f..db16af6 100644 --- a/src/FixedPointNano/FixedPointNano.cs +++ b/src/FixedPointNano/FixedPointNano.cs @@ -33,7 +33,10 @@ namespace Seerstone; IUnaryPlusOperators, IIncrementOperators, IDecrementOperators, - IComparisonOperators + IComparisonOperators, + IParsable, + ISpanParsable, + IMinMaxValue { /// The number of decimal places supported by . public const int DecimalPlaces = 9; diff --git a/tests/FixedPointNano.Tests/FixedPointNanoTests.cs b/tests/FixedPointNano.Tests/FixedPointNanoTests.cs index 34d155c..2795000 100644 --- a/tests/FixedPointNano.Tests/FixedPointNanoTests.cs +++ b/tests/FixedPointNano.Tests/FixedPointNanoTests.cs @@ -479,4 +479,130 @@ public void GenericMathInterfacesShouldProvideIdentities() static TResult GetAdditiveIdentity() where T : IAdditiveIdentity => T.AdditiveIdentity; static TResult GetMultiplicativeIdentity() where T : IMultiplicativeIdentity => T.MultiplicativeIdentity; } + + [Test] + public void ParseShouldRoundtripFormattedValues() + { + Assert.Multiple(() => + { + Assert.That(FixedPointNano.Parse("1.5", null), Is.EqualTo(FixedPointNano.FromDecimal(1.5m))); + Assert.That(FixedPointNano.Parse("0", null), Is.EqualTo(FixedPointNano.Zero)); + Assert.That(FixedPointNano.Parse("-3.141592653", null), Is.EqualTo(FixedPointNano.FromDecimal(-3.141592653m))); + Assert.That(FixedPointNano.Parse("1000000000.123456789", null), Is.EqualTo(FixedPointNano.FromDecimal(1000000000.123456789m))); + }); + } + + [Test] + public void ParseSpanShouldRoundtripFormattedValues() + { + Assert.Multiple(() => + { + Assert.That(FixedPointNano.Parse("1.5".AsSpan(), null), Is.EqualTo(FixedPointNano.FromDecimal(1.5m))); + Assert.That(FixedPointNano.Parse("0".AsSpan(), null), Is.EqualTo(FixedPointNano.Zero)); + Assert.That(FixedPointNano.Parse("-42.000000001".AsSpan(), null), Is.EqualTo(FixedPointNano.FromDecimal(-42.000000001m))); + }); + } + + [Test] + public void ParseShouldThrowOnInvalidInput() + { + Assert.Throws(() => FixedPointNano.Parse("not-a-number", null)); + Assert.Throws(() => FixedPointNano.Parse("", null)); + Assert.Throws(() => FixedPointNano.Parse("1.2.3", null)); + } + + [Test] + public void TryParseShouldReturnTrueForValidInput() + { + Assert.Multiple(() => + { + Assert.That(FixedPointNano.TryParse("2.718281828", null, out var result1), Is.True); + Assert.That(result1, Is.EqualTo(FixedPointNano.FromDecimal(2.718281828m))); + + Assert.That(FixedPointNano.TryParse("-1", null, out var result2), Is.True); + Assert.That(result2, Is.EqualTo(FixedPointNano.NegativeOne)); + + Assert.That(FixedPointNano.TryParse("0.000000001", null, out var result3), Is.True); + Assert.That(result3, Is.EqualTo(FixedPointNano.Epsilon)); + }); + } + + [Test] + public void TryParseShouldReturnFalseForInvalidInput() + { + Assert.Multiple(() => + { + Assert.That(FixedPointNano.TryParse((string?)null, null, out _), Is.False); + Assert.That(FixedPointNano.TryParse("not-a-number", null, out _), Is.False); + Assert.That(FixedPointNano.TryParse("", null, out _), Is.False); + }); + } + + [Test] + public void TryParseSpanShouldWorkCorrectly() + { + Assert.Multiple(() => + { + Assert.That(FixedPointNano.TryParse("1.23456789".AsSpan(), null, out var result1), Is.True); + Assert.That(result1, Is.EqualTo(FixedPointNano.FromDecimal(1.23456789m))); + + Assert.That(FixedPointNano.TryParse("abc".AsSpan(), null, out _), Is.False); + Assert.That(FixedPointNano.TryParse(ReadOnlySpan.Empty, null, out _), Is.False); + }); + } + + [Test] + public void TryParseConvenienceOverloadsShouldWork() + { + Assert.Multiple(() => + { + Assert.That(FixedPointNano.TryParse("5.5", out var result1), Is.True); + Assert.That(result1, Is.EqualTo(FixedPointNano.FromDecimal(5.5m))); + + Assert.That(FixedPointNano.TryParse("5.5".AsSpan(), out var result2), Is.True); + Assert.That(result2, Is.EqualTo(FixedPointNano.FromDecimal(5.5m))); + }); + } + + [Test] + public void ParseShouldUseFormatProviderForCultureSpecificInput() + { + var deDE = CultureInfo.GetCultureInfo("de-DE"); + + Assert.That(FixedPointNano.TryParse("1,5", deDE, out var result), Is.True); + Assert.That(result, Is.EqualTo(FixedPointNano.FromDecimal(1.5m))); + } + + [Test] + public void IParsableInterfaceShouldWork() + { + var parsed = ParseViaInterface("3.14", null); + Assert.That(parsed, Is.EqualTo(FixedPointNano.FromDecimal(3.14m))); + + static T ParseViaInterface(string s, IFormatProvider? provider) where T : IParsable + => T.Parse(s, provider); + } + + [Test] + public void ISpanParsableInterfaceShouldWork() + { + var parsed = ParseSpanViaInterface("2.71828".AsSpan(), null); + Assert.That(parsed, Is.EqualTo(FixedPointNano.FromDecimal(2.71828m))); + + static T ParseSpanViaInterface(ReadOnlySpan s, IFormatProvider? provider) where T : ISpanParsable + => T.Parse(s, provider); + } + + [Test] + public void IMinMaxValueInterfaceShouldExposeCorrectBounds() + { + var maxValue = GetMaxValue(); + var minValue = GetMinValue(); + + Assert.That(maxValue, Is.EqualTo(FixedPointNano.MaxValue)); + Assert.That(minValue, Is.EqualTo(FixedPointNano.MinValue)); + + static T GetMaxValue() where T : IMinMaxValue => T.MaxValue; + static T GetMinValue() where T : IMinMaxValue => T.MinValue; + } }