From 1019ed7bcf6bd3f0b5c8049fe0eb62f8b918faf6 Mon Sep 17 00:00:00 2001 From: nickzerjeski Date: Mon, 13 Apr 2026 11:00:22 +0200 Subject: [PATCH 1/4] feat: add repunit theorem helpers Fixes: #1866 --- Maths/RepunitTheorem.js | 78 +++++++++++++++++++++++++++++++ Maths/test/RepunitTheorem.test.js | 34 ++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 Maths/RepunitTheorem.js create mode 100644 Maths/test/RepunitTheorem.test.js diff --git a/Maths/RepunitTheorem.js b/Maths/RepunitTheorem.js new file mode 100644 index 0000000000..5d228f1089 --- /dev/null +++ b/Maths/RepunitTheorem.js @@ -0,0 +1,78 @@ +/** + * Repunit theorem helpers. + * + * A repunit of length n is: + * R_n = (10^n - 1) / 9 + * + * For a prime p (p != 2, 5), p divides R_n iff ord_p(10) divides n. + * Reference: https://en.wikipedia.org/wiki/Repunit + */ + +const gcd = (a, b) => { + let x = BigInt(a) + let y = BigInt(b) + while (y !== 0n) { + ;[x, y] = [y, x % y] + } + return x < 0n ? -x : x +} + +const modPow = (base, exp, mod) => { + let result = 1n + let b = BigInt(base) % BigInt(mod) + let e = BigInt(exp) + const m = BigInt(mod) + + while (e > 0n) { + if (e & 1n) result = (result * b) % m + b = (b * b) % m + e >>= 1n + } + + return result +} + +const multiplicativeOrder10 = (prime) => { + const p = BigInt(prime) + if (p <= 1n) throw new RangeError('prime must be > 1') + if (gcd(10n, p) !== 1n) throw new RangeError('10 and prime must be coprime') + + // For prime p, ord_p(10) divides p-1. + const upper = p - 1n + for (let k = 1n; k <= upper; k++) { + if (upper % k === 0n && modPow(10n, k, p) === 1n) { + return k + } + } + + throw new Error('multiplicative order not found') +} + +const repunitMod = (length, mod) => { + if (!Number.isInteger(length) || length < 1) { + throw new RangeError('length must be a positive integer') + } + const m = BigInt(mod) + if (m <= 0n) throw new RangeError('mod must be > 0') + + let remainder = 0n + for (let i = 0; i < length; i++) { + remainder = (remainder * 10n + 1n) % m + } + return remainder +} + +const isRepunitDivisibleByPrime = (length, prime) => { + if (!Number.isInteger(length) || length < 1) { + throw new RangeError('length must be a positive integer') + } + + const p = BigInt(prime) + if (p === 2n || p === 5n) return false + if (gcd(10n, p) !== 1n) return false + + const order = multiplicativeOrder10(p) + return BigInt(length) % order === 0n +} + +export { multiplicativeOrder10, repunitMod, isRepunitDivisibleByPrime } diff --git a/Maths/test/RepunitTheorem.test.js b/Maths/test/RepunitTheorem.test.js new file mode 100644 index 0000000000..7c3a70ff30 --- /dev/null +++ b/Maths/test/RepunitTheorem.test.js @@ -0,0 +1,34 @@ +import { + isRepunitDivisibleByPrime, + multiplicativeOrder10, + repunitMod +} from '../RepunitTheorem' + +describe('RepunitTheorem', () => { + it('computes multiplicative order examples', () => { + expect(multiplicativeOrder10(11n)).toBe(2n) + expect(multiplicativeOrder10(37n)).toBe(3n) + expect(multiplicativeOrder10(7n)).toBe(6n) + }) + + it('checks repunit divisibility using the theorem', () => { + // 111111 is divisible by 3, 7, 11, 13, 37 + expect(isRepunitDivisibleByPrime(6, 3n)).toBe(true) + expect(isRepunitDivisibleByPrime(6, 7n)).toBe(true) + expect(isRepunitDivisibleByPrime(6, 11n)).toBe(true) + expect(isRepunitDivisibleByPrime(6, 13n)).toBe(true) + expect(isRepunitDivisibleByPrime(6, 37n)).toBe(true) + }) + + it('returns false when divisibility condition does not hold', () => { + expect(isRepunitDivisibleByPrime(6, 19n)).toBe(false) + expect(isRepunitDivisibleByPrime(9, 2n)).toBe(false) + expect(isRepunitDivisibleByPrime(9, 5n)).toBe(false) + }) + + it('computes repunit modulo without building huge integers', () => { + expect(repunitMod(6, 37n)).toBe(0n) + expect(repunitMod(6, 11n)).toBe(0n) + expect(repunitMod(7, 13n)).toBe(1n) + }) +}) From 84b352f71f21f19f13095d4a1528c080d096eb4b Mon Sep 17 00:00:00 2001 From: nickzerjeski Date: Mon, 13 Apr 2026 11:12:15 +0200 Subject: [PATCH 2/4] test: cover repunit theorem validation branches --- Maths/test/RepunitTheorem.test.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Maths/test/RepunitTheorem.test.js b/Maths/test/RepunitTheorem.test.js index 7c3a70ff30..163c292cfd 100644 --- a/Maths/test/RepunitTheorem.test.js +++ b/Maths/test/RepunitTheorem.test.js @@ -31,4 +31,18 @@ describe('RepunitTheorem', () => { expect(repunitMod(6, 11n)).toBe(0n) expect(repunitMod(7, 13n)).toBe(1n) }) + + it('validates multiplicative order input', () => { + expect(() => multiplicativeOrder10(1n)).toThrow(RangeError) + expect(() => multiplicativeOrder10(10n)).toThrow(RangeError) + }) + + it('validates repunitMod input', () => { + expect(() => repunitMod(0, 7n)).toThrow(RangeError) + expect(() => repunitMod(5, 0n)).toThrow(RangeError) + }) + + it('validates repunit divisibility input length', () => { + expect(() => isRepunitDivisibleByPrime(0, 7n)).toThrow(RangeError) + }) }) From 140bffd025f0ed098fefcca219b125179ede7359 Mon Sep 17 00:00:00 2001 From: nickzerjeski Date: Mon, 13 Apr 2026 09:12:48 +0000 Subject: [PATCH 3/4] Updated Documentation in README.md --- DIRECTORY.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/DIRECTORY.md b/DIRECTORY.md index b7b8aca45d..d128f91650 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -249,6 +249,7 @@ * [PrimeFactors](Maths/PrimeFactors.js) * [QuadraticRoots](Maths/QuadraticRoots.js) * [RadianToDegree](Maths/RadianToDegree.js) + * [RepunitTheorem](Maths/RepunitTheorem.js) * [ReverseNumber](Maths/ReverseNumber.js) * [ReversePolishNotation](Maths/ReversePolishNotation.js) * [RowEchelon](Maths/RowEchelon.js) @@ -322,8 +323,8 @@ * [TernarySearch](Search/TernarySearch.js) * [UnionFind](Search/UnionFind.js) * **Sliding-Windows** - * [MaxSumSubarrayFixed](Sliding-Windows/MaxSumSubarrayFixed.js) * [LongestSubarrayWithSumAtMost](Sliding-Windows/LongestSubarrayWithSumAtMost.js) + * [MaxSumSubarrayFixed](Sliding-Windows/MaxSumSubarrayFixed.js) * **Sorts** * [AlphaNumericalSort](Sorts/AlphaNumericalSort.js) * [BeadSort](Sorts/BeadSort.js) From 45aa3f444621bb832e2b47f1c8d9a392eaf75a35 Mon Sep 17 00:00:00 2001 From: nickzerjeski Date: Mon, 13 Apr 2026 12:17:22 +0200 Subject: [PATCH 4/4] Fix repunit divisibility edge case for prime 3 --- Maths/RepunitTheorem.js | 3 ++- Maths/test/RepunitTheorem.test.js | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Maths/RepunitTheorem.js b/Maths/RepunitTheorem.js index 5d228f1089..23c1e5d279 100644 --- a/Maths/RepunitTheorem.js +++ b/Maths/RepunitTheorem.js @@ -4,7 +4,7 @@ * A repunit of length n is: * R_n = (10^n - 1) / 9 * - * For a prime p (p != 2, 5), p divides R_n iff ord_p(10) divides n. + * For a prime p (p != 2, 3, 5), p divides R_n iff ord_p(10) divides n. * Reference: https://en.wikipedia.org/wiki/Repunit */ @@ -69,6 +69,7 @@ const isRepunitDivisibleByPrime = (length, prime) => { const p = BigInt(prime) if (p === 2n || p === 5n) return false + if (p === 3n) return BigInt(length) % 3n === 0n if (gcd(10n, p) !== 1n) return false const order = multiplicativeOrder10(p) diff --git a/Maths/test/RepunitTheorem.test.js b/Maths/test/RepunitTheorem.test.js index 163c292cfd..ce886d3507 100644 --- a/Maths/test/RepunitTheorem.test.js +++ b/Maths/test/RepunitTheorem.test.js @@ -21,6 +21,8 @@ describe('RepunitTheorem', () => { }) it('returns false when divisibility condition does not hold', () => { + expect(isRepunitDivisibleByPrime(1, 3n)).toBe(false) + expect(isRepunitDivisibleByPrime(3, 3n)).toBe(true) expect(isRepunitDivisibleByPrime(6, 19n)).toBe(false) expect(isRepunitDivisibleByPrime(9, 2n)).toBe(false) expect(isRepunitDivisibleByPrime(9, 5n)).toBe(false)