Skip to content
Merged
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 CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ dotnet run -c Release --project PreciseNumber.Benchmarks -- --filter '*' --job s

- **Generic conversions** (`PreciseNumber/PreciseNumber.Conversions.cs`): The `TryConvertFrom*` and `TryConvertTo*` members behind `CreateChecked`, `CreateSaturating`, and `CreateTruncating`, for every built-in numeric type and `BigInteger`. `To<T>()` uses them too

- **PreciseNumberExtensions** (`PreciseNumber/PreciseNumberExtensions.cs`): Extension methods providing `ToPreciseNumber<T>()` for converting any `INumber<T>` to PreciseNumber
- **PreciseNumberExtensions** (`PreciseNumber/PreciseNumberExtensions.cs`): Extension methods providing `ToPreciseNumber<T>()` for converting any `INumber<T>` to PreciseNumber. Integer and floating-point types convert directly; any other type falls back to `CreateChecked`, so a type that converts itself through `TryConvertToChecked<PreciseNumber>` (such as `ktsu.SignificantNumber`) works, and only a type that can't throws `NotSupportedException`

### Key Design Patterns

Expand Down
29 changes: 29 additions & 0 deletions PreciseNumber.Test/PreciseNumberConversionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -438,4 +438,33 @@ public void SmallestExponentConvertsToZero()
AssertToInAllModes(value, BigInteger.Zero);
}
}

[TestMethod]
public void TestToPreciseNumberAcceptsATypeThatConvertsItself()
{
// SelfConvertingNumber is neither an integer nor a floating-point type, as SignificantNumber
// isn't, but it converts itself through TryConvertToChecked<PreciseNumber>.
SelfConvertingNumber one = new(1.5m, CanConvert: true);
SelfConvertingNumber lo = new(2m, CanConvert: true);
SelfConvertingNumber hi = new(3m, CanConvert: true);
PreciseNumber p = P("1.5");

Assert.AreEqual(Checked<PreciseNumber, SelfConvertingNumber>(one), one.ToPreciseNumber());
Assert.AreEqual(P("1.5"), one.ToPreciseNumber());
Assert.AreEqual(0, p.CompareTo(one));
Assert.AreEqual(-1, p.CompareTo(lo));
Assert.AreEqual(P("2"), p.Clamp(lo, hi));
Assert.AreEqual(P("1.5"), p.Clamp(one, hi));
}

[TestMethod]
public void TestToPreciseNumberStillRejectsATypeThatCannotConvert()
{
SelfConvertingNumber stuck = new(1.5m, CanConvert: false);
PreciseNumber p = P("1.5");

Assert.ThrowsExactly<NotSupportedException>(() => stuck.ToPreciseNumber());
Assert.ThrowsExactly<NotSupportedException>(() => p.CompareTo(stuck));
Assert.ThrowsExactly<NotSupportedException>(() => p.Clamp(stuck, stuck));
}
}
122 changes: 122 additions & 0 deletions PreciseNumber.Test/SelfConvertingNumber.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.PreciseNumber.Test;

using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Numerics;

/// <summary>
/// A number that is neither an integer nor a floating-point type, as <c>ktsu.SignificantNumber</c> is,
/// and that converts itself to <see cref="PreciseNumber"/> through <c>TryConvertToChecked</c> when
/// <see cref="CanConvert"/> is set, and to nothing otherwise.
/// </summary>
/// <param name="Value">The value it holds.</param>
/// <param name="CanConvert">Whether it can convert itself to <see cref="PreciseNumber"/>.</param>
internal readonly record struct SelfConvertingNumber(decimal Value, bool CanConvert) : INumber<SelfConvertingNumber>
{
public static SelfConvertingNumber One => new(1m, true);
public static int Radix => 10;
public static SelfConvertingNumber Zero => new(0m, true);
public static SelfConvertingNumber AdditiveIdentity => Zero;
public static SelfConvertingNumber MultiplicativeIdentity => One;

private SelfConvertingNumber With(decimal value) => new(value, CanConvert);

public static bool TryConvertToChecked<TOther>(SelfConvertingNumber value, [MaybeNullWhen(false)] out TOther result)
where TOther : INumberBase<TOther>
{
if (value.CanConvert && typeof(TOther) == typeof(PreciseNumber))
{
result = (TOther)(object)value.Value.ToPreciseNumber();
return true;
}

result = default;
return false;
}

public static bool TryConvertToSaturating<TOther>(SelfConvertingNumber value, [MaybeNullWhen(false)] out TOther result)
where TOther : INumberBase<TOther> => TryConvertToChecked(value, out result);

public static bool TryConvertToTruncating<TOther>(SelfConvertingNumber value, [MaybeNullWhen(false)] out TOther result)
where TOther : INumberBase<TOther> => TryConvertToChecked(value, out result);

public static bool TryConvertFromChecked<TOther>(TOther value, out SelfConvertingNumber result)
where TOther : INumberBase<TOther>
{
result = default;
return false;
}

public static bool TryConvertFromSaturating<TOther>(TOther value, out SelfConvertingNumber result)
where TOther : INumberBase<TOther> => TryConvertFromChecked(value, out result);

public static bool TryConvertFromTruncating<TOther>(TOther value, out SelfConvertingNumber result)
where TOther : INumberBase<TOther> => TryConvertFromChecked(value, out result);

public static SelfConvertingNumber Abs(SelfConvertingNumber value) => value.With(Math.Abs(value.Value));
public static bool IsCanonical(SelfConvertingNumber value) => true;
public static bool IsComplexNumber(SelfConvertingNumber value) => false;
public static bool IsEvenInteger(SelfConvertingNumber value) => decimal.IsEvenInteger(value.Value);
public static bool IsFinite(SelfConvertingNumber value) => true;
public static bool IsImaginaryNumber(SelfConvertingNumber value) => false;
public static bool IsInfinity(SelfConvertingNumber value) => false;
public static bool IsInteger(SelfConvertingNumber value) => decimal.IsInteger(value.Value);
public static bool IsNaN(SelfConvertingNumber value) => false;
public static bool IsNegative(SelfConvertingNumber value) => decimal.IsNegative(value.Value);
public static bool IsNegativeInfinity(SelfConvertingNumber value) => false;
public static bool IsNormal(SelfConvertingNumber value) => value.Value != 0m;
public static bool IsOddInteger(SelfConvertingNumber value) => decimal.IsOddInteger(value.Value);
public static bool IsPositive(SelfConvertingNumber value) => decimal.IsPositive(value.Value);
public static bool IsPositiveInfinity(SelfConvertingNumber value) => false;
public static bool IsRealNumber(SelfConvertingNumber value) => true;
public static bool IsSubnormal(SelfConvertingNumber value) => false;
public static bool IsZero(SelfConvertingNumber value) => value.Value == 0m;
public static SelfConvertingNumber MaxMagnitude(SelfConvertingNumber x, SelfConvertingNumber y) => x.With(decimal.MaxMagnitude(x.Value, y.Value));
public static SelfConvertingNumber MaxMagnitudeNumber(SelfConvertingNumber x, SelfConvertingNumber y) => MaxMagnitude(x, y);
public static SelfConvertingNumber MinMagnitude(SelfConvertingNumber x, SelfConvertingNumber y) => x.With(decimal.MinMagnitude(x.Value, y.Value));
public static SelfConvertingNumber MinMagnitudeNumber(SelfConvertingNumber x, SelfConvertingNumber y) => MinMagnitude(x, y);

public static SelfConvertingNumber Parse(ReadOnlySpan<char> s, NumberStyles style, IFormatProvider? provider) => new(decimal.Parse(s, style, provider), true);
public static SelfConvertingNumber Parse(string s, NumberStyles style, IFormatProvider? provider) => new(decimal.Parse(s, style, provider), true);
public static SelfConvertingNumber Parse(ReadOnlySpan<char> s, IFormatProvider? provider) => new(decimal.Parse(s, provider), true);
public static SelfConvertingNumber Parse(string s, IFormatProvider? provider) => new(decimal.Parse(s, provider), true);

public static bool TryParse(ReadOnlySpan<char> s, NumberStyles style, IFormatProvider? provider, out SelfConvertingNumber result)
{
bool parsed = decimal.TryParse(s, style, provider, out decimal value);
result = new(value, true);
return parsed;
}

public static bool TryParse([NotNullWhen(true)] string? s, NumberStyles style, IFormatProvider? provider, out SelfConvertingNumber result) =>
TryParse(s.AsSpan(), style, provider, out result);

public static bool TryParse(ReadOnlySpan<char> s, IFormatProvider? provider, out SelfConvertingNumber result) =>
TryParse(s, NumberStyles.Number, provider, out result);

public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, out SelfConvertingNumber result) =>
TryParse(s.AsSpan(), NumberStyles.Number, provider, out result);

public int CompareTo(object? obj) => obj is SelfConvertingNumber other ? CompareTo(other) : 1;
public int CompareTo(SelfConvertingNumber other) => Value.CompareTo(other.Value);
public string ToString(string? format, IFormatProvider? formatProvider) => Value.ToString(format, formatProvider);

public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider) =>
Value.TryFormat(destination, out charsWritten, format, provider);

public static SelfConvertingNumber operator +(SelfConvertingNumber value) => value;
public static SelfConvertingNumber operator -(SelfConvertingNumber value) => value.With(-value.Value);
public static SelfConvertingNumber operator +(SelfConvertingNumber left, SelfConvertingNumber right) => left.With(left.Value + right.Value);
public static SelfConvertingNumber operator -(SelfConvertingNumber left, SelfConvertingNumber right) => left.With(left.Value - right.Value);
public static SelfConvertingNumber operator *(SelfConvertingNumber left, SelfConvertingNumber right) => left.With(left.Value * right.Value);
public static SelfConvertingNumber operator /(SelfConvertingNumber left, SelfConvertingNumber right) => left.With(left.Value / right.Value);
public static SelfConvertingNumber operator %(SelfConvertingNumber left, SelfConvertingNumber right) => left.With(left.Value % right.Value);
public static SelfConvertingNumber operator ++(SelfConvertingNumber value) => value.With(value.Value + 1m);
public static SelfConvertingNumber operator --(SelfConvertingNumber value) => value.With(value.Value - 1m);
public static bool operator <(SelfConvertingNumber left, SelfConvertingNumber right) => left.Value < right.Value;
public static bool operator >(SelfConvertingNumber left, SelfConvertingNumber right) => left.Value > right.Value;
public static bool operator <=(SelfConvertingNumber left, SelfConvertingNumber right) => left.Value <= right.Value;
public static bool operator >=(SelfConvertingNumber left, SelfConvertingNumber right) => left.Value >= right.Value;
}
34 changes: 32 additions & 2 deletions PreciseNumber/PreciseNumberExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,38 @@ internal static bool TryCreate<TInput>([NotNullWhen(true)] TInput input, out Pre
return true;

default:
preciseNumber = default;
return false;
return TryCreateThroughGenericMath(input, out preciseNumber);
}
}

/// <summary>
/// Converts a type that is neither an integer nor a floating-point type, but may know how to become a
/// <see cref="PreciseNumber"/> itself through <c>TryConvertToChecked</c>, as <c>ktsu.SignificantNumber</c>
/// does.
/// </summary>
/// <remarks>
/// <c>CreateChecked</c> asks <see cref="PreciseNumber"/> first and then <typeparamref name="TInput"/>,
/// and throws <see cref="NotSupportedException"/> when neither can convert, which is read here as
/// "unsupported". Any other exception, such as an <see cref="OverflowException"/>, is the conversion's
/// own answer and is left to propagate, as it would from <c>CreateChecked</c>.
/// </remarks>
private static bool TryCreateThroughGenericMath<TInput>(TInput input, out PreciseNumber preciseNumber)
where TInput : INumber<TInput>
{
try
{
preciseNumber = CreateChecked<PreciseNumber, TInput>(input);
return true;
}
catch (NotSupportedException)
{
preciseNumber = default;
return false;
}
}

private static TSelf CreateChecked<TSelf, TOther>(TOther value)
where TSelf : INumberBase<TSelf>
where TOther : INumberBase<TOther>
=> TSelf.CreateChecked(value);
}
Loading