just 24d2aef892
.NET Test / .NET tests (push) Successful in 1m46s
interface rearrangement
2026-09-16 17:56:44 +04:00
2026-09-13 16:52:34 +04:00
2026-09-16 17:56:44 +04:00
2026-09-16 17:56:44 +04:00
2026-09-14 13:58:04 +04:00
2026-09-13 19:22:58 +04:00
2026-09-13 16:52:34 +04:00
2026-09-13 19:22:58 +04:00
2026-09-15 01:25:57 +04:00
2026-09-13 17:46:15 +04:00
2026-09-13 16:52:34 +04:00
2026-09-13 16:52:34 +04:00
2026-09-13 16:52:34 +04:00
2026-09-16 17:56:44 +04:00

Just.PreciseMath

Extended-precision floating-point arithmetic for .NET using double-double representations: a high/low pair of double values. The goal is to retain more precision than a single double while using a fixed-size representation, rather than arbitrary-precision arithmetic.

Work in progress. The public API is incomplete and may change. Numerical contracts are covered by regression tests, not an exhaustive accuracy certification. The library is not ready for production use.

DoubleDouble core

DoubleDouble stores a normalized high/low pair. Use new DoubleDouble(value) for a single double, or DoubleDouble.FromComponents(high, low) for arbitrary components. The factory normalizes finite sums and canonicalizes NaN/infinity with a positive-zero low component. The two-component constructor is internal and performs no normalization or validation; it is reserved for trusted, already-normalized results. Mathematical constants use precomputed high/low pairs checked against independently computed high-precision values; accessing them does not perform double-double arithmetic or allocate on the heap.

  • Arithmetic: unary +/-, binary +, -, *, /, and both operand orders with a double. Addition retains residuals under cancellation; multiplication uses fused multiply-add; division uses residual corrections. Mixed double operators use specialized scalar paths rather than promoting the scalar to DoubleDouble. Their finite fast paths normalize once with a final sum transform; scalar division uses one compensated quotient correction within the error contract below.
  • Exponent boundaries: bounded BigInteger calculations avoid intermediate overflow and underflow on the exceptional finite path. Ordinary arithmetic uses floating-point transforms without allocations. The stored value remains two doubles; this is not an arbitrary-precision API.
  • Comparisons use both components. Equals treats NaNs as equal and signed zeros as equal for collections. CompareTo orders NaN before other values. Numerical equality and relational operators treat NaN as unordered, like double.
  • Signed zero is preserved by single-value construction and unary negation. FromComponents with a zero low input preserves the high zero's sign. Exact cancellation of nonzero values yields positive zero. Arithmetic special values follow binary64 rules.

Arithmetic is approximate double-double arithmetic, not a promise of correctly rounded 106-bit results. The deterministic rational-oracle tests check a conservative error bound of 2^-100 relative plus one minimum binary64 subnormal, with exact component checks for selected representable cases. Near underflow, extended precision necessarily decreases; overflow produces infinity. Performance of the allocating exponent-boundary path is not covered by the basic benchmarks.

Named value constants include Zero, NegativeZero, One, NegativeOne, NaN, PositiveInfinity, NegativeInfinity, and Epsilon. Epsilon is the smallest positive representable value, 2^-1074 (the same as double.Epsilon), not a relative-error tolerance or machine epsilon. These constants have a positive-zero low component; NegativeZero preserves the high sign bit while comparing and hashing equal to Zero.

Predefined mathematical constants

All constants below are static DoubleDouble properties. Each stores the nearest binary64 high component followed by the nearest binary64 residual, rather than calculating a ratio, root, or logarithm on access. Names use PascalCase, including Pi, E, and Ln2.

DoubleDouble implements IFloatingPointConstants<DoubleDouble> for generic access to E, Pi, and Tau; this does not imply support for the full IFloatingPointIeee754<DoubleDouble> interface.

Group Properties and values
Circle and common angles Pi (π), Tau (2π), PiOver2, PiOver3, PiOver4, PiOver6
Angular conversion DegToRad (π/180), RadToDeg (180/π), InvPi (1/π), InvTau (1/(2π), radians to turns)
Exponential and logarithmic E, InvE (1/e), Ln2 (ln 2), Ln10 (ln 10)
Log-base conversion Log2E (1/ln 2), Log10E (1/ln 10), Log2Of10 (ln 10/ln 2), Log10Of2 (ln 2/ln 10)
Roots and geometry Sqrt2, Sqrt3, Sqrt5, InvSqrt2, InvSqrt3, GoldenRatio ((1+√5)/2)
Gaussian and error-function factors SqrtPi, InvSqrtPi, TwoInvSqrtPi (2/√π), SqrtTau (√(2π)), InvSqrtTau (1/√(2π))

Multiply by conversion factors instead of recomputing them:

using Just.PreciseMath;

DoubleDouble degrees = new(180.0);
DoubleDouble radians = degrees * DoubleDouble.DegToRad;
DoubleDouble convertedDegrees = radians * DoubleDouble.RadToDeg;
DoubleDouble quarterTurn = DoubleDouble.PiOver2;

The factors avoid deriving constants at runtime; the multiplication itself remains approximate DD arithmetic, so conversions are not guaranteed exact round trips. The list is mathematical and dimensionless, not a table of unit-dependent physical constants. The natural logarithm function is provided separately by DDMath.Log.

Mathematical functions

The DDMath static class provides:

  • Abs(DoubleDouble): preserves both components, maps either signed zero to positive zero and either infinity to positive infinity, and returns canonical NaN. It shares the existing DoubleDouble.Abs implementation.
  • Reciprocal(DoubleDouble): returns exactly the same high and low component bits as 1.0 / value. It specializes scalar/DD division for a numerator of one, omitting only redundant numerator checks while preserving both divisions, the FMA sequence, and normalization. Signed zeros map to signed infinities, signed infinities to signed zeros, and NaN to canonical NaN. The allocating exact boundary path handles extreme exponents; finite overflow produces signed infinity. No speedup over scalar/DD division has been measured.
  • Sqrt(DoubleDouble): uses power-of-two scaling and an FMA-based Newton correction to retain extended precision, including for subnormal inputs, without squaring an unscaled estimate near the exponent limits. Signed zero and positive infinity are preserved; negative nonzero inputs and NaN return canonical NaN.
  • InvSqrt(DoubleDouble): computes the reciprocal square root with power-of-two scaling and a compensated Newton step, avoiding double-double division and its allocating boundary paths. The estimate's squared-product residual is retained with FMA. Signed zeros map to correspondingly signed infinities; positive infinity maps to positive zero; negative nonzero inputs and NaN return canonical NaN. This is a dedicated algorithm, not a claim of measured speedup over 1.0 / Sqrt(x).
  • Pow(DoubleDouble, int): exponentiation by squaring with a separately tracked binary exponent. Supports the full int domain, including int.MinValue, and reciprocates a bounded significand before final scaling for negative exponents. Any value to power zero is one (including NaN and signed zero). Other NaNs propagate; zero/infinity signs follow integer-power parity and exponent sign.
  • Pow(DoubleDouble, double) and Pow(DoubleDouble, DoubleDouble): arbitrary real powers, retaining the base's low component and, for the DD overload, the exponent's low component. For finite exponents, negative finite bases require integers; integrality and odd/even parity use the complete exponent, even above 2^53. Integer-valued exponents within int range reuse the integer implementation. Other finite cases share Log's range-scaled finite kernel, followed by Exp. Near-one bases retain tiny low components before multiplication by large powers.
  • Exp(DoubleDouble): binary range reduction with three components of ln(2), followed by a [12/12] Padé approximation and power-of-two scaling. Both input components affect range boundaries; representable subnormals are retained. Either zero maps to one, negative infinity to positive zero, positive infinity to positive infinity, and NaN to canonical NaN.
  • Log(DoubleDouble): natural logarithm using binary range reduction and a centered atanh series. The bounded mantissa avoids denominator overflow for large inputs; a separate near-one path retains even minimum-subnormal low components. Either zero maps to negative infinity, one to positive zero, positive infinity to itself, and negative nonzero inputs or NaN to canonical NaN.
using Just.PreciseMath;

DoubleDouble root = DDMath.Sqrt(new DoubleDouble(2.0));
DoubleDouble inverseRoot = DDMath.InvSqrt(new DoubleDouble(2.0));
DoubleDouble reciprocal = DDMath.Reciprocal(new DoubleDouble(3.0));
DoubleDouble magnitude = DDMath.Abs(-root);
DoubleDouble smallPower = DDMath.Pow(new DoubleDouble(2.0), -1024);
DoubleDouble exponential = DDMath.Exp(new DoubleDouble(1.0));
DoubleDouble logarithm = DDMath.Log(new DoubleDouble(10.0));
DoubleDouble fractionalPower = DDMath.Pow(new DoubleDouble(2.0), 0.5);
DoubleDouble preciseExponent = DoubleDouble.FromComponents(0.5, 1e-30);
DoubleDouble precisePower = DDMath.Pow(new DoubleDouble(2.0), preciseExponent);

Reciprocal tests check bitwise equivalence with 1.0 / value and independently check exact rational error against 2^-100 relative plus one minimum binary64 subnormal. They sample every binary64 exponent, both signs, dense and sparse lows, binade neighbors, and special values; selected powers of two and sparse corrections also have exact component checks. This preserves division's approximate-accuracy contract, not a guarantee of correctly rounded results.

Square-root and inverse-square-root tests compare the exact component sum against a 2^-100 relative error bound using integer inequalities. They include samples at every binary64 exponent, boundary neighbors, both signs of the low component, and exact binary squares/powers of four. Square-root tests also bracket exact root-rounding midpoints; both suites exercise half-ulp low-component normalization boundaries. This is a tested approximate-accuracy contract, not exhaustive coverage of all component pairs or a guarantee of correctly rounded results.

Logarithm tests compare exact component sums with independently generated 120-digit decimal references, requiring agreement at 450 and 650 digits. They check 2^-100 relative error plus one minimum binary64 subnormal, with an explicit reference-rounding allowance. Powers of two cover every finite binary64 exponent; additional tests cover extreme magnitudes, near-one cancellation, sparse lows of either sign, and range-reduction transitions. This is sampled approximate accuracy, not a universal error proof or a correct-rounding guarantee.

Exponential tests use independent high-precision decimal references evaluated at two precisions, with exact integer comparisons of the stored component sum. They check 2^-100 relative error plus one minimum binary64 subnormal, with a separately bounded reference-rounding allowance. Integer-power tests use exact rational references and high-precision fixtures for large exponents; their tested absolute error bound is |exact result| * (|exponent| + 1) * 2^-100 + 2^-1074, not uniform relative accuracy independent of the exponent. Both functions are approximate; precision decreases near underflow and approximation can affect results extremely close to a rounding boundary. Final scaling uses the existing allocating exact boundary machinery where necessary. No performance measurements are claimed.

For real powers, x^±0 = 1 and 1^y = 1, including NaN in the other operand; other NaNs propagate. Infinite exponents compare the complete |x| with one, with (-1)^±∞ = 1. Zero and infinite bases yield a negative sign only for odd integer exponents; negative powers exchange zero and infinity. Finite negative bases with non-integer finite exponents return canonical NaN.

Real-power reference tests check 2^-90 relative error plus one minimum binary64 subnormal for the logarithm/Exp path, with an explicit reference-rounding allowance. Integer dispatch retains the exponent-dependent bound above. These are tested approximate-accuracy contracts, not universal error proofs or correct-rounding guarantees. Reference inputs are exact component sums; logarithms/exponentials are independently evaluated at 450 and 650 decimal digits.

Known real-power limitations: extremely close to the range boundaries, the logarithm/Exp path can return infinity for a mathematically finite result, or a minimum subnormal where zero is expected. The latter can also break monotonicity across integer-exponent dispatch. These issues remain unresolved; passing the sampled error bounds does not guarantee correct range decisions for every input.

Conversions and formatting

  • Explicit conversions support double, float, int, long, and decimal in both directions. Integer inputs are exact. Decimal inputs use their exact coefficient and scale to compute the high component and its residual.
  • Integer casts truncate the complete expansion toward zero and throw OverflowException for nonfinite or out-of-range results. IConvertible integer conversions instead round to nearest, ties to even, with range checks.
  • Binary32 output rounds the complete expansion directly, including low-component decisions at midpoints. Decimal output rounds to the greatest fitting scale up to 28; nonfinite values and magnitudes above decimal.MaxValue throw.
  • IConvertible reports TypeCode.Object, supports conversion to itself, and treats only numerical zero as false. Char, DateTime, and enum conversions are unsupported and throw InvalidCastException.
  • ToString formats the exact component sum, supports culture-sensitive G/g, E/e, and F/f, and rounds ties to even. Precision is bounded to 0999; other standard and custom formats throw FormatException. Default G32 is not shortest-round-trip formatting. NaN, infinities, and signed zero are supported without converting through decimal.
  • TryFormat(Span<char>, ...) implements ISpanFormattable with the same formats. It currently allocates via ToString; insufficient space returns false, writes zero characters, and leaves the destination unchanged.

DoubleDouble implements ISignedNumber<DoubleDouble>, including the inherited INumberBase contracts: binary radix, classification, absolute value, magnitude selection, increment/decrement, and generic numeric conversions. Integer/parity tests and magnitude comparisons retain both components. Magnitude ties prefer positive values for maximum and negative values for minimum, including signed zero; the Number variants prefer a number over NaN.

CreateChecked, CreateSaturating, and CreateTruncating support built-in numeric types and BigInteger. Floating overflow produces signed infinity in all modes. Finite integer output truncates the exact sum, then throws on overflow, clamps, or retains the low destination-width bits, respectively. Decimal nonchecked output clamps out-of-range values and maps NaN to zero. These policies are distinct from the existing casts and IConvertible conversions above.

Conversions, parsing, and formatting use allocating BigInteger intermediates where needed to preserve precision; no additional dependency is required.

Parsing

Parse and TryParse accept strings and ReadOnlySpan<char> and implement IParsable<DoubleDouble> / ISpanParsable<DoubleDouble>. This initial parser preserves high/low precision rather than parsing through double or decimal. It converts an exact decimal coefficient/exponent into rounded high and residual components, then normalizes the pair. It does not promise universally correctly rounded 106-bit results or a general ToString round trip.

using System.Globalization;
using Just.PreciseMath;

DoubleDouble value = DoubleDouble.Parse("9007199254740993", CultureInfo.InvariantCulture);
// value.High == 9007199254740992.0; value.Low == 1.0

bool success = DoubleDouble.TryParse("1.25e-2".AsSpan(), CultureInfo.InvariantCulture,
    out DoubleDouble parsed);
  • Provider-only finite grammar: optional sign, ASCII decimal digits with an optional decimal separator, and optional e/E exponent with sign and digits. At least one mantissa digit is required; .5 and 1. are accepted with invariant culture. Surrounding whitespace is allowed; internal whitespace is not.
  • Signs and the decimal separator come from the supplied culture; a null or omitted provider uses the current culture. Culture-specific NaN and infinity symbols are recognized case-insensitively. The additional alias inf accepts an optional culture-specific sign (inf, +inf, -inf with invariant culture). Exact custom special symbols take precedence over the alias. Special values accept surrounding whitespace and signs even with NumberStyles.None; ordinary finite numbers still obey the supplied style flags. Signed zero is preserved.
  • Provider-only overloads reject grouping, currency, and parentheses. Explicit NumberStyles overloads support decimal flags through NumberStyles.Any, including grouping, currency, parentheses, and trailing signs; group sizes are not validated. Hexadecimal, binary, and undefined style flags throw ArgumentException, including in TryParse. Hexadecimal notation and programming-language digit separators remain unsupported.
  • Input is limited to 2048 characters, including surrounding whitespace. Huge exponents are bounded before constructing powers of ten. Well-formed overflow succeeds with signed infinity; underflow rounds to a subnormal or signed zero. A second rounding just below the overflow midpoint stays finite.
  • Parse throws ArgumentNullException for a null string and FormatException for invalid, unsupported, or oversized input. TryParse returns false and positive Zero for those inputs.

Deferred scope

Natural DDMath.Log, Exp, and all three Pow overloads are implemented. Logarithms in other bases, generic-math interfaces beyond ISignedNumber and IFloatingPointConstants, additional text formats/general round-trip formatting, and non-arithmetic performance benchmarks remain deferred. Replacing allocating arithmetic boundary fallbacks is also deferred; the current BigInteger paths remain in place. That optimization does not require removing BigInteger from conversions, parsing, formatting, or independent test oracles.

Build and test

Requires the .NET 10 SDK in the 10.0.1xx feature band, as selected by global.json. Run from the repository root:

dotnet restore Just.PreciseMath.slnx --locked-mode
dotnet build Just.PreciseMath.slnx -c Release --no-restore
dotnet test --solution Just.PreciseMath.slnx -c Release --no-build --minimum-expected-tests 1
dotnet format Just.PreciseMath.slnx --verify-no-changes --no-restore

Test results and coverage reports are available in the test-results artifact on CI workflow runs.

Benchmarks

BenchmarkDotNet measures arithmetic throughput, dependent-chain latency, and allocations, including comparisons of DoubleDouble, decimal, and double, mixed scalar operations, and exponent-boundary paths. These are performance measurements, not accuracy tests.

After the Release build above, run from the repository root:

# Discover benchmark methods without running them.
dotnet run --project 2-benchmarks/Just.PreciseMath.Benchmarks -c Release --no-build -- --list flat

# Smoke test; Dry timings are not performance measurements.
dotnet run --project 2-benchmarks/Just.PreciseMath.Benchmarks -c Release --no-build -- --job Dry --filter '*'

# Full measurement run.
dotnet run --project 2-benchmarks/Just.PreciseMath.Benchmarks -c Release --no-build -- --filter '*'

Reports are written under the ignored BenchmarkDotNet.Artifacts/ directory. Replace '*' with a benchmark-name pattern to select a subset; use --artifacts <path> to keep runs separate. Run measurements on an idle machine and inspect BenchmarkDotNet warnings.

Project structure

  • 0-source/Just.PreciseMath/: library implementation.
  • 1-tests/Just.PreciseMath.Tests/: unit tests.
  • 2-benchmarks/Just.PreciseMath.Benchmarks/: arithmetic benchmarks against decimal and double.

Contributing

Follow .editorconfig and include regression tests with numerical changes. Explain the algorithm's assumptions, the source of reference values, and any error tolerances. Include updated packages.lock.json files with dependency changes.

License

Licensed under the MIT License.

S
Description
A .NET library for extended-precision floating-point arithmetic using double-double representations and common mathematical functions.
Readme MIT
811 KiB
Languages
C# 96%
Python 4%