You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 8c2cb14
Browse filesBrowse the repository at this point in the historyBrowse files
Divide computed its fractional part through a double, so it capped at
roughly sixteen significant digits regardless of what the operands
carried. On a type whose whole purpose is arbitrary precision that was
the wrong ceiling.
A quotient with a terminating decimal expansion is now produced exactly,
however many digits that takes. A reduced fraction terminates in base ten
when its denominator is a product of twos and fives, which is cheap to
test and usually cheap to reject: 1 / 8 is 0.125, and 1 / 2^64 keeps all
sixty-four decimal places rather than the seventeen a double allows.
A repeating quotient is taken to the precision of the wider operand, never
fewer than MinimumDivisionPrecision significant digits, with the last digit
rounded half away from zero. Whatever the scaled division discards is worth
less than one unit of the dropped digits, so it can never carry the rounding
decision across the halfway mark; at most it turns an exact tie into
something above it, which rounds the same way. A new overload takes the
precision explicitly.
This changes results. Three tests pinned the old behaviour and are updated:
two divisions that were correct only as far as a double reached, and one
that compared Exp against 1/e when both happened to be equally imprecise.
Exp still routes through a double, so that comparison is now made at the
precision Exp actually delivers.
It can also produce fewer significant digits than before in one case. The
old implementation computed the integer part of a quotient exactly and
appended a double-precision fraction, so a quotient with a long integer
part came out with more digits than the new default: 924880000e19 divided
by -4713100000e-19 gave fifty-four digits where it now gives fifty. That
was not a contract so much as a side effect, and an erratic one - the same
code gave seventeen digits for a quotient near one, and hundreds for a
quotient with a large exponent. A uniform floor with an explicit override
is the more predictable trade, and callers who want the extra digits can
ask for them.
Cost, from the benchmark suite: division is about twice as fast at eight
and thirty significant digits, because the double round trip through
formatting and parsing is gone, and about three and a half times slower at
two hundred, because it now computes two hundred digits instead of
discarding them.
Verified by checking every division in the differential fuzz harness
against the exact rational value rather than against the old
implementation: 4,944 terminating quotients exact to the digit and 32,347
repeating ones within half a unit of their last significant digit, inside
871,454 checks with no failures.
Also moves the benchmark project's coverage exemption out of the shared
.NET workflow and into the project itself. That workflow is duplicated
per repository rather than generated from one template - KtsuBuild carries
a near-identical copy - so an exclusion added there would have to be
repeated everywhere and re-applied on every resync. SonarQubeExclude in
the benchmark project travels with the project and needs no workflow to
know benchmarks exist, so the shared workflow goes back to its canonical
form.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018eTdSeGPQHGUKf9V3c2yXw
Copy file name to clipboardExpand all lines: CLAUDE.md
+2-1Lines changed: 2 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -33,7 +33,8 @@ dotnet run -c Release --project PreciseNumber.Benchmarks -- --filter '*' --job s
33
33
### Key Design Patterns
34
34
35
35
- Factory methods `CreateFromInteger<T>()` and `CreateFromFloatingPoint<T>()` handle type-specific conversion logic
36
-
- Arithmetic operations use `MakeCommonized()` to align exponents before calculation
36
+
- Addition, subtraction and modulus align exponents before calculating; multiplication and division work on the significands directly
37
+
-`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`
37
38
- The `sanitize` constructor parameter controls whether trailing zeros are removed (default: true)
Copy file name to clipboardExpand all lines: README.md
+9-1Lines changed: 9 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -351,9 +351,17 @@ You can control precision using:
351
351
352
352
-**ReduceSignificance()**: Reduces to a specific number of significant digits
353
353
354
+
-**Divide(left, right, significantDigits)**: Chooses the precision of a quotient
355
+
356
+
Division produces a terminating quotient exactly, however many digits that takes — `1 / 8` is
357
+
`0.125`, and `1 / 2^64` keeps all 64 decimal places. A repeating quotient is produced to the
358
+
precision of the wider operand, never fewer than `MinimumDivisionPrecision` (50) significant
359
+
digits, with the last digit rounded half away from zero. Pass an explicit precision to the
360
+
three-argument overload when you want something other than that.
361
+
354
362
## Limitations
355
363
356
-
-Operations that inherently require approximation (like certain roots or logarithms) fall back to`double` precision for calculation
364
+
-`Exp()`, and `Pow()` with a non-integer power, are computed through`double`and are therefore limited to its precision. Addition, subtraction, multiplication and division are not
357
365
358
366
- Conversion to standard types may throw `OverflowException` if the value is too large
0 commit comments