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 @@ -46,7 +46,7 @@ dotnet run -c Release --project PreciseNumber.Benchmarks -- --filter '*' --job s
- Constants (`Zero`, `One`, `Pi`, `E`, `Tau`) are pre-computed static instances
- `ConstantPrecision` is a ceiling, not just a width. `PiTo`/`ETo`/`Ln2To`/`Ln10To` serve at most that many digits and return the capped constant rather than failing, which is deliberate and pinned by `TestConstantAccessorsReturnTheWholeConstantWhenAskedForMore` — so a caller that *needs* what it asked for has to check, because the accessor will not. The circular functions are where this bites: reducing modulo `π/2` spends one digit of π per integer digit of the argument, so only about `ConstantPrecision - integerDigits` are left for the answer. `RequireReducibleArgument` enforces exactly that sum and nothing wider. Do not tighten it to `reductionDigits`: that pads the requirement with `TrigonometricGuardDigits` twice over as margin, margin is allowed to be unavailable, and bounding it would refuse `Sin(1000000, 130)` — a call whose every reported digit is correct. The half-turn family (`SinPi` and the rest) has no such ceiling, because it reduces before it multiplies, and `Log` has none either, because it carries the magnitude in the decimal exponent rather than cancelling it against a constant
- As a value type it can't be null or inherited. Don't add null checks for `PreciseNumber` parameters, and don't reintroduce `protected` members
- Conversions to integer types go through `BigInteger`, so range checks, clamping, and wrapping follow its conventions. Conversions to `double`, `float`, `Half`, and `decimal` render normalized scientific notation (`d.ddd…E±n`) and parse it, because the runtime parsers round correctly, with Clinger's fast path for small values. Keep one digit before the point. The .NET 7 and 8 parsers clamp an exponent above 1000 and still offset it by every digit ahead of the point, so a long significand rendered as an integer parses as zero there. Conversions from `double`, `float`, and `Half` use the shortest text that round-trips (`"R"`). NaN and infinity coming in follow `BigInteger` too
- Conversions to integer types go through `BigInteger`, so range checks, clamping, and wrapping follow its conventions. Conversions to `double`, `float`, `Half`, and `decimal` render normalized scientific notation (`d.ddd…E±n`) and parse it, because the runtime parsers round correctly, with Clinger's fast path for small values. Keep one digit before the point. The .NET 7 and 8 parsers clamp an exponent above 1000 and still offset it by every digit ahead of the point, so a long significand rendered as an integer parses as zero there. Conversions from `double`, `float`, and `Half` use the shortest text that round-trips (`"R"`). NaN and infinity coming in follow `BigInteger` too. `NFloat` converts through `float` or `double`, whichever its `NFloat.Size` says it is, in both directions

### Test Structure

Expand Down
48 changes: 48 additions & 0 deletions PreciseNumber.Test/PreciseNumberConversionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ namespace ktsu.PreciseNumber.Test;

using System.Globalization;
using System.Numerics;
using System.Runtime.InteropServices;

/// <summary>
/// Covers conversion through generic math, which is how code written against <see cref="INumber{TSelf}"/>
Expand Down Expand Up @@ -106,6 +107,53 @@ public void FromBinaryFloatingPointUsesDecimalText()
AssertFromInAllModes(0.0, "0");
}

[TestMethod]
public void FromNFloatMatchesToPreciseNumber()
{
AssertFromInAllModes((NFloat)1.5, "1.5");
AssertFromInAllModes((NFloat)(-0.3048), "-0.3048");
AssertFromInAllModes((NFloat)0.0, "0");

foreach (NFloat value in new[] { (NFloat)0.1, NFloat.Epsilon, NFloat.MaxValue, NFloat.MinValue })
{
Assert.AreEqual(value.ToPreciseNumber(), Checked<PreciseNumber, NFloat>(value), value.ToString(CultureInfo.InvariantCulture));
}

Assert.ThrowsExactly<OverflowException>(() => Checked<PreciseNumber, NFloat>(NFloat.NaN));
Assert.AreEqual(PreciseNumber.Zero, Saturating<PreciseNumber, NFloat>(NFloat.NaN));
Assert.ThrowsExactly<OverflowException>(() => Truncating<PreciseNumber, NFloat>(NFloat.PositiveInfinity));
}

[TestMethod]
public void ToNFloatIsCorrectlyRounded()
{
AssertToInAllModes("1.5", (NFloat)1.5);
Assert.AreEqual((NFloat)1.5, P("1.5").To<NFloat>());

NFloat expectedTenth = NFloat.Size == sizeof(float) ? new NFloat(0.1f) : new NFloat(0.1);
AssertToInAllModes("0.1", expectedTenth);
Assert.AreEqual(expectedTenth, P("0.1").To<NFloat>());

// Beyond range overflows to an infinity, and below it underflows to zero, as for double and float.
AssertToInAllModes("1E400", NFloat.PositiveInfinity);
AssertToInAllModes("-1E400", NFloat.NegativeInfinity);
AssertToInAllModes("1E-400", (NFloat)0.0);
}

[TestMethod]
public void NFloatRoundTripsThroughPreciseNumber()
{
foreach (NFloat value in new[] { (NFloat)1.5, (NFloat)0.1, NFloat.Epsilon, NFloat.MaxValue, NFloat.MinValue, (NFloat)(1.0 / 3) })
{
string label = value.ToString(CultureInfo.InvariantCulture);
PreciseNumber number = Checked<PreciseNumber, NFloat>(value);
Assert.AreEqual(value, Checked<NFloat, PreciseNumber>(number), label);
Assert.AreEqual(value, Saturating<NFloat, PreciseNumber>(number), label);
Assert.AreEqual(value, Truncating<NFloat, PreciseNumber>(number), label);
Assert.AreEqual(value, number.To<NFloat>(), label);
}
}

[TestMethod]
public void FromPreciseNumberIsIdentity()
{
Expand Down
17 changes: 15 additions & 2 deletions PreciseNumber/PreciseNumber.Conversions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Numerics;
using System.Runtime.InteropServices;

/// <summary>
/// Conversions between <see cref="PreciseNumber"/> and the other numeric types, as generic math
Expand Down Expand Up @@ -147,8 +148,8 @@
/// <typeparamref name="TOther"/> is an integer type or <see cref="decimal"/>, and the value is outside its range.
/// </exception>
/// <remarks>
/// Integer types keep the integral part, truncated toward zero. <see cref="double"/>, <see cref="float"/>
/// and <see cref="Half"/> receive the nearest representable value, correctly rounded however many digits
/// Integer types keep the integral part, truncated toward zero. <see cref="double"/>, <see cref="float"/>,
/// <see cref="Half"/> and <see cref="NFloat"/> receive the nearest representable value, correctly rounded however many digits
/// the number has, and overflow to an infinity rather than throwing, as every built-in conversion to a
/// binary floating point type does. <see cref="decimal"/> keeps as many digits as it can hold, rounding
/// the rest the way <see cref="decimal.Parse(string, NumberStyles, IFormatProvider)"/> does.
Expand Down Expand Up @@ -219,6 +220,11 @@
return TryConvertFromBinaryFloatingPoint((Half)(object)value, mode, out result);
}

if (typeof(TOther) == typeof(NFloat))
{
return TryConvertFromBinaryFloatingPoint((NFloat)(object)value, mode, out result);
}

if (typeof(TOther) == typeof(decimal))
{
result = CreateFromFloatingPoint((decimal)(object)value);
Expand Down Expand Up @@ -292,6 +298,13 @@
return true;
}

if (typeof(TOther) == typeof(NFloat))
{
// NFloat is a float or a double depending on the platform, so round once, to whichever it is.
result = (TOther)(object)(NFloat.Size == sizeof(float) ? new NFloat(value.ToSingle()) : new NFloat(value.ToDouble()));
return true;
}

if (typeof(TOther) == typeof(decimal))
{
result = (TOther)(object)value.ToDecimal(mode);
Expand Down Expand Up @@ -443,7 +456,7 @@

return mode == ConversionMode.Checked
? throw new OverflowException("Value was either too large or too small for a Decimal.")
: Significand.Sign < 0 ? decimal.MinValue : decimal.MaxValue;

Check warning on line 459 in PreciseNumber/PreciseNumber.Conversions.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 459 in PreciseNumber/PreciseNumber.Conversions.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.
}

/// <summary>
Expand Down
7 changes: 5 additions & 2 deletions PreciseNumber/PreciseNumber.cs
Original file line number Diff line number Diff line change
Expand Up @@ -736,7 +736,7 @@
/// A fixed precision such as <c>E15</c> rounds values that need 17 digits, which turns
/// <see cref="double.MaxValue"/> into a number that converts back to infinity.
/// </remarks>
internal static string GetStringFormatForFloatType<TFloat>()

Check warning on line 739 in PreciseNumber/PreciseNumber.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Remove this method and declare a constant for this value.

Check warning on line 739 in PreciseNumber/PreciseNumber.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Remove this method and declare a constant for this value.
where TFloat : INumber<TFloat>
=> "R";

Expand Down Expand Up @@ -1197,7 +1197,7 @@
public static PreciseNumber MinMagnitudeNumber(PreciseNumber x, PreciseNumber y) => MinMagnitude(x, y);

/// <inheritdoc/>
public static PreciseNumber Parse(ReadOnlySpan<char> s, NumberStyles style, IFormatProvider? provider)

Check warning on line 1200 in PreciseNumber/PreciseNumber.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 28 to the 15 allowed.

Check warning on line 1200 in PreciseNumber/PreciseNumber.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

All 'Parse' method overloads should be adjacent.

Check warning on line 1200 in PreciseNumber/PreciseNumber.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 28 to the 15 allowed.

Check warning on line 1200 in PreciseNumber/PreciseNumber.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

All 'Parse' method overloads should be adjacent.
{
// Read the separator and signs from the same culture TryFormat writes them with, so that
// Parse(x.ToString(p), p) round-trips for every provider.
Expand Down Expand Up @@ -1247,14 +1247,14 @@
}

hasDecimal = true;
i += decimalSeparator.Length - 1;

Check warning on line 1250 in PreciseNumber/PreciseNumber.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Do not update the stop condition variable 'i' in the body of the for loop.
continue;
}

// Group separators may only sit between integral digits.
if (allowThousands && !hasDecimal && digitCount > 0 && s[i..].StartsWith(groupSeparator, StringComparison.Ordinal))
{
i += groupSeparator.Length - 1;

Check warning on line 1257 in PreciseNumber/PreciseNumber.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Do not update the stop condition variable 'i' in the body of the for loop.
continue;
}

Expand Down Expand Up @@ -2152,8 +2152,11 @@
/// <remarks>
/// Built-in numeric types and <see cref="BigInteger"/> convert as
/// <see cref="TryConvertToChecked{TOther}(PreciseNumber, out TOther)"/> describes: integer types truncate
/// toward zero, and binary floating point types are correctly rounded. Any other type is built from
/// the significand and a <see cref="double"/> power of ten, and is limited to that precision.
/// toward zero, and binary floating point types, <see cref="System.Runtime.InteropServices.NFloat"/>
/// included, are correctly rounded. Any other type is built from the significand and a
/// <see cref="double"/> power of ten, and is limited to that precision; it must support
/// <c>CreateChecked</c> from <see cref="BigInteger"/> and <see cref="double"/>, or this throws
/// <see cref="NotSupportedException"/>.
/// </remarks>
public TOutput To<TOutput>()
where TOutput : INumber<TOutput> =>
Expand Down
Loading