Skip to content

Commit

Permalink
Implement Farey sequence generation
Browse files Browse the repository at this point in the history
Clarify the type of k-combination iterators.
  • Loading branch information
frostburn committed Feb 28, 2024
1 parent cc8e8f0 commit b55d1fc
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 2 deletions.
90 changes: 90 additions & 0 deletions src/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {describe, it, expect} from 'vitest';
import {
Fraction,
FractionSet,
arraysEqual,
binomial,
ceilPow2,
Expand All @@ -9,6 +11,8 @@ import {
div,
dot,
extendedEuclid,
fareyInterior,
fareySequence,
gcd,
iteratedEuclid,
norm,
Expand Down Expand Up @@ -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);
});
});
9 changes: 7 additions & 2 deletions src/combinations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,10 @@ export function combinations<T>(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<T>(set: T[], k: number): Generator<T[]> {
export function* iterKCombinations<T>(
set: T[],
k: number
): Generator<T[], number, undefined> {
// There is no way to take e.g. sets of 5 elements from
// a set of 4.
if (k > set.length || k <= 0) {
Expand Down Expand Up @@ -179,7 +182,9 @@ export function* iterKCombinations<T>(set: T[], k: number): Generator<T[]> {
* @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<T>(set: T[]): Generator<T[]> {
export function* iterCombinations<T>(
set: T[]
): Generator<T[], number, undefined> {
// Calculate all non-empty k-combinations
let total = 0;
for (let k = 1; k <= set.length; k++) {
Expand Down
43 changes: 43 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Fraction, undefined, undefined> {
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<Fraction, undefined, undefined> {
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);
}
}

0 comments on commit b55d1fc

Please sign in to comment.