diff --git a/README.md b/README.md index d9d1acb4..7a19d64f 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Parses and compiles CSS nth-checks to highly optimized functions. ### About -This module can be used to parse & compile nth-checks, as they are found in CSS 3's `nth-child()` and `nth-last-of-type()`. +This module can be used to parse & compile nth-checks, as they are found in CSS 3's `nth-child()` and `nth-last-of-type()`. It can be used to check if a given index matches a given nth-rule, or to generate a sequence of indices matching a given nth-rule. `nth-check` focusses on speed, providing optimized functions for different kinds of nth-child formulas, while still following the [spec](http://www.w3.org/TR/css3-selectors/#nth-child-pseudo). @@ -64,6 +64,62 @@ check(5); // `false` check(6); // `true` ``` +##### `generate([a, b])` + +Returns a function that produces a monotonously increasing sequence of indices. + +If the sequence has an end, the returned function will return `null` after the last index in the sequence. + +**Example:** An always increasing sequence + +```js +const gen = nthCheck.generate([2, 3]); + +gen(); // `1` +gen(); // `3` +gen(); // `5` +gen(); // `8` +gen(); // `11` +``` + +**Example:** With an end value + +```js +const gen = nthCheck.generate([-2, 5]); + +gen(); // 0 +gen(); // 2 +gen(); // 4 +gen(); // null +``` + +##### `sequence(formula)` + +Parses and compiles a formula to a generator that produces a sequence of indices. Combination of `parse` and `generate`. + +**Example:** An always increasing sequence + +```js +const gen = nthCheck.sequence("2n+3"); + +gen(); // `1` +gen(); // `3` +gen(); // `5` +gen(); // `8` +gen(); // `11` +``` + +**Example:** With an end value + +```js +const gen = nthCheck.sequence("-2n+5"); + +gen(); // 0 +gen(); // 2 +gen(); // 4 +gen(); // null +``` + --- License: BSD-2-Clause diff --git a/src/__fixtures__/rules.ts b/src/__fixtures__/rules.ts index d96c731f..f7ff3088 100644 --- a/src/__fixtures__/rules.ts +++ b/src/__fixtures__/rules.ts @@ -33,6 +33,7 @@ export const valid: [string, [number, number]][] = [ // Surprisingly, neither sizzle, qwery or nwmatcher cover these cases ["-4n+13", [-4, 13]], ["-2n + 12", [-2, 12]], + ["-n", [-1, 0]], ]; export const invalid = [ diff --git a/src/compile.spec.ts b/src/compile.spec.ts index c689a6ff..722e153b 100644 --- a/src/compile.spec.ts +++ b/src/compile.spec.ts @@ -1,4 +1,4 @@ -import nthCheck, { compile } from "."; +import nthCheck, { compile, generate, sequence } from "."; import { valid } from "./__fixtures__/rules"; const valArray = new Array(...Array(2e3)).map((_, i) => i); @@ -37,3 +37,59 @@ describe("parse", () => { } }); }); + +describe("generate", () => { + it("should return a function", () => { + expect(generate([1, 2])).toBeInstanceOf(Function); + }); + + it("should only return valid values", () => { + for (const [_, parsed] of valid) { + const gen = generate(parsed); + const check = compile(parsed); + let val = gen(); + + for (let i = 0; i < 1e3; i++) { + // Should pass the check iff `i` is the next value. + expect(val === i).toBe(check(i)); + + if (val === i) { + val = gen(); + } + } + } + }); + + it("should produce an increasing sequence", () => { + const gen = generate([2, 2]); + + expect(gen()).toBe(1); + expect(gen()).toBe(3); + expect(gen()).toBe(5); + expect(gen()).toBe(7); + expect(gen()).toBe(9); + }); + + it("should produce an increasing sequence for a negative `n`", () => { + const gen = generate([-1, 2]); + + expect(gen()).toBe(0); + expect(gen()).toBe(1); + expect(gen()).toBe(null); + }); + + it("should not produce any values for `-n`", () => { + const gen = generate([-1, 0]); + + expect(gen()).toBe(null); + }); + + it("should parse selectors with `sequence`", () => { + const gen = sequence("-2n+5"); + + expect(gen()).toBe(0); + expect(gen()).toBe(2); + expect(gen()).toBe(4); + expect(gen()).toBe(null); + }); +}); diff --git a/src/compile.ts b/src/compile.ts index 2b6ee506..d7d37e31 100644 --- a/src/compile.ts +++ b/src/compile.ts @@ -7,6 +7,8 @@ import { trueFunc, falseFunc } from "boolbase"; * @param parsed A tuple [a, b], as returned by `parse`. * @returns A highly optimized function that returns whether an index matches the nth-check. * @example + * + * ```js * const check = nthCheck.compile([2, 3]); * * check(0); // `false` @@ -16,6 +18,7 @@ import { trueFunc, falseFunc } from "boolbase"; * check(4); // `true` * check(5); // `false` * check(6); // `true` + * ``` */ export function compile( parsed: [a: number, b: number] @@ -52,3 +55,68 @@ export function compile( ? (index) => index >= b && index % absA === bMod : (index) => index <= b && index % absA === bMod; } + +/** + * Returns a function that produces a monotonously increasing sequence of indices. + * + * If the sequence has an end, the returned function will return `null` after + * the last index in the sequence. + * + * @param parsed A tuple [a, b], as returned by `parse`. + * @returns A function that produces a sequence of indices. + * @example Always increasing (2n+3) + * + * ```js + * const gen = nthCheck.generate([2, 3]) + * + * gen() // `1` + * gen() // `3` + * gen() // `5` + * gen() // `8` + * gen() // `11` + * ``` + * + * @example With end value (-2n+10) + * + * ```js + * + * const gen = nthCheck.generate([-2, 5]); + * + * gen() // 0 + * gen() // 2 + * gen() // 4 + * gen() // null + * ``` + */ +export function generate(parsed: [a: number, b: number]): () => number | null { + const a = parsed[0]; + // Subtract 1 from `b`, to convert from one- to zero-indexed. + let b = parsed[1] - 1; + + let n = 0; + + // Make sure to always return an increasing sequence + if (a < 0) { + const aPos = -a; + // Get `b mod a` + const minValue = ((b % aPos) + aPos) % aPos; + return () => { + const val = minValue + aPos * n++; + + return val > b ? null : val; + }; + } + + if (a === 0) + return b < 0 + ? // There are no result — always return `null` + () => null + : // Return `b` exactly once + () => (n++ === 0 ? b : null); + + if (b < 0) { + b += a * Math.ceil(-b / a); + } + + return () => a * n++ + b; +} diff --git a/src/index.ts b/src/index.ts index a47a5479..2494e25e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,11 @@ import { parse } from "./parse"; -import { compile } from "./compile"; +import { compile, generate } from "./compile"; -export { parse, compile }; +export { parse, compile, generate }; /** * Parses and compiles a formula to a highly optimized function. - * Combination of `parse` and `compile`. + * Combination of {@link parse} and {@link compile}. * * If the formula doesn't match any elements, * it returns [`boolbase`](https://github.com/fb55/boolbase)'s `falseFunc`. @@ -29,3 +29,37 @@ export { parse, compile }; export default function nthCheck(formula: string): (index: number) => boolean { return compile(parse(formula)); } + +/** + * Parses and compiles a formula to a generator that produces a sequence of indices. + * Combination of {@link parse} and {@link generate}. + * + * @param formula The formula to compile. + * @returns A function that produces a sequence of indices. + * @example Always increasing + * + * ```js + * const gen = nthCheck.sequence('2n+3') + * + * gen() // `1` + * gen() // `3` + * gen() // `5` + * gen() // `8` + * gen() // `11` + * ``` + * + * @example With end value + * + * ```js + * + * const gen = nthCheck.sequence('-2n+5'); + * + * gen() // 0 + * gen() // 2 + * gen() // 4 + * gen() // null + * ``` + */ +export function sequence(formula: string): () => number | null { + return generate(parse(formula)); +}