From aabc1fa1b1ddd42dc3dae554a4891d797e744037 Mon Sep 17 00:00:00 2001 From: Anton Date: Sat, 11 Jul 2026 01:46:43 +0200 Subject: [PATCH] chore: documentation and optimization What a rhyme. --- .config/build.js | 11 + .config/generateSwizzle.js | 165 +++++++++ .gitignore | 2 + doctests.config.ts | 1 + eslint.config.ts | 2 +- package.json | 11 +- pnpm-lock.yaml | 2 +- src/base/mixin.ts | 6 +- src/internal.ts | 2 +- src/mixin/awaited.ts | 24 +- src/mixin/math/base.ts | 69 +++- src/mixin/math/list.ts | 690 +++++++++++++++++++++++++++++++++---- src/mixin/math/number.ts | 89 +++-- src/mixin/math/swizzle.ts | 303 +++------------- 14 files changed, 1009 insertions(+), 368 deletions(-) create mode 100644 .config/build.js create mode 100644 .config/generateSwizzle.js diff --git a/.config/build.js b/.config/build.js new file mode 100644 index 0000000..079b630 --- /dev/null +++ b/.config/build.js @@ -0,0 +1,11 @@ +import * as esbuild from "esbuild"; + +const result = esbuild.buildSync({ + entryPoints: ["src/index.ts"], + bundle: true, + minify: true, + outdir: "dist", + define: { + "import.meta.vitest": "undefined", + }, +}); diff --git a/.config/generateSwizzle.js b/.config/generateSwizzle.js new file mode 100644 index 0000000..dbfc8b7 --- /dev/null +++ b/.config/generateSwizzle.js @@ -0,0 +1,165 @@ +import { writeFileSync } from "fs"; +import { relative } from "path"; + +function capitalize(/** @type {string} */ s) { + return s.slice(0, 1).toUpperCase() + s.slice(1).toLowerCase(); +} + +const script = import.meta.filename; + +const path = "./src/mixin/math/swizzlePermutations.d.ts"; +const axis = ["x", "y", "z", "w", "v"]; + +const property = ["first", "second", "third", "fourth", "fifth"]; + +const mixin = "./src/mixin/base/mixin"; + +let conditional = ""; + +for (let n = 0; n < axis.length; n++) { + if (n === axis.length - 1) { + conditional += `Swizzle${n + 1}D`; + break; + } + const type = new Array(n + 1).fill("number").join(", "); + const indent = "\t".repeat(n + 1); + conditional += `T extends readonly [${type}]\n`; + conditional += `${indent}? Swizzle${n + 1}D\n`; + conditional += `${indent}: `; +} + +const type = new Array(axis.length).fill("number").join(", "); + +let src = ` +// .d.ts file auto-generated by ${relative(path, script)} +// Do not edit + +import type { Props, Return } from '${relative(path, mixin)}'; + +type IsValid = number extends T['length'] + ? false + : T extends readonly [] + ? false + : true; + +export type SwizzleProps = + IsValid extends false + ? {} + : ${conditional + .split("\n") + .map((line) => "\t\t" + line) + .join("\n") + .trimStart()}; + +export interface ISwizzle< + T extends readonly [${type}] = + readonly [${type}], + t extends Props = Props +> extends Swizzle${axis.length}D {}; + +`.trimStart(); + +for (let n = 0; n < axis.length; n++) { + const state = new Array(axis.length).fill(-1); + + const props = []; + + outer: do { + for (let i = state.length - 1; i >= 0; i--) + if (++state[i] > n) { + state[i] = -1; + } else break; + + let nulled = false; + for (const item of state) { + if (item === -1) { + nulled = true; + continue; + } + if (nulled) continue outer; + } + if (state.every((x) => x === -1)) break; + + const key = state + .filter((x) => x !== -1) + .map((x) => axis[x]) + .join(""); + if (state.slice(1).every((x) => x === -1)) { + const arr = [5, 3, 8, 2, 1].map((x) => + Math.abs(x - n - state[0]), + ); + props.push({ + doc: ` +/** + * Vector-style accessor, ${property[state[0]]} property of the value + * @from {@link Math \`Math\`} + * @example + * \`\`\`ts + * const vec = [${arr + .slice(0, Math.max(n + 1, 2)) + .map((n) => n.toString()) + .join(", ")}] as const; + * + * const ${axis[state[0]]} = $(vec).${axis[state[0]]}.value; + * expect(${axis[state[0]]}).toBe(${arr[state[0]]}) + * \`\`\` + */`.trimStart(), + property: `readonly ${key}: Return`, + }); + continue; + } + + let count = 0; + if (state.every((x) => x === count++)) continue; + + const arr = [6, 2, 3, 5, 4, 7].map((x, idx) => + Math.abs(x - state[idx]), + ); + + const type = state + .filter((x) => x !== -1) + .map((x) => `T[${x}]`) + .join(", "); + props.push({ + doc: ` +/** + * Vector-style swizzle accessor, rearannges the elements of the value + * + * Equivalent to \`[${state + .filter((x) => x !== -1) + .map((x) => `vec[${x}]`) + .join(", ")}]\` + * @from {@link Math \`Math\`} + * @example + * \`\`\`ts + * const vec = [${arr.slice(0, n + 1).join(", ")}] as const; + * + * const ${key} = $(vec).${key}().value; + * expect(${key}).toMatchObject([${state + .filter((x) => x !== -1) + .map((x) => arr[x]) + .join(", ")}]) + * \`\`\` + */ +`.trimStart(), + property: `readonly ${key}: () => Return<[${type}], t>`, + }); + } while (state.some((x) => x !== -1)); + + props.sort((a, b) => a.property.length - b.property.length); + + src += `interface Swizzle${n + 1}D {\n`; + src += `${props + .map((x) => + [x.doc, x.property] + .join("\n") + .split("\n") + .map((line) => "\t" + line) + .join("\n"), + ) + .join("\n")}\n`; + src += "}\n"; + src += "\n"; +} + +writeFileSync(path, src); diff --git a/.gitignore b/.gitignore index 95e1312..910d57b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +src/**/*.d.ts + # Logs logs *.log diff --git a/doctests.config.ts b/doctests.config.ts index 68f7d3e..d639130 100644 --- a/doctests.config.ts +++ b/doctests.config.ts @@ -2,6 +2,7 @@ import { defineConfig } from "gen-doctests/config"; export default defineConfig({ include: ["src/**/*.{js,ts}"], + exclude: ["src/**/*.d.ts"], outDir: "tests/generated", templateHeader: [ "import { test, expect, expectTypeOf, describe, vi } from 'vitest'", diff --git a/eslint.config.ts b/eslint.config.ts index 135d8a5..f29c67b 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -3,7 +3,7 @@ import tseslint from "typescript-eslint"; import { defineConfig, globalIgnores } from "eslint/config"; export default defineConfig([ - globalIgnores(["dist", "tests/generated", "coverage"]), + globalIgnores(["dist", "tests/generated", "coverage", "*.d.ts"]), { files: ["**/*.{ts,tsx}"], extends: [ diff --git a/package.json b/package.json index 28c8c2b..0bacddc 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,14 @@ "version": "0.0.0", "type": "module", "scripts": { - "test": "gen-doctests && vitest --run --reporter=tree --coverage --typecheck", - "build": "tsc -b && esbuild --minify --bundle src/index.ts --outdir=dist --define:import.meta.vitest=undefined", + "test": "gen-doctests && vitest --run --reporter=tree --typecheck --cache", + "coverage": "gen-doctests && vitest --run --reporter=dot --coverage --cache", + "build:generate": "node .config/generateSwizzle.js", + "build": "tsc -b && node .config/build.js", "fmt": "prettier --write .", "lint": "eslint .", - "preview": "vite preview" + "prepublish": "pnpm build:generate", + "prepack": "pnpm test && pnpm build" }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -21,7 +24,7 @@ "jiti": "^2.7.0", "madge": "^8.0.0", "prettier": "^3.8.4", - "typescript": "6", + "typescript": "^6.0.3", "typescript-eslint": "^8.59.2", "vitest": "^4.1.9" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 113bbe8..8dcf815 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,7 +39,7 @@ importers: specifier: ^3.8.4 version: 3.8.4 typescript: - specifier: '6' + specifier: ^6.0.3 version: 6.0.3 typescript-eslint: specifier: ^8.59.2 diff --git a/src/base/mixin.ts b/src/base/mixin.ts index f3de819..0d20ce9 100644 --- a/src/base/mixin.ts +++ b/src/base/mixin.ts @@ -1,5 +1,5 @@ import type { Fluent } from "."; -import { never } from "../internal"; +import { phantom } from "../internal"; import type { Registry } from "../registry"; import type { RecPartial } from "../utility"; import type { HKT } from "./hkt"; @@ -29,8 +29,8 @@ export function Mixin>( fn: MixinFn, ): Mixin { return { - interface: never, - hkt: never, + interface: phantom, + hkt: phantom, fn, }; } diff --git a/src/internal.ts b/src/internal.ts index 99f91ad..6bf7a35 100644 --- a/src/internal.ts +++ b/src/internal.ts @@ -5,7 +5,7 @@ export interface Identity extends HKT { } // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -export const never = undefined as never; +export const phantom = undefined as never; export function isArray(v: unknown): v is unknown[] { return typeof v === "object" && v !== null && Array.isArray(v); diff --git a/src/mixin/awaited.ts b/src/mixin/awaited.ts index 6f4be8f..ae0b293 100644 --- a/src/mixin/awaited.ts +++ b/src/mixin/awaited.ts @@ -6,8 +6,9 @@ import { type Input, type Props, type Return, + type Shim, } from "../base/mixin"; -import { assert, never } from "../internal"; +import { assert, phantom } from "../internal"; import { Base } from "./base"; interface AwaitedIdentitity extends HKT { @@ -21,7 +22,7 @@ class Awaited> { } public get [shim]() { - return never as { + return phantom as { input: T extends Promise ? U : never; output: AwaitedIdentitity; value: T; @@ -114,11 +115,28 @@ export const AsyncMixin = Mixin( ); if (import.meta.vitest) { - const { test, expect } = import.meta.vitest; + const { test, expect, expectTypeOf, describe } = import.meta + .vitest; const registry = [Base, AsyncMixin] as const; const $ = makeFluent(registry); + describe("Awaited class", () => { + test("constructor", () => { + const promise = new Promise(() => void {}); + const awaited = new Awaited(promise); + + expect(awaited.value).toBe(promise); + }); + + test("shim", () => { + const awaited = new Awaited(new Promise(() => void {})); + + expect(awaited[shim]).toBeUndefined(); + expectTypeOf(awaited[shim]).toExtend(); + }); + }); + test(".awaited", async () => { const value = 10 as const; const promise = new Promise((r) => { diff --git a/src/mixin/math/base.ts b/src/mixin/math/base.ts index be04486..14b73dc 100644 --- a/src/mixin/math/base.ts +++ b/src/mixin/math/base.ts @@ -1,5 +1,14 @@ import type { Inc, Vec } from "../../utility"; +export type Truncate = + `${T}` extends `${infer N extends number}.${number}` ? N : T; +export type Decimal = + `${T}` extends `${infer Sign extends "-" | ""}${number}.${infer N extends number}` + ? `${Sign}0.${N}` extends `${infer O extends number}` + ? O + : never + : 0; + export type Floor = `${T}` extends `${infer N extends number}.${number}` ? N : T; export type Ceil = @@ -36,110 +45,144 @@ type Fn< interface IBase { /** * `x + y` - * @param other The second term of the addition (`y`) + * @param other Addend term of the addition (`y`) + * @from {@link Math `Math`} */ add: Fn<1>; /** * `x - y` - * @param other The second term of the subtraction (`y`) + * @param other Subtrahend term of the subtraction (`y`) + * @from {@link Math `Math`} */ subtract: Fn<1>; /** * `x * y` * @param factor Factor term of the multiplication (`y`) + * @from {@link Math `Math`} */ multiply: Fn<1>; /** * `x ** y` * @param exponent Exponent term of the power (`y`) + * @from {@link Math `Math`} */ pow: Fn<1, true>; /** * `Math.sqrt(x)` (or `Math.log2(x)`) + * @from {@link Math `Math`} */ sqrt: Fn<0, true>; /** * `x / y` * @param divisor Divisor term of the division (`y`) + * @from {@link Math `Math`} */ divide: Fn<1, true>; /** - * `x mod y`, not to be confused with `x % y` (remainder operation) + * `x mod y` modulo operation (not to be confused with `x % y` (remainder + * operation)) * @param divisor Divisor term of the modulo operation (`y`) * @see `.rem()` for the remainder operation + * @from {@link Math `Math`} */ mod: Fn<1, true>; /** - * `x % y` remainder operation, not to be confused with `x mod y` (modulo) + * `x % y` remainder operation (not to be confused with `x mod y` (modulo + * operation)) * @param divisor Divisor term of the remainder operation (`y`) * @see `.mod()` for the true modulo operation + * @from {@link Math `Math`} */ rem: Fn<1, true>; /** * `ln(x)`, `log(x, base)`, or alternatively `Math.log(x) / Math.log(base)` * @param base Base of the logarithm (`base`). Defaults to `Math.E` for a natural logarithm function + * @from {@link Math `Math`} */ log: Fn<0, true>; + /** + * `Math.trunc(x)` or `x - (x % 1)`, remove the decimal part of the value + * @from {@link Math `Math`} + */ + truncate: Fn<0, true>; + /** + * `x % 1`, remove the whole part of the value, leaving you with the decimals + * @from {@link Math `Math`} + */ + decimal: Fn<0, true>; /** * `Math.floor(x)` + * @from {@link Math `Math`} */ floor: Fn<0, true>; /** * `Math.ceil(x)` + * @from {@link Math `Math`} */ ceil: Fn<0, true>; /** - * `Math.round(x)` or `Math.round(x / multiple) * multiple` to round to the + * `Math.round(x)` or `Math.round(x / multiple) * multiple`, round to the * specified multiple * @param multiple Multiple to round to, 1 if unspecified + * @from {@link Math `Math`} */ round: Fn<1, true>; /** * `Math.fround(x)`, round to the nearest 32-bit float * approximation of value + * @from {@link Math `Math`} */ fround: Fn<0, true>; /** * `Math.f16round(x)`, round to the nearest 16-bit float * approximation of value + * @from {@link Math `Math`} */ ffround: Fn<0, true>; /** * `Math.abs(x)`, calculate the absolute value (distance - * from 0). + * from 0) + * @from {@link Math `Math`} */ abs: Fn<0, true>; /** * `Math.sign(x)`, returns the sign (1 or -1) of the value or zero + * @from {@link Math `Math`} */ sign: Fn<0, true>; /** * `-x` or alternatively `x * -1` + * @from {@link Math `Math`} */ negate: Fn<0, true>; /** - * `Math.min(x, ...values)` + * `Math.min(x, ...values)`, return the smallest of the provided values * @param values Other canidates + * @from {@link Math `Math`} */ min: Fn<0>; /** - * `Math.min(x, ...values)` + * `Math.min(x, ...values)`, return the largest of the provided values * @param values Other canidates + * @from {@link Math `Math`} */ max: Fn<0>; /** - * `Math.min(Math.max(x, min), max)` - * @param min Smallest allowed value to clamp to - * @param max Largest allowed value to clamp to + * `Math.min(Math.max(x, min), max)`, clamp value to the specified range + * [min, max] + * @param min Minimum value of the range + * @param max Maximum value of the range + * @from {@link Math `Math`} */ clamp: Fn<2, true>; /** - * Clamps value into the range [0.0, 1.0] + * Clamp value into the range [0.0, 1.0] * @see `.clamp()` for a customizable range + * @from {@link Math `Math`} */ saturate: Fn<0, true>; @@ -156,6 +199,7 @@ interface IBase { * @param b Target value to interpolate to * @param t Interpolation factor, the point on the line where 0.0 is `a` and 1.0 is `b` * @see `.inverseLerp()` for the inverse operation (to calculate `t`) + * @from {@link Math `Math`} */ lerp: Fn<2, true>; /** @@ -170,6 +214,7 @@ interface IBase { * @param a The 0.0 point of the range * @param b The 1.0 point of the range * @see `.lerp()` for the inverse operation + * @from {@link Math `Math`} */ inverseLerp: Fn<2, true>; } diff --git a/src/mixin/math/list.ts b/src/mixin/math/list.ts index 0c153c7..b829b4f 100644 --- a/src/mixin/math/list.ts +++ b/src/mixin/math/list.ts @@ -10,13 +10,25 @@ import { type Abs, type Base, type Ceil, + type Decimal, type Floor, type Negate, type Round, type Sign, + type Truncate, } from "./base"; -type Arg = Vec; +type Arg< + T extends readonly number[], + Readonly extends boolean = false, +> = Vec; + +interface TruncateHkt extends HKT { + new: (t: HKT.T) => Truncate; +} +interface DecimalHkt extends HKT { + new: (t: HKT.T) => Decimal; +} interface FloorHkt extends HKT { new: (t: HKT.T) => Floor; @@ -38,59 +50,419 @@ interface NegateHkt extends HKT { new: (t: HKT.T) => Negate; } +type ElementArg = + number extends T["length"] ? number : number | Arg; + namespace IList { export type BaseImpl< T extends readonly number[], t extends Props, - > = Base<{ - add: ( - other: number extends T["length"] - ? number - : number | Arg, - ) => Return, t>; - subtract: ( - other: number extends T["length"] - ? number - : number | Arg, - ) => Return, t>; - multiply: (factor: number) => Return, t>; - pow: (exponent: number) => Return, t>; - sqrt: () => Return, t>; - divide: (divisor: number) => Return, t>; - mod: (divisor: number) => Return, t>; - rem: (divisor: number) => Return, t>; - log: (base?: number) => Return, t>; + > = Base< + { + /** + * for each element + * @example + * ```ts + * const x = [4, 2] as const; + * + * const a = $(x).add(2).value; + * expect(a).toMatchObject([6, 4]); + * + * const b = $(x).add([2, 4]).value; + * expect(b).toMatchObject([6, 6]); + * ``` + */ + add: ( + /** + * Addend the addition (`y`), may also be a vector to add to + * the value + */ + other: ElementArg, + ) => Return, t>; + /** + * for each element + * @example + * ```ts + * const x = [1, 2] as const; + * + * const a = $(x).subtract(3).value; + * expect(a).toMatchObject([-2, -1]); + * + * const b = $(x).subtract([1, 2]).value; + * expect(b).toMatchObject([0, 0]); + * ``` + */ + subtract: ( + /** + * Subtrahend of the subtraction (`y`), may also be a vector to + * subtract from the value + */ + other: ElementArg, + ) => Return, t>; + /** + * for each element + * @example + * ```ts + * const x = [1, 2, 3] as const; + * + * const a = $(x).multiply(3).value; + * expect(a).toMatchObject([3, 6, 9]); + * + * const b = $(x).multiply([4, 5, 6]).value; + * expect(b).toMatchObject([4, 10, 18]); + * ``` + */ + multiply: ( + /** + * Factor term of the multiplicaton (`y`), may also be a vector to + * multiply the value by + */ + factor: ElementArg, + ) => Return, t>; + /** + * for each element + * @example + * ```ts + * const x = [4, 8]; + * + * const value = $(x).pow(2).value; + * expect(value).toMatchObject([16, 64]); + * ``` + */ + pow: ( + /** Exponent term of the power (`y`) */ + exponent: number, + ) => Return, t>; + /** + * for each element + * @example + * ```ts + * const x = [1, 4, 9, 16] as const; + * + * const value = $(x).sqrt().value; + * expect(value).toMatchObject([1, 2, 3, 4]); + * ``` + */ + sqrt: () => Return, t>; + /** + * for each element + * @example + * ```ts + * const x = [4, 5, 6] as const; + * + * const a = $(x).divide(2).value; + * expect(a).toMatchObject([2, 2.5, 3]); + * + * const b = $(x).divide([2, 2.5, 3]).value; + * expect(b).toMatchObject([2, 2, 2]); + * ``` + */ + divide: ( + /** + * Divisor term of the division (`y`), may also be a vector to + * divide the value by + */ + divisor: ElementArg, + ) => Return, t>; + /** + * for each element + * @example + * ```ts + * const x = [-5, 4, 1] as const; + * + * const value = $(x).mod(3).value; + * expect(value).toMatchObject([1, 1, 1]); + * ``` + */ + mod: ( + /** Divisor term of the modulo operation (`y`) */ + divisor: number, + ) => Return, t>; + /** + * for each element + * @example + * ```ts + * const x = [1, 2, 3, 4, 5] as const; + * + * const value = $(x).rem(2).value; + * expect(value).toMatchObject([1, 0, 1, 0, 1]); + * ``` + */ + rem: ( + /** Divisor term of the remainder operation (`y`) */ + divisor: number, + ) => Return, t>; + /** + * for each element + * @example + * ```ts + * const x = [4, Math.E, 8] as const; + * + * const a = $(x).log().value; + * expect(a).toMatchObject([Math.log(4), 1, Math.log(8)]); + * + * const b = $(x).log(2).value; + * expect(b).toMatchObject([2, Math.log2(Math.E), 3]) + * ``` + */ + log: ( + /** + * Base term of the logarithm (`base`) + * @default Math.E + */ + base?: number, + ) => Return, t>; - floor: () => Return, t>; - ceil: () => Return, t>; - round: ( - ...multiple: N - ) => Return< - N extends readonly [number] ? Arg : Apply, - t - >; - fround: () => Return, t>; - ffround: () => Return, t>; + /** + * for each element + * @example + * ```ts + * const x = [-3.5, 0, 0.5, 1] as const; + * + * const value = $(x).truncate().value; + * expect(value).toMatchObject([-3, 0, 0, 1]); + * ``` + */ + truncate: () => Return, t>; + /** + * for each element + * @example + * ```ts + * const x = [-0.5, 0, 2.5, 1] as const; + * + * const value = $(x).decimal().value; + * expect(value).toMatchObject([-0.5, 0, 0.5, 0]); + * ``` + */ + decimal: () => Return, t>; + /** + * for each element + * @example + * ```ts + * const x = [-1.2, 0, 1.7, 2] as const; + * + * const value = $(x).floor().value; + * expect(value).toMatchObject([-2, 0, 1, 2]); + * ``` + */ + floor: () => Return, t>; + /** + * for each element + * @example + * ```ts + * const x = [-1.9, 0, 1, 1.9, 2.1] as const; + * + * const value = $(x).ceil().value; + * expect(value).toMatchObject([-1, 0, 1, 2, 3]); + * ``` + */ + ceil: () => Return, t>; + /** + * for each element + * @example + * ```ts + * const x = [-1.9, 0.2, 1.4, 1.6, 2.7] as const; + * + * const a = $(x).round().value; + * expect(a).toMatchObject([-2, 0, 1, 2, 3]); + * + * const b = $(x).round(1.5).value; + * expect(b).toMatchObject([-1.5, 0, 1.5, 1.5, 3]); + * ``` + */ + round: ( + /** Unit multiple to round to */ + ...multiple: N + ) => Return< + N extends readonly [number] + ? Arg + : Apply, + t + >; + /** + * for each element + * @example + * ```ts + * const x = [1/2, 1/3, 1/4, 1/5] as const; + * + * const value = $(x).fround().value; + * expect(value).toMatchObject([ + * 0.5, + * Math.fround(0.33333333), + * 0.25, + * Math.fround(0.2) + * ]); + * ``` + */ + fround: () => Return, t>; + /** + * for each element + * @example + * ```ts + * const x = [1/2, 1/3, 1/4, 1/5] as const; + * + * const value = $(x).ffround().value; + * expect(value).toMatchObject([ + * 0.5, + * Math.f16round(0.3333), + * 0.25, + * Math.f16round(0.2) + * ]); + * ``` + */ + ffround: () => Return, t>; - abs: () => Return, t>; - sign: () => Return, t>; - negate: () => Return, t>; + /** + * for each element + * @example + * ```ts + * const x = [-2, -1, 0, 1, 2] as const; + * + * const value = $(x).abs().value; + * expect(value).toMatchObject([2, 1, 0, 1, 2]); + * ``` + */ + abs: () => Return, t>; + /** + * for each element + * @example + * ```ts + * const x = [-2, -1, 0, 1, 2] as const; + * + * const value = $(x).sign().value; + * expect(value).toMatchObject([-1, -1, 0, 1, 1]); + * ``` + */ + sign: () => Return, t>; + /** + * for each element + * @example + * ```ts + * const x = [-2, -1, 0, 1, 2] as const; + * + * const value = $(x).negate().value; + * expect(value).toMatchObject([2, 1, -0, -1, -2]); + * ``` + */ + negate: () => Return, t>; - min: ( - ...others: U - ) => Return; - max: ( - ...others: U - ) => Return; - clamp: ( - min: Min, - max: Max, - ) => Return, T>, t>; - saturate: () => Return, t>; + /** + * `Math.min(...values)`, return the smallest/minimum of the values + * @param values Other canidates + * @from {@link Math `Math`} + * @example + * ```ts + * const x = [4, 2, 7, 1] as const; + * + * const a = $(x).min().value; + * expect(a).toBe(1); + * + * const b = $(x).min(7, 0, 2).value; + * expect(b).toBe(0); + * ``` + */ + min: ( + /** Other candidates */ + ...others: U + ) => Return; + /** + * `Math.max(...values)`, return the largest/maximum of the values + * @param values Other canidates + * @from {@link Math `Math`} + * @example + * ```ts + * const x = [5, 2, 7, 1] as const; + * + * const a = $(x).max().value; + * expect(a).toBe(7); + * + * const b = $(x).max(4, 8).value; + * expect(b).toBe(8); + * ``` + */ + max: ( + /** Other candidates */ + ...others: U + ) => Return; + /** + * for each element + * @example + * ```ts + * const min = 0; + * const max = 10; + * + * const x = [-3, -1, 0, 4, 8, 12] as const; + * const value = $(x).clamp(min, max).value; + * expect(value).toMatchObject([0, 0, 0, 4, 8, 10]); + * ``` + */ + clamp: ( + min: Min, + max: Max, + ) => Return, T>, t>; + /** + * for each element + * @example + * ```ts + * const x = [-1, 0, 0.2, 0.7, 1, 1.1] as const; + * + * const value = $(x).saturate().value; + * expect(value).toMatchObject([0, 0, 0.2, 0.7, 1, 1]); + * ``` + */ + saturate: () => Return, t>; - lerp: (b: Arg, t: number) => Return, t>; - inverseLerp: (a: Arg, b: Arg) => Return, t>; - }>; + /** + * @example + * ```ts + * const from = [1, 2, 3, 4] as const; + * const to = [4, 3, 2, 1] as const; + * + * const a = $(from).lerp(to, 0).value; + * const b = $(from).lerp(to, 0.5).value; + * const c = $(from).lerp(to, 1).value; + * + * expect(a).toMatchObject(from); + * expect(b).toMatchObject([2.5, 2.5, 2.5, 2.5]); + * expect(c).toMatchObject(to); + * + * const d = $(from).lerp(to, 2).value; + * const e = $(from).lerp(to, -1).value; + * + * expect(d).toMatchObject([7, 4, 1, -2]); + * expect(e).toMatchObject([-2, 1, 4, 7]); + * ``` + */ + lerp: (b: Arg, t: number) => Return, t>; + /** + * @example + * ```ts + * const from = [1, 3, 5, 7, 9] as const; + * const to = [2, 4, 6, 8, 10] as const; + * + * const a = $([1, 3, 5, 7, 9]) + * .inverseLerp(from, to).value; + * const b = $([1.5, 3.5, 5.5, 7.5, 9.5]) + * .inverseLerp(from, to).value; + * const c = $([2, 4, 6, 8, 10]) + * .inverseLerp(from, to).value; + * + * expect(a).toMatchObject([0.0, 0.0, 0.0, 0.0, 0.0]); + * expect(b).toMatchObject([0.5, 0.5, 0.5, 0.5, 0.5]); + * expect(c).toMatchObject([1.0, 1.0, 1.0, 1.0, 1.0]); + * + * const d = $([0.5, 3, 5.5, 8, 10.5]) + * .inverseLerp(from, to).value; + * expect(d).toMatchObject([-0.5, 0.0, 0.5, 1.0, 1.5]); + * ``` + */ + inverseLerp: ( + a: Arg, + b: Arg, + ) => Return, t>; + }, + "min" | "max" + >; } export type IList< @@ -99,21 +471,193 @@ export type IList< > = T extends readonly [] | readonly [number] ? {} : IList.BaseImpl & { + /** + * Compute the sum of all elements, equivalent to `.reduce(0, (a, b) + * => a + b)` + * @example + * ```ts + * const x = [1, 2, 3, 4, 5] as const; + * + * const value = $(x).sum().value; + * expect(value).toBe(15); + * ``` + */ sum: () => Return; + /** + * Compute the product of all elements, equivalent to `.reduce(0, + * (a, b) => a * b)` + * @example + * ```ts + * const x = [5, 1, 3] as const; + * + * const value = $(x).product().value; + * expect(value).toBe(15); + * ``` + */ product: () => Return; + /** + * Get the index of the first occurance of the smallest/minimum + * value in the list + * @example + * ```ts + * const x = [5, 2, 4, 8] as const; + * + * const index = $(x).findMin().value; + * expect(index).toBe(1); + * ``` + */ findMin: () => Return; + /** + * Get the index of the first occurance of the largest/maximum value + * in the list + * @example + * ```ts + * const x = [4, 5, 2, 8] as const; + * + * const index = $(x).findMax().value; + * expect(index).toBe(3); + * ``` + */ findMax: () => Return; + /** + * Compute the statistical mean or average of all elements + * + * Equivalent to `.reduce((a, b) => a + b).divide(N)` where `N` is + * the count of elements (`.length()`) + * @example + * ```ts + * const a = $([1, 2, 3, 4, 5]).average().value; + * const b = $([3, 3, 3, 3, 3]).average().value; + * + * expect(a).toBe(3); + * expect(b).toBe(3); + * ``` + */ average: () => Return; + /** + * Compute the statistical median of all the elements + * + * The median is the average of the one or two middle value(s) in + * this list when it's sorted + * @example + * ```ts + * const a = $([1, 2, 3, 4, 5]).median().value; + * const b = $([3, 4, 0, 3, 7]).median().value; + * + * expect(a).toBe(3); + * expect(b).toBe(3); + * + * const c = $([1, 2, 3, 4]).median().value; + * expect(c).toBe(2.5); + * ``` + */ median: () => Return; + /** + * Gather the statistical mode of all the elements, get a list of + * the elements which occured most times + * @example + * ```ts + * const a = $([1, 2, 1, 1, 4, 5]).mode().value; + * const b = $([1, 2, 5]).mode().value; + * + * expect(a).toMatchObject([1]); + * expect(b).toMatchObject([1, 2, 5]); + * ``` + */ mode: () => Return; + /** + * Compute the statistical range of the list + * + * Equivalent to `Math.max(...value) - Math.min(...value)` + * @example + * ```ts + * const x = [16, 3, 8, 3, 5, 2] + * + * const range = $(x).range().value; + * expect(range).toBe(14) + * ``` + */ range: () => Return; + /** + * Compute the statistical variance across the list, treating it as + * a *population* + * + * The deviation of any given element is `(x - mean) ** 2`, where + * `mean` is the mean or average of the entire list. The list's + * variance is the mean of the deviation across all elements. + * + * @see `.variance.sample()` for a sample variance + * @see `.standardDeviation()` for standard deviation + * @example + * ```ts + * const x = [1, 3, 5, 8, 9] as const; + * + * const variance = $(x).variance().value; + * expect(variance).toBeCloseTo(8.96); + * ``` + */ variance: (() => Return) & { + /** + * Compute the statistical variance across the list, treating it + * as a *sample* + * + * Instead of calculating the mean of the deviation of all + * elements, the sum is divided by `N - 1` where `N` is the + * number of elements, this is effectively an overshot mean + * which compensates for the data being a subset of a larger + * population + * + * @see `.variance()` for a population variance and the details about deviation + * @see `.standardDeviation.sample()` for standard deviation + * @example + * ```ts + * const x = [5, 3, 2, 8, 5] as const; + * + * const variance = $(x).variance.sample().value; + * expect(variance).toBeCloseTo(5.3); + * ``` + */ sample: () => Return; } & HidePrototype; - deviation: (() => Return) & { + /** + * Compute the standard deviation across the list, treating it as a + * *population*. This is equivalent to `Math.sqrt(v)` where `v` is + * the population variance of the same data. + * + * A low standard deviation indicates that induvidual values of the + * data tend to be close to the average, while a high standard + * deviation indicates that the induvidual values are more spread + * out. + * + * @see `.standardDeviation.sample()` for a standard deviation treating the data as a sample + * @see `.variance()` + * @example + * ```ts + * const x = [6, 2, 1, 3, 5] as const; + * + * const stdDv = $(x).standardDeviation().value; + * expect(stdDv).toBeCloseTo(1.854); + * ``` + */ + standardDeviation: (() => Return) & { + /** + * Compute the standard deviation across the list, treating it as a + * *sample*. This is equivalent to `Math.sqrt(sv)` where `sv` is + * the sample variance of the same data. + * + * @see `.standardDeviation()` for a standard deviation treating the data as a population + * @see `.variance.sample()` + * @example + * ```ts + * const x = [7, 2, 1, 3] as const; + * + * const stdDv = $(x).standardDeviation.sample().value; + * expect(stdDv).toBeCloseTo(2.63); + * ``` + */ sample: () => Return; } & HidePrototype; }; @@ -140,13 +684,31 @@ export const applyList = Mixin.partial( } return fluent(value.map((x) => x - other)); }; - $.multiply = (factor: number) => - fluent(value.map((x) => x * factor)); + $.multiply = (factor: number | number[]) => { + if (typeof factor === "object") { + assert( + value.length === factor.length, + "List.multiply() parameters must be of equal length", + ); + return fluent(value.map((x, idx) => x * factor[idx])); + } + return fluent(value.map((x) => x * factor)); + }; $.pow = (exponent: number) => fluent(value.map((x) => x ** exponent)); $.sqrt = () => fluent(value.map(Math.sqrt)); - $.divide = (divisor: number) => - fluent(value.map((x) => x / divisor)); + $.divide = (divisor: number | number[]) => { + if (typeof divisor === "object") { + assert( + value.length === divisor.length, + "List.divide() parameters must be of equal length", + ); + return fluent( + value.map((x, idx) => x / divisor[idx]), + ); + } + return fluent(value.map((x) => x / divisor)); + }; $.mod = (divisor: number) => fluent( value.map((x) => ((x % divisor) + divisor) % divisor), @@ -156,6 +718,9 @@ export const applyList = Mixin.partial( $.log = (base: number = Math.E) => fluent(value.map((x) => log(x, base))); + $.truncate = () => fluent(value.map(Math.trunc)); + $.decimal = () => fluent(value.map((x) => x % 1)); + $.floor = () => fluent(value.map(Math.floor)); $.ceil = () => fluent(value.map(Math.ceil)); $.round = (multiple?: number) => @@ -165,7 +730,7 @@ export const applyList = Mixin.partial( (x) => Math.round(x / multiple) * multiple, ) - : value.map(Math.floor), + : value.map(Math.round), ); $.fround = () => fluent(value.map(Math.fround)); $.ffround = () => fluent(value.map(Math.f16round)); @@ -215,12 +780,13 @@ export const applyList = Mixin.partial( const sorted = value.toSorted((a, b) => a - b); const middle = (sorted.length - 1) / 2; - if (sorted.length % 2 === 1) return sorted[middle]; + if (sorted.length % 2 === 1) + return fluent(sorted[Math.floor(middle)]); const a = sorted[Math.floor(middle)]; const b = sorted[Math.ceil(middle)]; - return (a + b) / 2; + return fluent((a + b) / 2); }; $.mode = () => { const occurances = new Map(); @@ -244,8 +810,10 @@ export const applyList = Mixin.partial( fluent(Math.max(...value) - Math.min(...value)); const variance = (sample: boolean) => { - if (value.length === 0) return NaN; - if (value.length === 1) return sample ? NaN : 0; + assert( + value.length > 1, + "variance() takes in a list of at least two elements", + ); const mean = value.reduce((a, b) => a + b, 0) / value.length; @@ -259,7 +827,7 @@ export const applyList = Mixin.partial( $.variance = Object.assign(() => fluent(variance(false)), { sample: () => fluent(variance(true)), }); - $.deviation = Object.assign( + $.standardDeviation = Object.assign( () => fluent(Math.sqrt(variance(false))), { sample: () => fluent(Math.sqrt(variance(true))), @@ -267,11 +835,3 @@ export const applyList = Mixin.partial( ); }, ); - -if (import.meta.vitest) { - const { test, expect, expectTypeOf, describe } = import.meta - .vitest; - - const registry = [M] as const; - const $ = makeFluent(registry); -} diff --git a/src/mixin/math/number.ts b/src/mixin/math/number.ts index db273a3..6c56322 100644 --- a/src/mixin/math/number.ts +++ b/src/mixin/math/number.ts @@ -7,16 +7,17 @@ import { type Abs, type Base, type Ceil, + type Decimal, type Floor, type Negate, type Round, type Sign, + type Truncate, } from "./base"; namespace INumber { export type BaseImpl = Base<{ /** - * @from {@link Math `Math`} * @example * ```ts * const v = $(12).add(8).value; @@ -24,11 +25,10 @@ namespace INumber { * ``` */ add: ( - /** Term to add */ + /** Addend term of the addition (`y`) */ other: number, ) => Return; /** - * @from {@link Math `Math`} * @example * ```ts * const v = $(12).subtract(8).value; @@ -36,11 +36,10 @@ namespace INumber { * ``` */ subtract: ( - /** Term to subtract by */ + /** Subtrahend term of the subtraction (`y`) */ other: number, ) => Return; /** - * @from {@link Math `Math`} * @example * ```ts * const v = $(12).multiply(8).value; @@ -48,11 +47,10 @@ namespace INumber { * ``` */ multiply: ( - /** Factor to multiply by */ + /** Factor term of the multiplication (`y`) */ factor: number, ) => Return; /** - * @from {@link Math `Math`} * @example * ```ts * const v = $(12).pow(2).value; @@ -60,11 +58,10 @@ namespace INumber { * ``` */ pow: ( - /** Exponent to raise to */ + /** Exponent term of the power (`y`) */ exponent: number, ) => Return; /** - * @from {@link Math `Math`} * @example * ```ts * const v = $(144).sqrt().value; @@ -73,7 +70,6 @@ namespace INumber { */ sqrt: () => Return; /** - * @from {@link Math `Math`} * @example * ```ts * const v = $(12).divide(8).value; @@ -81,11 +77,10 @@ namespace INumber { * ``` */ divide: ( - /** Divisor to divide by */ + /** Divisor term of the division (`y`) */ divisor: number, ) => Return; /** - * @from {@link Math `Math`} * @example * ```ts * const v = $(12).mod(8).value; @@ -93,11 +88,10 @@ namespace INumber { * ``` */ mod: ( - /** Divisor to divide by */ + /** Divisor term of the modulo operation (`y`) */ divisor: number, ) => Return; /** - * @from {@link Math `Math`} * @example * ```ts * const v = $(12).rem(8).value; @@ -105,11 +99,10 @@ namespace INumber { * ``` */ rem: ( - /** Divisor to divide by */ + /** Divisor term of the remainder operation (`y`) */ divisor: number, ) => Return; /** - * @from {@link Math `Math`} * @example * ```ts * const a = $(Math.E).log().value; @@ -120,12 +113,31 @@ namespace INumber { * ``` */ log: ( - /** The base of the logarithm */ + /** + * Base term of the logarithm (`base`) + * @default Math.E + */ base?: number, ) => Return; /** - * @from {@link Math `Math`} + * @example + * ```ts + * const v = $(-1.8).truncate().value; + * expect(v).toBe(-1); + * ``` + */ + truncate: () => Return, t>; + /** + * @example + * ```ts + * const v = $(4.25).decimal().value; + * expect(v).toBeCloseTo(0.25); + * ``` + */ + decimal: () => Return, t>; + + /** * @example * ```ts * const v = $(1.7).floor().value; @@ -134,7 +146,6 @@ namespace INumber { */ floor: () => Return, t>; /** - * @from {@link Math `Math`} * @example * ```ts * const v = $(1.3).ceil().value; @@ -143,7 +154,6 @@ namespace INumber { */ ceil: () => Return, t>; /** - * @from {@link Math `Math`} * @example * ```ts * const a = $(1.3).round().value; @@ -167,7 +177,6 @@ namespace INumber { t >; /** - * @from {@link Math `Math`} * @example * ```ts * const v = $(0.99999999).fround().value; @@ -176,7 +185,6 @@ namespace INumber { */ fround: () => Return; /** - * @from {@link Math `Math`} * @example * ```ts * const v = $(0.9999).ffround().value; @@ -186,7 +194,6 @@ namespace INumber { ffround: () => Return; /** - * @from {@link Math `Math`} * @example * ```ts * const v = 4 as const; @@ -200,7 +207,6 @@ namespace INumber { */ abs: () => Return, t>; /** - * @from {@link Math `Math`} * @example * ```ts * const v = 10 as const; @@ -214,7 +220,6 @@ namespace INumber { */ negate: () => Return, t>; /** - * @from {@link Math `Math`} * @example * ```ts * const a = $(-42).sign().value; @@ -229,7 +234,6 @@ namespace INumber { sign: () => Return, t>; /** - * @from {@link Math `Math`} * @example * ```ts * const maximum = 10; @@ -246,7 +250,6 @@ namespace INumber { ...values: U ) => Return; /** - * @from {@link Math `Math`} * @example * ```ts * const minimum = 0; @@ -263,7 +266,6 @@ namespace INumber { ...values: U ) => Return; /** - * @from {@link Math `Math`} * @example * ```ts * const min = 0; @@ -279,13 +281,12 @@ namespace INumber { * ``` */ clamp: ( - /** The minimum (smallest) value of the range */ + /** Minimum value of the range */ min: Min, - /** The maximum (largest) value of the range */ + /** Maximum value of the range */ max: Max, ) => Return; /** - * @from {@link Math `Math`} * @example * ```ts * const a = $(-2).saturate().value; @@ -300,7 +301,6 @@ namespace INumber { saturate: () => Return; /** - * @from {@link Math `Math`} * @example * ```ts * const from = 12; @@ -327,6 +327,27 @@ namespace INumber { /** Interpolation factor, 0.0 corresponds to `a` and 1.0 corresponds to `b` */ t: number, ) => Return; + /** + * @example + * ```ts + * const from = 5; + * const to = 10; + * + * const a = $(5).inverseLerp(from, to).value; + * const b = $(7.5).inverseLerp(from, to).value; + * const c = $(10).inverseLerp(from, to).value; + * + * expect(a).toBe(0.0); + * expect(b).toBe(0.5); + * expect(c).toBe(1.0); + * + * const d = $(15).inverseLerp(from, to).value; + * const e = $(0).inverseLerp(from, to).value; + * + * expect(d).toBe(2); + * expect(e).toBe(-1); + * ``` + */ inverseLerp: ( /** 0.0 point of the range */ a: number, @@ -483,6 +504,8 @@ export const applyNumber = Mixin.partial( return fluent(Math.abs(value - other) < delta); }; + $.truncate = () => fluent(Math.trunc(value)); + $.decimal = () => fluent(value % 1); $.floor = () => fluent(Math.floor(value)); $.ceil = () => fluent(Math.ceil(value)); $.round = (multiple?: number) => @@ -575,6 +598,8 @@ if (import.meta.vitest) { ).toBe(false); }); + t("truncate", -6.4, -6); + t("decimal", -6.4, -6.4 % 1); test("floor()", () => { expect($(1).floor().value).toBe(1); expect($(1.7).floor().value).toBe(1); diff --git a/src/mixin/math/swizzle.ts b/src/mixin/math/swizzle.ts index d274ba4..3cff64d 100644 --- a/src/mixin/math/swizzle.ts +++ b/src/mixin/math/swizzle.ts @@ -1,230 +1,65 @@ import { makeFluent } from "../../base"; -import { Mixin, type Props, type Return } from "../../base/mixin"; +import { Mixin } from "../../base/mixin"; import { Math } from "../math"; -import type { Vec, Dec, Inc as __Inc } from "../../utility"; +import type { ISwizzle, SwizzleProps } from "./swizzlePermutations"; -type Axis = typeof axis; +const axis = ["x", "y", "z", "w", "v"] as const; -type Inc = N extends number - ? __Inc - : 0; - -type Next< - N extends number, - State extends readonly (number | undefined)[], - TOut extends (number | undefined)[] = [], -> = State extends readonly [ - infer Current extends number | undefined, - ...infer Rest extends (number | undefined)[], -] - ? Inc extends infer TInc - ? TInc extends N - ? Next - : [...TOut, TInc, ...Rest] - : never - : null; - -type Key< - State extends (number | undefined)[], - TOut extends string = "", -> = State extends readonly [infer Only extends number, ...undefined[]] - ? `${TOut}${Axis[Only]}` - : State extends readonly [ - infer Current extends number, - ...infer Rest extends (number | undefined)[], - ] - ? Key - : never; - -type IsAscending< - State extends (number | undefined)[], - Prev extends number | undefined = undefined, -> = State extends readonly [Inc, ...infer Rest extends number[]] - ? IsAscending> - : State extends readonly [] - ? true - : false; - -type Pretty = { [K in keyof T]: T[K] }; - -type SwizzlePermutations< - N extends number, - State extends Vec = Vec, - TOut extends Record = Record< - never, - never - >, -> = - Next extends infer TNext - ? TNext extends Vec - ? SwizzlePermutations< - N, - TNext, - TOut & - (IsAscending extends true - ? unknown - : Record, State>) - > - : Pretty, State>> - : never; - -type Sequence< - T extends readonly unknown[], - Indexes extends (number | undefined)[], - TOut extends T[number][] = [], -> = Indexes extends readonly [ - infer Current extends number, - ...infer Rest extends (number | undefined)[], -] - ? Sequence - : TOut; - -type Primative< - T extends readonly unknown[], - t extends Props, - Axes extends readonly string[] = Axis, - Acc = unknown, - TOut extends unknown[] = [], -> = Axes extends readonly [ - infer Current extends string, - ...infer Rest extends string[], -] - ? Acc & - Record< - Current, - Return - > extends infer TAcc - ? Primative - : never - : TOut; - -interface Swizzle4D extends SwizzlePermutations<4> {} - -type SwizzleCache = [ - { x: [0] }, - SwizzlePermutations<2>, - SwizzlePermutations<3>, - Swizzle4D, -]; - -export type Swizzle< - T extends readonly unknown[], - t extends Props, -> = number extends T["length"] - ? unknown - : T extends readonly [] - ? unknown - : ( - Dec extends infer K extends - | 0 - | 1 - | 2 - | 3 - ? { k: K; c: SwizzleCache[K] } - : SwizzleCache extends [ - ...unknown[], - infer Last, - ] - ? { - k: Dec; - c: Last; - } - : never - ) extends { - k: infer K extends number; - c: infer C extends Record< - PropertyKey, - (number | undefined)[] - >; - } - ? Omit< - { - readonly [K in keyof C]: Return< - Sequence, - t - >; - }, - Axis[number] - > & - Primative[K] - : never; - -export type ISwizzle< - T extends readonly unknown[] = readonly unknown[], - t extends Props = Props, -> = { - readonly [K in keyof Swizzle4D]: Return< - Sequence, - t - >; -} & Primative[3]; - -const axis = ["x", "y", "z", "w"] as const; +export type { ISwizzle, SwizzleProps as Swizzle }; export const applySwizzle = Mixin.partial< ISwizzle, readonly unknown[] >((value, $, fluent) => { const length = globalThis.Math.min(value.length, axis.length); - const state = new Array(length).fill(-1); + const state = new Array(axis.length).fill(-1); - main: do { - for (let i = 0; i < state.length; i++) { - state[i] += 1; - if (state[i] >= length) state[i] = -1; - else break; - } + outer: do { + for (let i = 0; i < state.length; i++) + if (++state[i] >= length) { + state[i] = -1; + } else break; - let reachedEmpty = false; + let nulled = false; for (const item of state) { if (item === -1) { - reachedEmpty = true; - } else if (reachedEmpty) { - continue main; + nulled = true; + continue; } + if (nulled) continue outer; } + if (state.every((x) => x === -1)) break; const key = state .filter((x) => x !== -1) .map((x) => axis[x]) .join(""); - if (key.length <= 1 || key in $) continue; + if (state.slice(1).every((x) => x === -1)) { + const v = value[state[0]]; + Object.defineProperty($, key, { + get: () => { + return fluent(v); + }, + }); + continue; + } + + let count = 0; + if (state.every((x) => x === count++)) continue; + const permutation = state .filter((x) => x !== -1) .map((x) => value[x]); Object.defineProperty($, key, { - get: () => { - // OPTIMIZE? Reduce amount of arrays allocated and held for the lambda..? + value: () => { return fluent(permutation); }, }); } while (state.some((x) => x !== -1)); - - Object.defineProperty($, "x", { - get: () => { - return fluent(value[0]); - }, - }); - Object.defineProperty($, "y", { - get: () => { - return fluent(value[1]); - }, - }); - Object.defineProperty($, "z", { - get: () => { - return fluent(value[2]); - }, - }); - Object.defineProperty($, "w", { - get: () => { - return fluent(value[3]); - }, - }); }); if (import.meta.vitest) { - const { test, describe, expect, expectTypeOf } = import.meta - .vitest; + const { test, expect, expectTypeOf } = import.meta.vitest; const registry = [Math] as const; const $ = makeFluent(registry); @@ -235,20 +70,33 @@ if (import.meta.vitest) { const vec = [x, y] as const; - expect($(vec).xx.value).toMatchObject([x, x]); - expect($(vec).yy.value).toMatchObject([y, y]); + expect($(vec).xx().value).toMatchObject([x, x]); + expect($(vec).yy().value).toMatchObject([y, y]); - const yx = $(vec).yx.value; + expect($(vec).xyxy().value).toMatchObject([x, y, x, y]); + + const yx = $(vec).yx().value; expect(yx).toMatchObject([y, x]); expectTypeOf(yx).toEqualTypeOf<[typeof y, typeof x]>(); const large = [x, y, x, y] as const; - expect($(large).wzyx.value).toMatchObject([y, x, y, x]); - expect($(large).xzyw.value).toMatchObject([x, x, y, y]); - expect($(large).ywxz.value).toMatchObject([y, y, x, x]); - expect($(large).xxxx.value).toMatchObject([x, x, x, x]); - expect($(large).wwww.value).toMatchObject([y, y, y, y]); + expect($(large).wzyx().value).toMatchObject([y, x, y, x]); + expect($(large).xzyw().value).toMatchObject([x, x, y, y]); + expect($(large).ywxz().value).toMatchObject([y, y, x, x]); + expect($(large).xxxx().value).toMatchObject([x, x, x, x]); + expect($(large).wwww().value).toMatchObject([y, y, y, y]); + + const v = 3 as const; + const gigantic = [x, y, v, y, x] as const; + + expect($(gigantic).vwzyx().value).toMatchObject([ + x, + y, + v, + y, + x, + ]); }); test(".x", () => { @@ -287,49 +135,12 @@ if (import.meta.vitest) { expectTypeOf(v).toEqualTypeOf(w); }); - describe("types", () => { - test("Next", () => { - type N = 3; + test(".v", () => { + const v = 0 as const; + const arr = [v, v, v, v, v] as const; - expectTypeOf< - Next - >().toEqualTypeOf<[0, undefined, undefined]>(); - - expectTypeOf< - Next - >().toEqualTypeOf<[undefined, 0, undefined]>(); - - expectTypeOf>().toEqualTypeOf(); - }); - - test("Key", () => { - expectTypeOf< - Key<[1, 2, undefined]> - >().toEqualTypeOf<"yz">(); - expectTypeOf>().toEqualTypeOf<"yyy">(); - }); - - test("Sequence", () => { - type In = [1, 2, 3, 4, 5]; - type Out = [5, 4, 3, 2, 1]; - type Reverse = [4, 3, 2, 1, 0]; - - expectTypeOf< - Sequence - >().toEqualTypeOf(); - }); - - test("Primative", () => { - interface Props { - value: [number, number]; - meta: { registry: [] }; - } - - type V = Primative<[number, number], Props>[1]; - - expectTypeOf().toHaveProperty("x"); - expectTypeOf().toHaveProperty("y"); - expectTypeOf().not.toHaveProperty("z"); - }); + const value = $(arr).v.value; + expect(value).toBe(v); + expectTypeOf(value).toEqualTypeOf(v); }); }