Skip to content

Commit

Permalink
feat: Add generate and sequence methods (#207)
Browse files Browse the repository at this point in the history
  • Loading branch information
fb55 committed May 22, 2022
1 parent 57a5c62 commit 3e8cd1e
Show file tree
Hide file tree
Showing 5 changed files with 220 additions and 5 deletions.
58 changes: 57 additions & 1 deletion README.md
Expand Up @@ -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).

Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/__fixtures__/rules.ts
Expand Up @@ -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 = [
Expand Down
58 changes: 57 additions & 1 deletion 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);
Expand Down Expand Up @@ -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);
});
});
68 changes: 68 additions & 0 deletions src/compile.ts
Expand Up @@ -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`
Expand All @@ -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]
Expand Down Expand Up @@ -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 <caption>Always increasing (2n+3)</caption>
*
* ```js
* const gen = nthCheck.generate([2, 3])
*
* gen() // `1`
* gen() // `3`
* gen() // `5`
* gen() // `8`
* gen() // `11`
* ```
*
* @example <caption>With end value (-2n+10)</caption>
*
* ```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;
}
40 changes: 37 additions & 3 deletions 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`.
Expand All @@ -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 <caption>Always increasing</caption>
*
* ```js
* const gen = nthCheck.sequence('2n+3')
*
* gen() // `1`
* gen() // `3`
* gen() // `5`
* gen() // `8`
* gen() // `11`
* ```
*
* @example <caption>With end value</caption>
*
* ```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));
}

0 comments on commit 3e8cd1e

Please sign in to comment.