# 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. ## 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`. | 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: ```csharp 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. Precomputed roots/logarithms do not imply general `Sqrt`/`Log` functions are implemented. ## 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 0–999; 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, ...)` 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`, 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` and implement `IParsable` / `ISpanParsable`. 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. ```csharp 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 The planned `PreciseMath` static class and its `Abs`, `Sqrt`, `Pow`, `Exp`, and `Log` functions are not implemented. Generic-math interfaces beyond `ISignedNumber`, 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: ```sh 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: ```sh # 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 ` 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](LICENSE).