172 lines
9.4 KiB
Markdown
172 lines
9.4 KiB
Markdown
# 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. The constants `PI`, `E`, and `LN2` include binary64
|
||
residuals checked against independently computed high-precision values.
|
||
|
||
- 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
|
||
has not been benchmarked, including the allocating exponent-boundary path.
|
||
|
||
## 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<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.
|
||
|
||
```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 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.
|
||
|
||
## Project structure
|
||
|
||
- `0-source/Just.PreciseMath/`: library implementation.
|
||
- `1-tests/Just.PreciseMath.Tests/`: unit tests.
|
||
- `2-benchmarks/Just.PreciseMath.Benchmarks/`: reserved for performance benchmarks.
|
||
|
||
## 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).
|