This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
ktsu.PreciseNumber is a high-precision numeric type for .NET that provides arbitrary precision arithmetic. It combines the scale benefits of scientific notation with the precision of BigInteger, storing values internally as significand × 10^exponent.
dotnet build # Build the solution
dotnet test # Run all tests
dotnet test --filter "FullyQualifiedName~TestName" # Run specific test
# Benchmarks (Release only; BenchmarkDotNet refuses to measure a debug build)
dotnet run -c Release --project PreciseNumber.Benchmarks # Pick from a list
dotnet run -c Release --project PreciseNumber.Benchmarks -- --filter '*Compar*' # One class
dotnet run -c Release --project PreciseNumber.Benchmarks -- --filter '*' --job short-
PreciseNumber (
PreciseNumber/PreciseNumber.cs): The main numeric type, areadonly partial record structimplementingINumber<PreciseNumber>. ItsdefaultisZero, whichPreciseNumberValueTypeTestspins. Stores values using:Significand: ABigIntegercontaining all significant digitsExponent: Anintdetermining the decimal placeSignificantDigits: Count of significant digits
-
Generic conversions (
PreciseNumber/PreciseNumber.Conversions.cs): TheTryConvertFrom*andTryConvertTo*members behindCreateChecked,CreateSaturating, andCreateTruncating, for every built-in numeric type andBigInteger.To<T>()uses them too -
PreciseNumberExtensions (
PreciseNumber/PreciseNumberExtensions.cs): Extension methods providingToPreciseNumber<T>()for converting anyINumber<T>to PreciseNumber
- Factory methods
CreateFromInteger<T>()andCreateFromFloatingPoint<T>()handle type-specific conversion logic - Addition, subtraction and modulus align exponents before calculating; multiplication and division work on the significands directly
Divideis exact when the quotient terminates, and otherwise rounds to a precision that never falls below the wider operand orMinimumDivisionPrecision- Roots (
PreciseNumber/PreciseNumber.Roots.cs, satisfyingIRootFunctions<PreciseNumber>) followDivide's precision rule and do not route throughdouble. Each scales the significand by a power of ten until the degree divides the exponent, then takes an integer Newton root of the significand, so an exact root stops on the exact answer rather than on a tolerance and no seed has to survive a value outsidedouble's range - Exponentials, logarithms and powers (
PreciseNumber/PreciseNumber.Exponentials.cs, satisfyingIExponentialFunctions,ILogarithmicFunctionsandIPowerFunctions) follow the same precision rule and do not route throughdoubleeither.ln(m · 10^k)isln m + k · ln 10against the storedLn10, with the mantissa centred on[1/√10, √10)and fed to the atanh series;exp(v)factors out10^round(v / ln 10)as an exponent shift and halves what is left before a Taylor sum. Nothing here is a free-standing decision:Exp10/Log10must not route through the natural log, because the exponent is the whole answer for a power of ten, and the…M1/…P1variants must not be computed asExp(x) - 1/Log(1 + x), because that cancels away the precision near zero they exist to keep - A fractional
Powisexp(y · ln x), carried wider by the integer digits ofy · ln xbecauseExp's range reduction consumes them. The integer path stays exponentiation by squaring and is exact; tests pin that exactness rather than a tolerance - Hyperbolics (
PreciseNumber/PreciseNumber.Hyperbolics.cs, satisfyingIHyperbolicFunctions) are the exponentials and logarithms under other names and add no transcendental machinery of their own. Which of them needs its textbook form rearranged is narrower than floating-point habit suggests, and the reason is worth keeping straight: addition, subtraction and multiplication here are exact, so cancelling two nearly equal values costs nothing by itself. Digits are lost only by cancelling against somethingExp,Log,SqrtorDividehas already rounded to a working width. That is whysinhsums its own series below one half instead of taking(e^x - e^-x)/2, and whyasinhandatanhsubtract their one analytically and go throughLogP1— each of those three otherwise returns about thirty correct digits from a fifty-digit type at1e-30, and the tests fail outright on them rather than drifting in the last place.cosh,tanhandacoshdo not need it:coshsums two positive terms, and the other two cancel only against operands nothing has rounded yet.tanhis still written as-t/(2 + t)witht = expm1(-2x), but for range rather than precision — the negative exponent decays instead of growing, so it saturates to±1wheree^2xwould overflow — andacoshstill factors the difference of squares, to keep a2n-digit intermediate out of the root. Don't simplify the first three back, and don't defend the last two on precision grounds AddandSubtractare exact, which is a trap when one operand sits millions of digits below the other: aligning them builds a significand that wide only for the caller to round it away. Where a sum is only ever going to be rounded (the±1inExpM1andLogP1, the reciprocal inSinhandCosh, the ones inAsinhandAcosh), useAddToPrecision/SubtractToPrecision, which collapse an operand wholly below the kept digits into one sticky unit that rounds the same way.ExpM1also returns-1outright oncee^xis below the working width, sinceExpof a very negative argument overflows its exponent- The
sanitizeconstructor parameter controls whether trailing zeros are removed (default: true) - Constants (
Zero,One,Pi,E,Tau) are pre-computed static instances ConstantPrecisionis a ceiling, not just a width.PiTo/ETo/Ln2To/Ln10Toserve at most that many digits and return the capped constant rather than failing, which is deliberate and pinned byTestConstantAccessorsReturnTheWholeConstantWhenAskedForMore— so a caller that needs what it asked for has to check, because the accessor will not. The circular functions are where this bites: reducing moduloπ/2spends one digit of π per integer digit of the argument, so only aboutConstantPrecision - integerDigitsare left for the answer.RequireReducibleArgumentenforces exactly that sum and nothing wider. Do not tighten it toreductionDigits: that pads the requirement withTrigonometricGuardDigitstwice over as margin, margin is allowed to be unavailable, and bounding it would refuseSin(1000000, 130)— a call whose every reported digit is correct. The half-turn family (SinPiand the rest) has no such ceiling, because it reduces before it multiplies, andLoghas none either, because it carries the magnitude in the decimal exponent rather than cancelling it against a constant- As a value type it can't be null or inherited. Don't add null checks for
PreciseNumberparameters, and don't reintroduceprotectedmembers - Conversions to integer types go through
BigInteger, so range checks, clamping, and wrapping follow its conventions. Conversions todouble,float,Half, anddecimalrender normalized scientific notation (d.ddd…E±n) and parse it, because the runtime parsers round correctly, with Clinger's fast path for small values. Keep one digit before the point. The .NET 7 and 8 parsers clamp an exponent above 1000 and still offset it by every digit ahead of the point, so a long significand rendered as an integer parses as zero there. Conversions fromdouble,float, andHalfuse the shortest text that round-trips ("R"). NaN and infinity coming in followBigIntegertoo
Tests use MSTest. PreciseNumber.Test/PreciseNumberTests.cs covers arithmetic, parsing, and formatting, PreciseNumberConversionTests.cs covers generic math conversion in every mode, PreciseNumberRootTests.cs pins the roots against published digits and against squaring back, PreciseNumberExponentialTests.cs does the same for the exponentials and logarithms and additionally pins the cases a double fallback cannot reach — fifty published digits of a fractional power, and ExpM1/LogP1 of 1e-30 not collapsing to zero — PreciseNumberHyperbolicTests.cs pins the hyperbolics against published digits and against cosh²x - sinh²x = 1, and separates the small-argument assertions that actually discriminate (sinh, asinh, atanh) from the two that read like they do and don't (tanh, acosh) — the class remark records which is which, so the distinction survives the next person to read it — PreciseNumberLargeArgumentTests.cs bounds the time of ExpM1/LogP1/hyperbolics on huge arguments and pins digits the exact sums produced, and PreciseNumberValueTypeTests.cs pins default as zero and asserts that small-value addition, subtraction, multiplication, and comparison allocate nothing. The test project targets only .NET 10.0 while the main library multi-targets net7.0, net8.0, net9.0, and net10.0.
PreciseNumber.Benchmarks is a BenchmarkDotNet suite, one class per area (construction,
comparison, arithmetic, pow, roots, rounding, text, conversion). The library exposes its internals to it
so construction can be measured directly.
Most classes are parameterised by Digits (8, 30, 200). That axis is the point: digits live in a
BigInteger, so anything that touches them one at a time looks fine at 8 digits and collapses at
200. Read results across the Digits column, not down one value of it.
Allocation is reported alongside time and matters just as much. The number is a value type, so the
only allocations are BigInteger digit arrays, and avoiding an intermediate shows up in Allocated
before it shows up in Mean. Comparison, addition, subtraction, and multiplication should allocate
nothing when the operands and every intermediate and final significand fit in an int. Exponent
alignment counts, so 1 + 0.0000000001 allocates because it scales 1 by 10^10, and 99999 * 99999
allocates because its product is 9,999,800,001.
Run the relevant benchmarks before and after any change to the library's internals. Compare two
refs in one place rather than across two runs: the Benchmarks workflow's baseline input
measures a given ref and this checkout on the same runner, and locally the same shape is a
worktree plus one --artifacts directory per ref, run sequentially. Numbers from two separate
runs are not comparable — a cloud runner in particular can change CPU generation between them.
See PreciseNumber.Benchmarks/README.md for details.