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
6 changes: 4 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,18 @@ dotnet run -c Release --project PreciseNumber.Benchmarks -- --filter '*' --job s

- Factory methods `CreateFromInteger<T>()` and `CreateFromFloatingPoint<T>()` handle type-specific conversion logic
- Addition, subtraction and modulus align exponents before calculating; multiplication and division work on the significands directly
- `Divide` is exact when the quotient terminates, and otherwise rounds to a precision that never falls below the wider operand or `MinimumDivisionPrecision`. `Exp` and non-integer `Pow` still route through `double`
- `Divide` is exact when the quotient terminates, and otherwise rounds to a precision that never falls below the wider operand or `MinimumDivisionPrecision`
- Roots (`PreciseNumber/PreciseNumber.Roots.cs`, satisfying `IRootFunctions<PreciseNumber>`) follow `Divide`'s precision rule and do not route through `double`. Each scales the significand by a power of ten until the degree divides the exponent, then takes an integer Newton root of the significand, so an exact root stops on the exact answer rather than on a tolerance and no seed has to survive a value outside `double`'s range
- Exponentials, logarithms and powers (`PreciseNumber/PreciseNumber.Exponentials.cs`, satisfying `IExponentialFunctions`, `ILogarithmicFunctions` and `IPowerFunctions`) follow the same precision rule and do not route through `double` either. `ln(m · 10^k)` is `ln m + k · ln 10` against the stored `Ln10`, with the mantissa centred on `[1/√10, √10)` and fed to the atanh series; `exp(v)` factors out `10^round(v / ln 10)` as an exponent shift and halves what is left before a Taylor sum. Nothing here is a free-standing decision: `Exp10`/`Log10` must not route through the natural log, because the exponent is the whole answer for a power of ten, and the `…M1`/`…P1` variants must not be computed as `Exp(x) - 1`/`Log(1 + x)`, because that cancels away the precision near zero they exist to keep
- A fractional `Pow` is `exp(y · ln x)`, carried wider by the integer digits of `y · ln x` because `Exp`'s range reduction consumes them. The integer path stays exponentiation by squaring and is exact; tests pin that exactness rather than a tolerance
- The `sanitize` constructor parameter controls whether trailing zeros are removed (default: true)
- Constants (`Zero`, `One`, `Pi`, `E`, `Tau`) are pre-computed static instances
- 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

### Test Structure

Tests use MSTest. `PreciseNumber.Test/PreciseNumberTests.cs` covers arithmetic, parsing, and formatting, `PreciseNumberConversionTests.cs` covers generic math conversion in every mode, `PreciseNumberRootTests.cs` pins the roots against published digits and against squaring back, and `PreciseNumberValueTypeTests.cs` pins `default` as zero and asserts that small-value addition, subtraction, multiplication, and comparison allocate nothing. The test project targets only .NET 10.0 while the main library multi-targets net7.0, net8.0, net9.0, and net10.0.
Tests use MSTest. `PreciseNumber.Test/PreciseNumberTests.cs` covers arithmetic, parsing, and formatting, `PreciseNumberConversionTests.cs` covers generic math conversion in every mode, `PreciseNumberRootTests.cs` pins the roots against published digits and against squaring back, `PreciseNumberExponentialTests.cs` does the same for the exponentials and logarithms and additionally pins the cases a `double` fallback cannot reach — fifty published digits of a fractional power, and `ExpM1`/`LogP1` of `1e-30` not collapsing to zero — and `PreciseNumberValueTypeTests.cs` pins `default` as zero and asserts that small-value addition, subtraction, multiplication, and comparison allocate nothing. The test project targets only .NET 10.0 while the main library multi-targets net7.0, net8.0, net9.0, and net10.0.

### Benchmarks

Expand Down
106 changes: 106 additions & 0 deletions PreciseNumber.Benchmarks/ExponentialBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.PreciseNumber.Benchmarks;

using BenchmarkDotNet.Attributes;

/// <summary>
/// Measures the exponentials, the logarithms, and the fractional power built on them.
/// </summary>
/// <remarks>
/// These replaced a <see cref="double"/> round trip, so the cost of correctness belongs on the
/// record rather than being discovered later. <see cref="DoubleExpBaseline"/> and
/// <see cref="DoubleLogBaseline"/> are that record: they are what <c>Exp</c> and a fractional
/// <c>Pow</c> used to do, and they answer in about fifteen correct digits whatever the
/// <c>Digits</c> axis says, so read them as a floor on the measurement rather than as an
/// alternative.
/// <para>
/// The <c>Digits</c> axis drives both the operand and the digits asked of the answer, since the
/// default precision follows the operand. Both series are summed at the requested precision, so
/// the cost grows with the digit count twice over: more terms are needed, and each term is a
/// wider <see cref="System.Numerics.BigInteger"/>. Read it against
/// <see cref="ArithmeticBenchmarks"/>'s division, which is the operation inside both loops.
/// </para>
/// <para>
/// <see cref="Exp10OfAnInteger"/> and <see cref="Log10OfAPowerOfTen"/> are the cases the
/// representation answers for free — an exponent shift and an exponent read. They should not move
/// with the <c>Digits</c> axis at all, and should allocate nothing beyond the one significand. A
/// regression there means a series is being run where none is needed.
/// </para>
/// </remarks>
[MemoryDiagnoser]
public class ExponentialBenchmarks
{
private PreciseNumber value = PreciseNumber.Zero;
private PreciseNumber exponent = PreciseNumber.Zero;
private PreciseNumber fractionalPower = PreciseNumber.Zero;
private PreciseNumber integerPower = PreciseNumber.Zero;
private PreciseNumber powerOfTen = PreciseNumber.Zero;
private PreciseNumber tenExponent = PreciseNumber.Zero;
private double valueAsDouble;
private double exponentAsDouble;

/// <summary>
/// Gets or sets the number of significant digits in the operand, and so in the answer.
/// </summary>
[Params(8, 30, 200)]
public int Digits { get; set; }

/// <summary>
/// Prepares the operands.
/// </summary>
[GlobalSetup]
public void Setup()
{
// Kept near one so that the logarithm's mantissa reduction and the exponential's power-of-ten
// reduction are both exercised without either dominating.
value = Operands.Number(Digits, -(Digits - 1));
exponent = Operands.Number(Digits, -Digits, offset: 11);
fractionalPower = Operands.Number(Digits, -Digits, offset: 23);
integerPower = 64.ToPreciseNumber();
powerOfTen = PreciseNumber.Parse("1E50", System.Globalization.CultureInfo.InvariantCulture);
tenExponent = 50.ToPreciseNumber();
valueAsDouble = value.To<double>();
exponentAsDouble = exponent.To<double>();
}

/// <summary>Takes the natural logarithm.</summary>
/// <returns>The result.</returns>
[Benchmark]
public PreciseNumber Log() => PreciseNumber.Log(value);

/// <summary>Raises e to a power.</summary>
/// <returns>The result.</returns>
[Benchmark]
public PreciseNumber Exp() => PreciseNumber.Exp(exponent);

/// <summary>Raises a value to a fractional power, which is an exponential of a logarithm.</summary>
/// <returns>The result.</returns>
[Benchmark]
public PreciseNumber PowFractional() => value.Pow(fractionalPower);

/// <summary>Raises a value to an integer power, which is exponentiation by squaring and exact.</summary>
/// <returns>The result.</returns>
[Benchmark]
public PreciseNumber PowInteger() => value.Pow(integerPower);

/// <summary>Raises ten to an integer power, which is an exponent shift and no series.</summary>
/// <returns>The result.</returns>
[Benchmark]
public PreciseNumber Exp10OfAnInteger() => PreciseNumber.Exp10(tenExponent);

/// <summary>Takes the base-10 logarithm of a power of ten, which is an exponent read and no series.</summary>
/// <returns>The result.</returns>
[Benchmark]
public PreciseNumber Log10OfAPowerOfTen() => PreciseNumber.Log10(powerOfTen);

/// <summary>The <see cref="double"/> logarithm these replaced, as a floor on the measurement.</summary>
/// <returns>The result.</returns>
[Benchmark(Baseline = true)]
public double DoubleLogBaseline() => Math.Log(valueAsDouble);

/// <summary>The <see cref="double"/> exponential these replaced, as a floor on the measurement.</summary>
/// <returns>The result.</returns>
[Benchmark]
public double DoubleExpBaseline() => Math.Exp(exponentAsDouble);
}
Loading
Loading