Adds nthroot methods#113
Conversation
Implements arbitrary-precision nth root with the full RoundingMode contract. Odd degrees accept negative inputs. - Add Calculator::nthRoot() with Newton-Raphson fallback and GmpCalculator override via gmp_root() - Add BigInteger::nthRoot(int $n, RoundingMode = Unnecessary) - Add BigDecimal::nthRoot(int $n, int $scale, RoundingMode = Unnecessary) - Add named exception constructors for the new failure modes - Extend random-tests.php to fuzz nthRoot across all three backends - Update CHANGELOG
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #113 +/- ##
============================================
+ Coverage 98.86% 98.98% +0.12%
- Complexity 709 755 +46
============================================
Files 20 20
Lines 1759 1872 +113
============================================
+ Hits 1739 1853 +114
+ Misses 20 19 -1 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
Hi @Pablo1Gustavo, thank you for your PR! Claude found a bug affecting This code returns I pushed a fix, do you want to review it? |
Test cleanup in
|
| Value | n | Existing location |
|---|---|---|
'16' |
3 | lines 3773–3776 |
'17' |
3 | lines 3779–3782 |
'-17' |
3 | lines 3814–3817 |
'8' |
4 | lines 3833–3836 |
'864' |
3 | lines 3898–3901 (boundary band) |
PHPUnit ran each scenario twice without adding coverage, so I removed the duplicated block.
New tests: tightest possible Half* boundary
Added V = 43 and V = 91 at n = 3 (and their negatives) to both BigIntegerTest and BigDecimalTest (scale 0). These are the smallest inputs where 2^n · |V| differs from (2·|t| + 1)^n by exactly ±1:
V = 43:8·43 = 344vs7³ = 343→ diff+1, smallest "round up" (∛43 ≈ 3.5034)V = 91:8·91 = 728vs9³ = 729→ diff-1, smallest "round down" (∛91 ≈ 4.4979)
The other Half* cases sit comfortably away from the threshold (e.g. 864 is 53 units inside the band). These two values pin the comparison to its most discriminative point — any off-by-one in the threshold computation would flip them.
|
Thanks @BenMorel. Makes sense and LGTM. |
Add
nthRoot()toBigIntegerandBigDecimalSummary
This PR adds
nthRoot()methods toBigIntegerandBigDecimal, providingarbitrary-precision nth root computation with the full
RoundingModecontractused everywhere else in the library. Odd degrees accept negative inputs.
Motivation
brick/mathalready exposessqrt()on bothBigIntegerandBigDecimal,but callers who need a cube root, fourth root, or any higher degree root have
to reach for ad-hoc Newton iterations, or drop down to native
floatarithmetic — losing the arbitrary precision that the library exists to
provide.
Real-world cases where this matters:
(
(final/initial)^(1/n) − 1), and volatility calculations commonly requirenon-square roots at precisions beyond IEEE-754
double.perfect kth power (e.g., Miller–Rabin preconditions, AKS primality) needs a
floor integer nth root.
inversions, bezier parameterizations, and physics-engine mass
distributions.
x^(1/n)wherexhasmore than 15 significant digits.
Today every one of these callers either rolls their own loop — a
well-known source of off-by-one bugs in the floor/ceiling boundary — or
accepts silent
floatrounding. Both are avoidable.API
BigInteger::nthRoot()Returns the integer nth root of
$this, rounded according to$roundingMode.$nmust be≥ 1.$thismay be negative only when$nis odd.RoundingMode::UnnecessarythrowsRoundingNecessaryExceptionif$thisis not a perfect nth power — matching the library-wide contractthat lossy operations require explicit opt-in.
BigDecimal::nthRoot()Returns the nth root rounded to
$scaledecimal places.$nas above.$scalemust be non-negative (as inBigDecimal::sqrt()anddividedBy()).RoundingNecessaryExceptionwith distinct messages for "notexact" vs "exact but scale too small", mirroring
sqrt().Signature parity with
sqrt()sqrt()nthRoot()BigIntegersqrt(RoundingMode = Unnecessary)nthRoot(int $n, RoundingMode = Unnecessary)BigDecimalsqrt(int $scale, RoundingMode = Unnecessary)nthRoot(int $n, int $scale, RoundingMode = Unnecessary)$nRoundingModesImplementation
Two-layer split (matches
sqrt())Calculator layer
Calculator::nthRoot()returns the truncated-toward-zero integer nth root.It has one backend-specific override:
GMP →
gmp_root(), with sign re-applied manually to freezetruncation-toward-zero semantics across PHP/GMP versions.
BCMath & Native → shared Newton-Raphson fallback:
Starting strictly above the true root guarantees monotone convergence
from above; the iterate stabilises when it can no longer decrease.
BigInteger::nthRoot()— roundingAfter obtaining the truncated root
rand computingr^n, the methodbranches on
$roundingMode:Down/Floor/Ceiling/Up— direct choice betweenrandthe next step one unit further from zero.
Half*— would normally require a midpoint comparison. But forconsecutive integers
randr±1, the sumr^n + (r±1)^nis alwaysodd, while
2·|value|is always even, so a midpoint tie isprovably impossible. All five
Half*modes therefore collapse to asingle comparison:
increment ⇔ 2·|value| > r^n + (r±1)^n.BigDecimal::nthRoot()— scale handlingGiven input scale
sand target scaleS:s ≡ 0 (mod n)(append up ton−1zeros).intermediateScale = max(S, s/n) + 1(one guard digit).n·intermediateScale − szeros.Up/Ceiling/Flooraway-from-zero direction, and — since the true value is irrational —
collapse all
Half*modes toHalfUp(no midpoint tie is possible).SviaDecimalHelper::scale().All scale arithmetic goes through
Safe::{add, sub, mul}so overflowsurfaces as
IntegerOverflowExceptionrather than silentfloatcorruption.
Testing
covering: identity cases (
n=1), zero and unit inputs at all degrees,perfect powers for
n ∈ {2, 3, 4, 5, 7, 100}, non-exact boundarycases exercising every
RoundingMode, negative inputs with oddn,fractional inputs (
inputScale % n ≠ 0), large scales (30), and everyexception path.
automatically fans out to the modes that are mathematically
equivalent for the given sign (e.g., positive-value
Up ≡ Ceiling),so no branch is left unexercised.
random-tests.phpnow fuzzesnthRootfork ∈ {2, 3, 4, 5, 7}on both positive and negativeoperands against all three backends, exiting on the first
mismatch.
Test plan
CALCULATOR=Native(13,469 tests pass)CALCULATOR=BCMath(13,469 tests pass)CALCULATOR=BCMathwithBCMATH_DEFAULT_SCALE=8vendor/bin/phpstan --no-progress— no errors at level 10Calculator::nthRootCALCULATOR=GMPon PHP 8.2–8.5 (64-bit and 32-bit)random-tests.phpfor ≥ several million iterations across all three backendstools/ecscoding-standard checkBackward compatibility
Purely additive: no existing API surface changes. Matches the
0.x.ybump convention documented inREADME.mdfor non-breakingadditions.