Files
Just.PreciseMath/README.md
T
just 082fd84c87
.NET Test / .NET tests (push) Successful in 1m14s
ported legacy DoubleDouble with tests
2026-09-13 22:19:23 +04:00

146 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
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.
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);
```
- Supported 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. Signed zero is preserved.
- Group separators, currency, parentheses, hexadecimal notation, digit separators,
and `NumberStyles` overloads are not supported.
- Input is limited to **4096 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. Broader generic-math interfaces, expanded
parsing/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).