diff --git a/src/__tests__/index.spec.ts b/src/__tests__/index.spec.ts index 05c3407..fe0ef09 100644 --- a/src/__tests__/index.spec.ts +++ b/src/__tests__/index.spec.ts @@ -1,5 +1,7 @@ import {describe, it, expect} from 'vitest'; import { + Fraction, + FractionSet, arraysEqual, binomial, ceilPow2, @@ -9,6 +11,8 @@ import { div, dot, extendedEuclid, + fareyInterior, + fareySequence, gcd, iteratedEuclid, norm, @@ -179,3 +183,89 @@ describe('Ceiling power of two', () => { expect(Math.log2(p2)).toBeCloseTo(Math.round(Math.log2(p2))); }); }); + +describe('Farey sequence generator', () => { + it('generates all fractions with max denominator 6 between 0 and 1 inclusive', () => { + const F6 = Array.from(fareySequence(6)).map(f => f.toFraction()); + expect(F6).toEqual([ + '0', + '1/6', + '1/5', + '1/4', + '1/3', + '2/5', + '1/2', + '3/5', + '2/3', + '3/4', + '4/5', + '5/6', + '1', + ]); + }); + + it('agrees with the brute force method', () => { + const everything = new FractionSet(); + const N = Math.floor(Math.random() * 50) + 1; + for (let d = 1; d <= N; ++d) { + for (let n = 0; n <= d; ++n) { + everything.add(new Fraction(n, d)); + } + } + const brute = Array.from(everything); + brute.sort((a, b) => a.compare(b)); + const farey = fareySequence(N); + for (const entry of brute) { + const f = farey.next().value!; + expect(entry.equals(f)).toBe(true); + } + expect(farey.next().done).toBe(true); + }); +}); + +describe('Farey interior generator', () => { + it('generates all fractions with max denominator 8 between 0 and 1 exclusive', () => { + const Fi8 = Array.from(fareyInterior(8)).map(f => f.toFraction()); + expect(Fi8).toEqual([ + '1/8', + '1/7', + '1/6', + '1/5', + '1/4', + '2/7', + '1/3', + '3/8', + '2/5', + '3/7', + '1/2', + '4/7', + '3/5', + '5/8', + '2/3', + '5/7', + '3/4', + '4/5', + '5/6', + '6/7', + '7/8', + ]); + }); + + it('agrees with the brute force method', () => { + const everything = new FractionSet(); + const N = Math.floor(Math.random() * 50) + 1; + for (let d = 1; d <= N; ++d) { + for (let n = 1; n < d; ++n) { + everything.add(new Fraction(n, d)); + } + } + const brute = Array.from(everything); + brute.sort((a, b) => a.compare(b)); + const farey = fareyInterior(N); + for (const entry of brute) { + const f = farey.next().value!; + expect(entry.equals(f)).toBe(true); + } + expect(farey.next().done).toBe(true); + }); +}); diff --git a/src/combinations.ts b/src/combinations.ts index f03b4a6..df79f9e 100644 --- a/src/combinations.ts +++ b/src/combinations.ts @@ -137,7 +137,10 @@ export function combinations(set: T[]): T[][] { * @param k Size of combinations to search for. * @returns Generator of found combinations, size of a combination is k. */ -export function* iterKCombinations(set: T[], k: number): Generator { +export function* iterKCombinations( + set: T[], + k: number +): Generator { // There is no way to take e.g. sets of 5 elements from // a set of 4. if (k > set.length || k <= 0) { @@ -179,7 +182,9 @@ export function* iterKCombinations(set: T[], k: number): Generator { * @param set Array of objects of any type. They are treated as unique. * @returns Generator of arrays representing all possible non-empty combinations of elements in a set. */ -export function* iterCombinations(set: T[]): Generator { +export function* iterCombinations( + set: T[] +): Generator { // Calculate all non-empty k-combinations let total = 0; for (let k = 1; k <= set.length; k++) { diff --git a/src/index.ts b/src/index.ts index 93a7849..8cd6684 100644 --- a/src/index.ts +++ b/src/index.ts @@ -299,3 +299,46 @@ export function ceilPow2(x: number) { } return 2 ** Math.ceil(Math.log2(x)); } + +/** + * Create an iterator over the n'th Farey sequence. (All fractions between 0 and 1 inclusive.) + * @param maxDenominator Maximum denominator in the sequence. + * @yields Fractions in ascending order starting from 0/1 and ending at 1/1. + */ +export function* fareySequence( + maxDenominator: number +): Generator { + let a = 0; + let b = 1; + let c = 1; + let d = maxDenominator; + yield new Fraction(a, b); + while (0 <= c && c <= maxDenominator) { + const k = Math.floor((maxDenominator + b) / d); + [a, b, c, d] = [c, d, k * c - a, k * d - b]; + yield new Fraction(a, b); + } +} + +/** + * Create an iterator over the interior of n'th Farey sequence. (All fractions between 0 and 1 exclusive.) + * @param maxDenominator Maximum denominator in the sequence. + * @yields Fractions in ascending order starting from 1/maxDenominator and ending at (maxDenominator-1)/maxDenominator. + */ +export function* fareyInterior( + maxDenominator: number +): Generator { + if (maxDenominator < 2) { + return; + } + let a = 1; + let b = maxDenominator; + let c = 1; + let d = maxDenominator - 1; + yield new Fraction(a, b); + while (d > 1) { + const k = Math.floor((maxDenominator + b) / d); + [a, b, c, d] = [c, d, k * c - a, k * d - b]; + yield new Fraction(a, b); + } +}