From 2068ec881e1402d55d187a15cad272e84729ddac Mon Sep 17 00:00:00 2001 From: Felipe Fernandes Date: Mon, 3 Aug 2026 18:56:42 -0300 Subject: [PATCH 1/2] feat(Maths): add Tonelli-Shanks modular square root MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements Tonelli–Shanks for odd prime moduli with Legendre check, p≡3 (mod 4) fast path, and Vitest coverage. Signed-off-by: Felipe Fernandes --- Maths/TonelliShanks.js | 108 +++++++++++++++++++++++++++++++ Maths/test/TonelliShanks.test.js | 25 +++++++ 2 files changed, 133 insertions(+) create mode 100644 Maths/TonelliShanks.js create mode 100644 Maths/test/TonelliShanks.test.js diff --git a/Maths/TonelliShanks.js b/Maths/TonelliShanks.js new file mode 100644 index 0000000000..37efc2b5c8 --- /dev/null +++ b/Maths/TonelliShanks.js @@ -0,0 +1,108 @@ +/** + * Tonelli–Shanks algorithm for modular square roots modulo an odd prime. + * https://en.wikipedia.org/wiki/Tonelli%E2%80%93Shanks_algorithm + * + * Returns the smaller non-negative root r such that r^2 ≡ n (mod p). + * Throws RangeError when n is not a quadratic residue or p is invalid. + */ + +/** + * @param {number} a + * @param {number} p odd prime + * @returns {number} Legendre symbol (a/p) in {-1, 0, 1} + */ +function legendreSymbol(a, p) { + const exp = (p - 1) / 2 + let result = 1 + a = ((a % p) + p) % p + let base = a + let e = exp + while (e > 0) { + if (e % 2 === 1) result = (result * base) % p + base = (base * base) % p + e = Math.floor(e / 2) + } + if (result === p - 1) return -1 + return result +} + +/** + * @param {number} n integer + * @param {number} p odd prime modulus + * @returns {number} smaller non-negative modular square root + */ +export function tonelliShanks(n, p) { + if ( + typeof n !== 'number' || + typeof p !== 'number' || + !Number.isInteger(n) || + !Number.isInteger(p) + ) { + throw new TypeError('Arguments must be integers') + } + if (p <= 2 || p % 2 === 0) { + throw new RangeError('p must be an odd prime') + } + + n = ((n % p) + p) % p + if (n === 0) return 0 + + const ls = legendreSymbol(n, p) + if (ls !== 1) { + throw new RangeError('n is not a quadratic residue modulo p') + } + + const modPow = (base, exp, mod) => { + let result = 1 + base = ((base % mod) + mod) % mod + while (exp > 0) { + if (exp % 2 === 1) result = (result * base) % mod + base = (base * base) % mod + exp = Math.floor(exp / 2) + } + return result + } + + // Fast path: p ≡ 3 (mod 4) + if (p % 4 === 3) { + const r = modPow(n, (p + 1) / 4, p) + return Math.min(r, p - r) + } + + // Write p - 1 = q * 2^s with q odd + let q = p - 1 + let s = 0 + while (q % 2 === 0) { + q /= 2 + s += 1 + } + + // Find a quadratic non-residue z + let z = 2 + while (legendreSymbol(z, p) !== -1) { + z += 1 + if (z >= p) throw new RangeError('failed to find quadratic non-residue') + } + + let m = s + let c = modPow(z, q, p) + let r = modPow(n, (q + 1) / 2, p) + let t = modPow(n, q, p) + + while (t !== 1) { + let i = 1 + let t2i = (t * t) % p + while (t2i !== 1) { + t2i = (t2i * t2i) % p + i += 1 + if (i === m) throw new RangeError('tonelli-shanks failed') + } + const b = modPow(c, 2 ** (m - i - 1), p) + r = (r * b) % p + c = (b * b) % p + t = (t * c) % p + m = i + } + + return Math.min(r, p - r) +} diff --git a/Maths/test/TonelliShanks.test.js b/Maths/test/TonelliShanks.test.js new file mode 100644 index 0000000000..3885467b30 --- /dev/null +++ b/Maths/test/TonelliShanks.test.js @@ -0,0 +1,25 @@ +import { tonelliShanks } from '../TonelliShanks' + +describe('tonelliShanks', () => { + it.each([ + [2, 7, 3], // 3^2 = 9 ≡ 2 (mod 7) + [10, 13, 6], // 6^2 = 36 ≡ 10 (mod 13) + [0, 11, 0], + [5, 11, 4] // 4^2 = 16 ≡ 5 (mod 11); p ≡ 3 (mod 4) + ])('returns smaller root for %i mod %i', (n, p, expected) => { + expect(tonelliShanks(n, p)).toBe(expected) + }) + + it('throws for non-residues', () => { + expect(() => tonelliShanks(3, 7)).toThrow(RangeError) + }) + + it('throws for invalid modulus', () => { + expect(() => tonelliShanks(2, 2)).toThrow(RangeError) + expect(() => tonelliShanks(2, 4)).toThrow(RangeError) + }) + + it('throws for non-integer inputs', () => { + expect(() => tonelliShanks('2', 7)).toThrow(TypeError) + }) +}) From 200108ae019e59ffe9aa19e3ef3e5a31341cf9a7 Mon Sep 17 00:00:00 2001 From: Felipe Fernandes Date: Mon, 3 Aug 2026 18:57:32 -0300 Subject: [PATCH 2/2] docs: list TonelliShanks in DIRECTORY.md --- DIRECTORY.md | 1 + 1 file changed, 1 insertion(+) diff --git a/DIRECTORY.md b/DIRECTORY.md index b7b8aca45d..d4b037abed 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -259,6 +259,7 @@ * [Softmax](Maths/Softmax.js) * [SquareRoot](Maths/SquareRoot.js) * [SquareRootLogarithmic](Maths/SquareRootLogarithmic.js) + * [TonelliShanks](Maths/TonelliShanks.js) * [SumOfDigits](Maths/SumOfDigits.js) * [SumOfGeometricProgression](Maths/SumOfGeometricProgression.js) * [TwoSum](Maths/TwoSum.js)