Skip to content

Commit 8c2cb14

Browse files
committed
Divide exactly, or to a chosen precision
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
1 parent e65a24c commit 8c2cb14

7 files changed

Lines changed: 282 additions & 20 deletions

File tree

‎.github/workflows/dotnet.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ jobs:
352352
'/d:sonar.host.url=https://sonarcloud.io'
353353
'/d:sonar.projectBaseDir=${{ github.workspace }}'
354354
'/d:sonar.cs.vscoveragexml.reportsPaths=coverage/**/coverage.xml'
355-
'/d:sonar.coverage.exclusions=**/*Test*.cs,**/*.Tests.cs,**/*.Tests/**/*,**/*.Benchmarks/**/*,**/obj/**/*,**/*.dll,**/NativeExports.cs'
355+
'/d:sonar.coverage.exclusions=**/*Test*.cs,**/*.Tests.cs,**/*.Tests/**/*,**/obj/**/*,**/*.dll,**/NativeExports.cs'
356356
'/d:sonar.cs.vstest.reportsPaths=coverage/**/*.trx'
357357
'/d:sonar.exclusions=**/NativeExports.cs'
358358
)

‎CLAUDE.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ dotnet run -c Release --project PreciseNumber.Benchmarks -- --filter '*' --job s
3333
### Key Design Patterns
3434

3535
- 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`
3738
- The `sanitize` constructor parameter controls whether trailing zeros are removed (default: true)
3839
- Constants (`Zero`, `One`, `Pi`, `E`, `Tau`) are pre-computed static instances
3940

‎PreciseNumber.Benchmarks/PreciseNumber.Benchmarks.csproj‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@
1616
internal copy here through InternalsVisibleTo - referencing it again would make
1717
every polyfilled member ambiguous. -->
1818
<NoWarn>$(NoWarn);KTSU0001</NoWarn>
19+
<!-- Benchmarks are tooling: not shipped, and not something unit tests cover. Telling the
20+
Sonar scanner so here keeps the coverage gate off this project's back without any
21+
workflow needing to know benchmarks exist. The equivalent workflow-level exclusion
22+
would have to be repeated in every repository that gains a benchmark project, and
23+
re-applied whenever the shared .NET workflow is resynced. -->
24+
<SonarQubeExclude>true</SonarQubeExclude>
1925
</PropertyGroup>
2026

2127
<ItemGroup>

‎PreciseNumber.Benchmarks/README.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,11 @@ operation returns a new instance, so a change that avoids an intermediate value
5757
`Allocated` column before it shows up in `Mean`. A comparison that allocates at all is a
5858
regression — none of them should.
5959

60-
One entry is easy to misread. `Divide` barely moves between 8 and 200 digits, and at 200 digits
61-
it comes out faster than `Add`. That is not division being efficient — it computes its fractional
62-
part through a `double`, so it discards everything past roughly the 17th significant digit and
63-
the result it constructs is small no matter how large the operands were. Read it as a measure of
64-
how much precision the operation throws away, not how fast it is.
60+
`Divide` is the one row whose cost is not driven by the operands alone. It produces a terminating
61+
quotient exactly and a repeating one to a chosen precision, so the `Digits` column moves it twice
62+
over: wider operands are more work to divide, and they also raise the precision the quotient is
63+
taken to. A number that looks expensive at 200 digits is doing proportionally more work, not doing
64+
the same work badly.
6565

6666
Benchmark operands come from a fixed digit pattern rather than a random source, so two runs on
6767
the same machine measure the same work. Numbers are still only comparable within a single run on

‎PreciseNumber.Test/PreciseNumberTests.cs‎

Lines changed: 126 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1031,8 +1031,10 @@ public void TestOperatorDivide()
10311031
PreciseNumber number1 = PreciseNumber.CreateFromComponents(-2, 12345);
10321032
PreciseNumber number2 = PreciseNumber.CreateFromComponents(-3, 678);
10331033
PreciseNumber result = number1 / number2;
1034-
Assert.AreEqual(BigInteger.Parse("18207964601769911504"), result.Significand);
1035-
Assert.AreEqual(-17, result.Exponent);
1034+
// 123.45 / 0.678 does not terminate, so it comes out at the default precision.
1035+
Assert.AreEqual(BigInteger.Parse("18207964601769911504424778761061946902654867256637"), result.Significand);
1036+
Assert.AreEqual(-47, result.Exponent);
1037+
Assert.AreEqual(PreciseNumber.MinimumDivisionPrecision, result.SignificantDigits);
10361038
}
10371039

10381040
[TestMethod]
@@ -1603,7 +1605,11 @@ public void TestExpWithPositivePower()
16031605
public void TestExpWithNegativePower()
16041606
{
16051607
PreciseNumber result = PreciseNumber.Exp(-1.ToPreciseNumber());
1606-
PreciseNumber expected = PreciseNumber.One / PreciseNumber.E; // e^-1 = 1/e
1608+
1609+
// Exp routes through a double, so 1/e is taken to the precision Exp actually delivers
1610+
// rather than the full precision Divide is now capable of.
1611+
PreciseNumber expected = PreciseNumber.Divide(PreciseNumber.One, PreciseNumber.E, result.SignificantDigits);
1612+
16071613
Assert.AreEqual(expected, result);
16081614
}
16091615

@@ -2000,6 +2006,122 @@ public void TestCountDigitsMatchesDecimalText()
20002006
}
20012007
}
20022008

2009+
[TestMethod]
2010+
public void TestDivideIsExactWhenTheQuotientTerminates()
2011+
{
2012+
// A quotient terminates exactly when the reduced denominator is a product of twos and
2013+
// fives, and those come out exact however many digits that takes.
2014+
(int Numerator, int Denominator, string Expected)[] cases =
2015+
[
2016+
(1, 2, "0.5"),
2017+
(1, 4, "0.25"),
2018+
(1, 5, "0.2"),
2019+
(1, 8, "0.125"),
2020+
(1, 10, "0.1"),
2021+
(1, 16, "0.0625"),
2022+
(1, 20, "0.05"),
2023+
(1, 25, "0.04"),
2024+
(3, 8, "0.375"),
2025+
(-3, 8, "-0.375"),
2026+
(7, 1, "7"),
2027+
];
2028+
2029+
foreach ((int numerator, int denominator, string expected) in cases)
2030+
{
2031+
PreciseNumber quotient = numerator.ToPreciseNumber() / denominator.ToPreciseNumber();
2032+
Assert.AreEqual(expected, quotient.ToString(CultureInfo.InvariantCulture), $"{numerator}/{denominator}");
2033+
}
2034+
}
2035+
2036+
[TestMethod]
2037+
public void TestDivideKeepsEveryDigitOfALongTerminatingQuotient()
2038+
{
2039+
// 2^-64 terminates, but only after 64 decimal places - far more than the default
2040+
// precision would allow, and far more than a double could carry.
2041+
PreciseNumber quotient = PreciseNumber.One / PreciseNumber.CreateFromComponents(0, BigInteger.Pow(2, 64));
2042+
2043+
Assert.AreEqual(
2044+
"0.0000000000000000000542101086242752217003726400434970855712890625",
2045+
quotient.ToString(CultureInfo.InvariantCulture));
2046+
2047+
// Exact means exact: multiplying back reproduces the dividend.
2048+
Assert.AreEqual(PreciseNumber.One, quotient * PreciseNumber.CreateFromComponents(0, BigInteger.Pow(2, 64)));
2049+
}
2050+
2051+
[TestMethod]
2052+
public void TestDivideProducesTheRequestedPrecisionWhenTheQuotientRepeats()
2053+
{
2054+
PreciseNumber one = PreciseNumber.One;
2055+
PreciseNumber three = 3.ToPreciseNumber();
2056+
2057+
Assert.AreEqual("0.3", PreciseNumber.Divide(one, three, 1).ToString(CultureInfo.InvariantCulture));
2058+
Assert.AreEqual("0.333", PreciseNumber.Divide(one, three, 3).ToString(CultureInfo.InvariantCulture));
2059+
Assert.AreEqual(20, PreciseNumber.Divide(one, three, 20).SignificantDigits);
2060+
Assert.AreEqual(200, PreciseNumber.Divide(one, three, 200).SignificantDigits);
2061+
}
2062+
2063+
[TestMethod]
2064+
public void TestDivideRoundsHalfAwayFromZero()
2065+
{
2066+
PreciseNumber two = 2.ToPreciseNumber();
2067+
PreciseNumber three = 3.ToPreciseNumber();
2068+
PreciseNumber five = 5.ToPreciseNumber();
2069+
PreciseNumber nine = 9.ToPreciseNumber();
2070+
2071+
// 0.666... rounds up, 0.333... rounds down, and the sign does not change which way.
2072+
Assert.AreEqual("0.667", PreciseNumber.Divide(two, three, 3).ToString(CultureInfo.InvariantCulture));
2073+
Assert.AreEqual("0.333", PreciseNumber.Divide(PreciseNumber.One, three, 3).ToString(CultureInfo.InvariantCulture));
2074+
Assert.AreEqual("-0.667", PreciseNumber.Divide(-two, three, 3).ToString(CultureInfo.InvariantCulture));
2075+
Assert.AreEqual("-0.333", PreciseNumber.Divide(-PreciseNumber.One, three, 3).ToString(CultureInfo.InvariantCulture));
2076+
2077+
// 0.555... cut after one digit sits just above the halfway mark, so it rounds away.
2078+
Assert.AreEqual("0.6", PreciseNumber.Divide(five, nine, 1).ToString(CultureInfo.InvariantCulture));
2079+
Assert.AreEqual("-0.6", PreciseNumber.Divide(-five, nine, 1).ToString(CultureInfo.InvariantCulture));
2080+
}
2081+
2082+
[TestMethod]
2083+
public void TestDivideNeverReducesTheOperandsPrecision()
2084+
{
2085+
// An operand carrying more digits than the floor pulls the quotient up to match it.
2086+
PreciseNumber wide = PreciseNumber.Parse(new string('7', 120), CultureInfo.InvariantCulture);
2087+
PreciseNumber three = 3.ToPreciseNumber();
2088+
2089+
Assert.AreEqual(120, (wide / three).SignificantDigits);
2090+
Assert.AreEqual(120, (three / wide).SignificantDigits);
2091+
Assert.AreEqual(PreciseNumber.MinimumDivisionPrecision, (three / 7.ToPreciseNumber()).SignificantDigits);
2092+
}
2093+
2094+
[TestMethod]
2095+
public void TestDivideRejectsNonPositivePrecision()
2096+
{
2097+
PreciseNumber one = PreciseNumber.One;
2098+
PreciseNumber three = 3.ToPreciseNumber();
2099+
2100+
Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => PreciseNumber.Divide(one, three, 0));
2101+
Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => PreciseNumber.Divide(one, three, -1));
2102+
}
2103+
2104+
[TestMethod]
2105+
public void TestDivideZeroDividend()
2106+
{
2107+
Assert.AreEqual(PreciseNumber.Zero, PreciseNumber.Zero / 3.ToPreciseNumber());
2108+
Assert.AreEqual(PreciseNumber.Zero, PreciseNumber.Zero / PreciseNumber.NegativeOne);
2109+
}
2110+
2111+
[TestMethod]
2112+
public void TestDivideRoundTripsThroughMultiplication()
2113+
{
2114+
// For a terminating quotient the round trip is exact, which is the strongest statement
2115+
// that can be made about a division.
2116+
foreach (int denominator in new[] { 2, 4, 5, 8, 10, 16, 20, 25, 32, 50, 64, 100, 125, 128 })
2117+
{
2118+
PreciseNumber divisor = denominator.ToPreciseNumber();
2119+
PreciseNumber quotient = 123456789.ToPreciseNumber() / divisor;
2120+
2121+
Assert.AreEqual(123456789.ToPreciseNumber(), quotient * divisor, $"123456789/{denominator}");
2122+
}
2123+
}
2124+
20032125
[TestMethod]
20042126
public void TestPow10IsCorrectAcrossCacheBoundaries()
20052127
{
@@ -2057,7 +2179,7 @@ public void TestDivideWithExponentsBeyondDoubleRange()
20572179

20582180
PreciseNumber result = left / right;
20592181

2060-
Assert.AreEqual("2.3333333333333333", result.ToString(CultureInfo.InvariantCulture));
2182+
Assert.AreEqual("2.3333333333333333333333333333333333333333333333333", result.ToString(CultureInfo.InvariantCulture));
20612183
}
20622184

20632185
[TestMethod]

‎PreciseNumber/PreciseNumber.cs‎

Lines changed: 133 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ public record PreciseNumber
4444
/// </summary>
4545
private const string InvalidFormatMessage = "Input string was not in a correct format.";
4646

47+
/// <summary>
48+
/// The fewest significant digits <see cref="Divide(PreciseNumber, PreciseNumber)"/> produces
49+
/// when a quotient does not terminate.
50+
/// </summary>
51+
public const int MinimumDivisionPrecision = 50;
52+
4753
/// <summary>
4854
/// Pre-computed powers of ten, grown on demand. Declared before any other static state so
4955
/// that the static constants below can rely on it while they are being initialized.
@@ -1291,30 +1297,149 @@ public static PreciseNumber Multiply(PreciseNumber left, PreciseNumber right)
12911297
/// <param name="left">The number to divide.</param>
12921298
/// <param name="right">The number to divide by.</param>
12931299
/// <returns>The result of the division.</returns>
1300+
/// <exception cref="DivideByZeroException">Thrown when <paramref name="right"/> is zero.</exception>
1301+
/// <remarks>
1302+
/// A quotient whose decimal expansion terminates is produced exactly, however many digits that
1303+
/// takes. One that repeats is produced to the precision of the wider operand, and never fewer
1304+
/// than <see cref="MinimumDivisionPrecision"/> significant digits, with the last digit rounded
1305+
/// half away from zero. Use <see cref="Divide(PreciseNumber, PreciseNumber, int)"/> to choose
1306+
/// that precision.
1307+
/// </remarks>
12941308
public static PreciseNumber Divide(PreciseNumber left, PreciseNumber right)
12951309
{
12961310
Ensure.NotNull(left);
12971311
Ensure.NotNull(right);
12981312

1313+
// Dividing must not silently discard precision the operands already carry.
1314+
int significantDigits = Math.Max(
1315+
Math.Max(left.SignificantDigits, right.SignificantDigits),
1316+
MinimumDivisionPrecision);
1317+
1318+
return Divide(left, right, significantDigits);
1319+
}
1320+
1321+
/// <summary>
1322+
/// Divides one number by another, to a chosen number of significant digits.
1323+
/// </summary>
1324+
/// <param name="left">The number to divide.</param>
1325+
/// <param name="right">The number to divide by.</param>
1326+
/// <param name="significantDigits">
1327+
/// The number of significant digits to produce when the quotient does not terminate. A quotient
1328+
/// that does terminate is exact regardless of this value.
1329+
/// </param>
1330+
/// <returns>The result of the division.</returns>
1331+
/// <exception cref="DivideByZeroException">Thrown when <paramref name="right"/> is zero.</exception>
1332+
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="significantDigits"/> is less than one.</exception>
1333+
public static PreciseNumber Divide(PreciseNumber left, PreciseNumber right, int significantDigits)
1334+
{
1335+
Ensure.NotNull(left);
1336+
Ensure.NotNull(right);
1337+
1338+
if (significantDigits < 1)
1339+
{
1340+
throw new ArgumentOutOfRangeException(nameof(significantDigits), significantDigits, "At least one significant digit is required.");
1341+
}
1342+
12991343
if (right.Significand.IsZero)
13001344
{
13011345
throw new DivideByZeroException();
13021346
}
13031347

1304-
if (Compare(left, right) == 0)
1348+
if (left.Significand.IsZero)
13051349
{
1306-
return One;
1350+
return Zero;
13071351
}
13081352

1309-
(BigInteger commonLeft, BigInteger commonRight, _) = CommonizeSignificands(left, right);
1353+
BigInteger numerator = left.Significand;
1354+
BigInteger denominator = right.Significand;
1355+
int exponent = left.Exponent - right.Exponent;
13101356

1311-
BigInteger integerComponent = BigInteger.DivRem(commonLeft, commonRight, out BigInteger remainder);
1357+
// Carry the sign on the numerator so the denominator can be factorized as a positive value.
1358+
if (denominator.Sign < 0)
1359+
{
1360+
numerator = -numerator;
1361+
denominator = -denominator;
1362+
}
13121363

1313-
// The common power of ten cancels between the remainder and the divisor, so it is left out
1314-
// entirely; including it would overflow to infinity for large exponents.
1315-
double fractionalComponent = double.CreateTruncating(remainder) / double.CreateTruncating(commonRight);
1364+
return TryDivideExactly(numerator, denominator, exponent, out PreciseNumber? exact)
1365+
? exact
1366+
: DivideToPrecision(numerator, denominator, exponent, significantDigits);
1367+
}
1368+
1369+
/// <summary>
1370+
/// Divides exactly, when the quotient has a terminating decimal expansion.
1371+
/// </summary>
1372+
/// <param name="numerator">The numerator, carrying the sign of the quotient.</param>
1373+
/// <param name="denominator">The denominator, which must be positive.</param>
1374+
/// <param name="exponent">The exponent the quotient's significand sits at.</param>
1375+
/// <param name="result">The exact quotient, when there is one.</param>
1376+
/// <returns><c>true</c> if the quotient terminates and <paramref name="result"/> is exact; otherwise <c>false</c>.</returns>
1377+
private static bool TryDivideExactly(BigInteger numerator, BigInteger denominator, int exponent, [NotNullWhen(true)] out PreciseNumber? result)
1378+
{
1379+
// A fraction terminates in base ten exactly when its denominator is 2^twos * 5^fives. Most
1380+
// denominators are rejected by the first remainder test, which is why this is worth trying
1381+
// before falling back to a rounded quotient.
1382+
int twos = (int)BigInteger.TrailingZeroCount(denominator);
1383+
BigInteger remaining = denominator >> twos;
1384+
1385+
int fives = 0;
1386+
while ((remaining % 5).IsZero)
1387+
{
1388+
remaining /= 5;
1389+
fives++;
1390+
}
1391+
1392+
if (!remaining.IsOne)
1393+
{
1394+
result = null;
1395+
return false;
1396+
}
1397+
1398+
// 1 / (2^p * 5^q) == (2^(k-p) * 5^(k-q)) / 10^k, where k is the larger of p and q.
1399+
int scale = Math.Max(twos, fives);
1400+
BigInteger significand = numerator * BigInteger.Pow(2, scale - twos) * BigInteger.Pow(5, scale - fives);
1401+
1402+
result = new PreciseNumber(exponent - scale, significand);
1403+
return true;
1404+
}
1405+
1406+
/// <summary>
1407+
/// Divides to a fixed number of significant digits, rounding the last of them half away from zero.
1408+
/// </summary>
1409+
/// <param name="numerator">The numerator, carrying the sign of the quotient.</param>
1410+
/// <param name="denominator">The denominator, which must be positive.</param>
1411+
/// <param name="exponent">The exponent the quotient's significand sits at.</param>
1412+
/// <param name="significantDigits">The number of significant digits to produce.</param>
1413+
/// <returns>The rounded quotient.</returns>
1414+
private static PreciseNumber DivideToPrecision(BigInteger numerator, BigInteger denominator, int exponent, int significantDigits)
1415+
{
1416+
// A quotient has either digits(numerator) - digits(denominator) digits or one more, so
1417+
// scaling by this much leaves at least one digit past the ones being kept: the digit the
1418+
// rounding decision is made on.
1419+
int scale = significantDigits + 1 - CountDigits(numerator) + CountDigits(denominator);
1420+
BigInteger scaled = scale > 0 ? numerator * Pow10(scale) : numerator;
1421+
int scaledExponent = exponent - Math.Max(scale, 0);
1422+
1423+
BigInteger quotient = scaled / denominator;
1424+
int excess = CountDigits(quotient) - significantDigits;
1425+
1426+
if (excess <= 0)
1427+
{
1428+
return new PreciseNumber(scaledExponent, quotient);
1429+
}
1430+
1431+
BigInteger divisor = Pow10(excess);
1432+
BigInteger kept = BigInteger.DivRem(quotient, divisor, out BigInteger dropped);
1433+
1434+
// Round half away from zero. Whatever the division above discarded is worth less than one
1435+
// unit of the dropped digits, so it can never carry the comparison across the halfway mark;
1436+
// at most it turns an exact tie into something above it, which rounds the same way.
1437+
if (BigInteger.Abs(dropped) * 2 >= divisor)
1438+
{
1439+
kept += quotient.Sign;
1440+
}
13161441

1317-
return new PreciseNumber(0, integerComponent) + fractionalComponent.ToPreciseNumber();
1442+
return new PreciseNumber(scaledExponent + excess, kept);
13181443
}
13191444

13201445
/// <summary>

‎README.md‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,9 +351,17 @@ You can control precision using:
351351

352352
- **ReduceSignificance()**: Reduces to a specific number of significant digits
353353

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+
354362
## Limitations
355363

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
357365

358366
- Conversion to standard types may throw `OverflowException` if the value is too large
359367

0 commit comments

Comments
 (0)