Skip to content

Adds nthroot methods#113

Open
Pablo1Gustavo wants to merge 4 commits intobrick:mainfrom
Pablo1Gustavo:nthroot
Open

Adds nthroot methods#113
Pablo1Gustavo wants to merge 4 commits intobrick:mainfrom
Pablo1Gustavo:nthroot

Conversation

@Pablo1Gustavo
Copy link
Copy Markdown

@Pablo1Gustavo Pablo1Gustavo commented Apr 20, 2026

Add nthRoot() to BigInteger and BigDecimal

Summary

This PR adds nthRoot() methods to BigInteger and BigDecimal, providing
arbitrary-precision nth root computation with the full RoundingMode contract
used everywhere else in the library. Odd degrees accept negative inputs.

BigInteger::of('1267650600228229401496703205376')->nthRoot(100);
// => BigInteger('2')   — exact: 2^100

BigDecimal::of(2)->nthRoot(3, 20, RoundingMode::HalfUp);
// => BigDecimal('1.25992104989487316477')   — cube root of 2

BigInteger::of(-27)->nthRoot(3);
// => BigInteger('-3')   — negative input, odd degree

Motivation

brick/math already exposes sqrt() on both BigInteger and BigDecimal,
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 float
arithmetic — losing the arbitrary precision that the library exists to
provide.

Real-world cases where this matters:

  • Financial & statistical workloads — geometric means, CAGR
    ((final/initial)^(1/n) − 1), and volatility calculations commonly require
    non-square roots at precisions beyond IEEE-754 double.
  • Cryptography & number theory — checking whether a large integer is a
    perfect kth power (e.g., Miller–Rabin preconditions, AKS primality) needs a
    floor integer nth root.
  • Geometric modelling — cube roots and fifth roots show up in volume
    inversions, bezier parameterizations, and physics-engine mass
    distributions.
  • Scientific computing — any formula involving x^(1/n) where x has
    more 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 float rounding. Both are avoidable.

API

BigInteger::nthRoot()

public function nthRoot(
    int $n,
    RoundingMode $roundingMode = RoundingMode::Unnecessary,
): BigInteger;

Returns the integer nth root of $this, rounded according to
$roundingMode.

  • $n must be ≥ 1.
  • $this may be negative only when $n is odd.
  • Default RoundingMode::Unnecessary throws RoundingNecessaryException if
    $this is not a perfect nth power — matching the library-wide contract
    that lossy operations require explicit opt-in.

BigDecimal::nthRoot()

public function nthRoot(
    int $n,
    int $scale,
    RoundingMode $roundingMode = RoundingMode::Unnecessary,
): BigDecimal;

Returns the nth root rounded to $scale decimal places.

  • Same constraints on $n as above.
  • $scale must be non-negative (as in BigDecimal::sqrt() and
    dividedBy()).
  • Throws RoundingNecessaryException with distinct messages for "not
    exact" vs "exact but scale too small", mirroring sqrt().

Signature parity with sqrt()

sqrt() nthRoot()
BigInteger sqrt(RoundingMode = Unnecessary) nthRoot(int $n, RoundingMode = Unnecessary)
BigDecimal sqrt(int $scale, RoundingMode = Unnecessary) nthRoot(int $n, int $scale, RoundingMode = Unnecessary)
Negative input rejected rejected only for even $n
All 10 RoundingModes

Implementation

Two-layer split (matches sqrt())

┌────────────────────────────────────────────────────────────────┐
│  Public layer                                                  │
│  ├─ BigInteger::nthRoot(int $n, RoundingMode)                  │
│  │     · signs, identity fast-paths, rounding-mode logic       │
│  └─ BigDecimal::nthRoot(int $n, int $scale, RoundingMode)      │
│        · scale padding, one-extra-digit precision, rescale     │
│                         │                                      │
│                         ▼                                      │
│  Internal layer                                                │
│  └─ Calculator::nthRoot(string $n, int $k) → truncated root    │
│         ├─ GmpCalculator override → gmp_root()                 │
│         └─ shared fallback → Newton-Raphson                    │
└────────────────────────────────────────────────────────────────┘

Calculator layer

Calculator::nthRoot() returns the truncated-toward-zero integer nth root.
It has one backend-specific override:

  • GMPgmp_root(), with sign re-applied manually to freeze
    truncation-toward-zero semantics across PHP/GMP versions.

  • BCMath & Native → shared Newton-Raphson fallback:

    x₀ = 10^(⌈digits(m)/k⌉)                                (overshoot)
    xᵢ₊₁ = ⌊((k−1)·xᵢ + ⌊m/xᵢ^(k−1)⌋) / k⌋                (recurrence)
    stop when xᵢ₊₁ ≥ xᵢ                                    (terminate)
    

    Starting strictly above the true root guarantees monotone convergence
    from above; the iterate stabilises when it can no longer decrease.

BigInteger::nthRoot() — rounding

After obtaining the truncated root r and computing r^n, the method
branches on $roundingMode:

  • Down / Floor / Ceiling / Up — direct choice between r and
    the next step one unit further from zero.
  • Half* — would normally require a midpoint comparison. But for
    consecutive integers r and r±1, the sum r^n + (r±1)^n is always
    odd
    , while 2·|value| is always even, so a midpoint tie is
    provably impossible. All five Half* modes therefore collapse to a
    single comparison: increment ⇔ 2·|value| > r^n + (r±1)^n.

BigDecimal::nthRoot() — scale handling

Given input scale s and target scale S:

  1. Pad the value so s ≡ 0 (mod n) (append up to n−1 zeros).
  2. Choose intermediateScale = max(S, s/n) + 1 (one guard digit).
  3. Left-shift the value by n·intermediateScale − s zeros.
  4. Take the integer nth root at the intermediate scale.
  5. If the root isn't exact, pre-adjust for Up / Ceiling / Floor away-
    from-zero direction, and — since the true value is irrational —
    collapse all Half* modes to HalfUp (no midpoint tie is possible).
  6. Rescale to S via DecimalHelper::scale().

All scale arithmetic goes through Safe::{add, sub, mul} so overflow
surfaces as IntegerOverflowException rather than silent float
corruption.

Testing

  • 551 new PHPUnit assertions spread across 8 new test methods
    covering: identity cases (n=1), zero and unit inputs at all degrees,
    perfect powers for n ∈ {2, 3, 4, 5, 7, 100}, non-exact boundary
    cases exercising every RoundingMode, negative inputs with odd n,
    fractional inputs (inputScale % n ≠ 0), large scales (30), and every
    exception path.
  • Rounding-mode equivalence expansion — each provider row
    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.
  • Cross-backend parity fuzzrandom-tests.php now fuzzes
    nthRoot for k ∈ {2, 3, 4, 5, 7} on both positive and negative
    operands against all three backends, exiting on the first
    mismatch.
  • PHPStan level 10 clean.

Test plan

  • Full PHPUnit suite on CALCULATOR=Native (13,469 tests pass)
  • Full PHPUnit suite on CALCULATOR=BCMath (13,469 tests pass)
  • Full PHPUnit suite on CALCULATOR=BCMath with BCMATH_DEFAULT_SCALE=8
  • vendor/bin/phpstan --no-progress — no errors at level 10
  • BCMath ↔ Native parity fuzz on Calculator::nthRoot
  • CI matrix: verify CALCULATOR=GMP on PHP 8.2–8.5 (64-bit and 32-bit)
  • CI: random-tests.php for ≥ several million iterations across all three backends
  • CI: tools/ecs coding-standard check

Backward compatibility

Purely additive: no existing API surface changes. Matches the
0.x.y bump convention documented in README.md for non-breaking
additions.

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
Copy link
Copy Markdown

codecov Bot commented Apr 20, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.98%. Comparing base (6aef71a) to head (7cde0a6).

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     1870     +111     
============================================
+ Hits           1739     1851     +112     
+ Misses           20       19       -1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant