feat: progress

This commit is contained in:
2026-06-26 04:21:49 +02:00
parent d1a1fbf85a
commit a9925ac2ec
33 changed files with 4608 additions and 811 deletions
+15 -2
View File
@@ -10,14 +10,27 @@ export type Base<T> = T extends { [shim]: { value: infer U } }
export type Fluent<
T,
Reg extends Registry = DefaultRegistry,
> = Base<T> & Methods<T, Reg>;
> = Base<T> & Methods<T, Reg> extends infer U
? U &
Pick<
{
/**
* Immediate casts and conversions to other types.
*/
to: unknown;
[K: PropertyKey]: unknown;
},
keyof U
>
: never;
export function makeFluent<const Reg extends Registry>(
registry: Reg,
) {
const fluent = <const T>(value: T) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const f = { value } as never as Fluent<T, Reg>;
const f = { value } as unknown as Fluent<T, Reg>;
for (const mixin of registry) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
+1
View File
@@ -2,3 +2,4 @@ import { makeFluent } from "./base";
import { DEFAULT_REGISTRY } from "./registry";
export const fluent = makeFluent(DEFAULT_REGISTRY);
export const $ = fluent;
+1 -1
View File
@@ -7,7 +7,7 @@ export interface Identity extends HKT {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
export const never = undefined as never;
export type Constraint<T, U extends T> = U;
export type MaxDepth = 20;
export class AssertionError extends Error {
public constructor(msg?: string) {
+193 -26
View File
@@ -141,7 +141,9 @@ export interface Array extends Mixin.HKT {
* Index the array using the specified zero-based index.
* Negative indexes start at the end of the array.
* @param index Zero-based index
* @from {@link Array `Array`}
* @example
* ```ts
* const array = ["first", "middle", "last"] as const;
*
* const first = $(array).at(0).value;
@@ -154,16 +156,19 @@ export interface Array extends Mixin.HKT {
*
* const oob = $(array).at(10).value;
* expect(oob).toBe(undefined);
* @from {@link Array `Array`}
* ```
*/
at: <const K extends keyof T & number>(
/** Zero-based index */
index: K,
) => Return<At<T, K>, typeof t>;
/**
* Interospect each item of the array using the specified
* callback
* @param callback Callback taking item, index, and the array for each item.
* @param callback Function called with each element of the array
* @from {@link Array `Array`}
* @example
* ```ts
* const array = [4, 5, 6] as const;
*
* let sum = 0;
@@ -171,55 +176,84 @@ export interface Array extends Mixin.HKT {
*
* expect(sum).toBe(15);
* expect(v).toMatchObject(array);
* @from {@link Array `Array`}
* ```
*/
each: (
/**
* Predicate to run on every element
* @param element The current element
* @param index The index of the current element in the array
* @param array The array being iterated
*/
callback: (...args: IterArgs<T>) => void,
) => Return<T, typeof t>;
/**
* Transform each item of the array using the specified callback
* @param callback Transformative callback taking item, index, and array; returns the new item
* @param callback Predicate to compute each new value with
* @from {@link Array `Array`}
* @example
* ```ts
* const chars = ["h", "e", "l", "l", "o"] as const;
*
* const v = $(chars).map(ch => ch.toUpperCase()).value;
* expect(v).toMatchObject(["H", "E", "L", "L", "O"]);
* @from {@link Array `Array`}
* ```
*/
map: <U>(
/**
* Predicate to compute each new value of the array
* @param element The current element
* @param index The index of the current element in the array
* @param array The array being iterated
* @return The new value to use for this element
*/
callback: (...args: IterArgs<T>) => U,
) => Return<Map<T, U>, typeof t>;
/**
* Extend the array by repeating its current contents n times
* @param count The number of repetitions to add
* @param count The amount of repetitions to extend the array by
* @from {@link Array `Array`}
* @example
* ```ts
* const array = [1, 2];
*
* const v = $(array).extend(2).value;
* expect(v).toMatchObject([1, 2, 1, 2, 1, 2]);
* @from {@link Array `Array`}
* ```
*/
extend: <const N extends number>(
/** The amount of repetitions to extend the array by */
count: N,
) => Return<Repeat<T, N>, typeof t>;
/**
* Filter the array to contain only items satisfying the
* specified callback conditional
* @param callback Callback getting item, index, and array for each item and determining if it should be filtered out or not
* @param callback Predicate determining whether each element should be kept in the array
* @from {@link Array `Array`}
* @example
* ```ts
* const dirty = [-2, 4, 1, -5, -6] as const;
*
* const v = $(dirty).filter(v => v >= 0).value;
* expect(v).toMatchObject([4, 1]);
* @from {@link Array `Array`}
* ```
*/
filter: ((
/**
* Predicate to compute filtering with
* @param element The current element
* @param index The index of the current element in the array
* @param array The array being iterated
* @return `true` if the value should be kept and `false` if it should not
*/
callback: (...args: IterArgs<T>) => boolean,
) => Return<Unordered<T>, typeof t>) & {
/**
* Filter the array to only contain items which are not
* `null` or `undefined`.
* @from {@link Array `Array`}
* @example
* ```ts
* const array = new Array(5);
*
* array[0] = 1;
@@ -228,7 +262,7 @@ export interface Array extends Mixin.HKT {
*
* const v = $(array).filter.some().value;
* expect(v).toMatchObject([1, 5, 2]);
* @from {@link Array `Array`}
* ```
*/
some: () => Return<
T extends unknown[]
@@ -243,23 +277,106 @@ export interface Array extends Mixin.HKT {
typeof t
>;
} & HidePrototype;
/**
* Collapse array into one value using the specified accumulator callback
* @param initial The initial value of the accumulator
* @param callback Predicate used to compute the new value of the accumulator with each element
* @from {@link Array `Array`}
* @example
* ```ts
* const data = [-5, -4, -2, 1, 2, 3, 5] as const;
*
* const sum = $(data)
* .reduce(0, (total, value) => total + value)
* .value;
* expect(sum).toBe(0);
* ```
*/
reduce: (<U>(
/**
* Initial value to use for the accumulator
*/
initial: U,
/**
* Predicate to compute the new accumulator value
* @param value The current status of the accumulator
* @param element The current element
* @param index The index of the current element in the array
* @param array The array being iterated
*/
callback: (
value: U,
...args: IterArgs<T>
) => U,
) => Return<U, typeof t>) & {
/**
* Collapse the array using the specified accumulator function, starting from the right.
* @param initial The initial value of the accumulator
* @param callback Predicate used to compute the new value of the accumulator with each element
* @see `.reduce()`
* @example
* ```ts
* const path = ["a", "b", "c"] as const;
*
* const v = $(path)
* .reduce.right({}, (obj, key) => ({ [key]: obj }))
* .value;
* expect(v).toMatchObject({ a: { b: { c: { } } } });
* ```
*/
right: <U>(
/**
* Initial value to use for the accumulator
*/
initial: U,
callback: (
value: U,
...args: IterArgs<T>
) => U,
/**
* Predicate to compute the new accumulator value
* @param value The current status of the accumulator
* @param element The current element
* @param index The index of the current element in the array
* @param array The array being iterated
*/
) => Return<U, typeof t>;
} & HidePrototype;
/**
* Get the number of items in the array
* @example
* ```ts
* const apples = ["red", "green", "yellow"];
*
* const count = $(apples).length().value;
* expect(count).toBe(3);
* ```
*/
length: () => Return<T["length"], typeof t>;
/**
* Rearrange the items in the array in accordance to the
* specified sorting function
* @param sort The sorting algorithm function to determine the correct order of elements
* @see Nested properties for various pre-made sorting algorithms.
* @from {@link Array `Array`}
* @example
* ```ts
* const unordered = [7, 2, 4, 6, 3, 5, 1];
*
* const ordered = $(unordered)
* .sort((a, b) => a - b)
* .value;
* expect(ordered).toMatchObject([1, 2, 3, 4, 5, 6, 7]);
* ```
*/
sort: ((
/**
* Predicate used to compare and determine the ordering
* between any 2 elements of the array
* @param a One element of the array to comapare
* @param b Another element of the array to compare against
* @param arr A view of the array being sorted
* @return A negative value if `a` should come before
* `b`, a positive value if `a` should come after `b`,
* and `0` if `a` and `b` are considered the same.
*/
callback: (
a: Item,
b: Item,
@@ -268,13 +385,33 @@ export interface Array extends Mixin.HKT {
) => Return<Unordered<T>, typeof t>) &
([Item] extends [string]
? {
alpha: () => Return<
/**
* @param via A callback to compute a
* representation of each element to use for
* sorting
*/
alpha: (
/**
* @param v The current element
* @return A string representation of `v`
*/
via?: (v: Item) => string,
) => Return<
Unordered<T>,
typeof t
>;
}
: {
/**
* @param via A callback to map each element
* of the array to a string that is used for
* sorting
*/
alpha: (
/**
* @param v The current element
* @return The string representation of `v`
*/
via: (v: Item) => string,
) => Return<
Unordered<T>,
@@ -305,8 +442,19 @@ export interface Array extends Mixin.HKT {
Unordered<T>,
typeof t
>;
}) &
HidePrototype;
}) & {
/**
* Sort alphabetically (A-Z) by Unicode code points
* @example
* const names = ["Johnny", "Anna", "Bart", "Xavier", "Java"];
*
* const sorted = $(names).sort.alpha().value;
* expect(sorted).toMatchObject([
* "Anna", "Bart", "Java", "Johnny", "Xavier"
* ])
*/
alpha: unknown;
} & HidePrototype;
reverse: () => Return<Reverse<T>, typeof t>;
}
: unknown
@@ -328,7 +476,9 @@ export const Array = Mixin<Array>((value, $, fluent) => {
assert(count >= 0);
if (count === 0) return fluent(value);
return fluent(
value.concat(...new globalThis.Array(count).fill(value)),
value.concat(
...new globalThis.Array<unknown>(count).fill(value),
),
);
};
@@ -388,9 +538,7 @@ export const Array = Mixin<Array>((value, $, fluent) => {
),
) => {
return fluent(
value.toSorted((a, b) =>
via(a)! < via(b)! ? -1 : 1,
),
value.toSorted((a, b) => (via(a) < via(b) ? -1 : 1)),
);
},
ascending: (
@@ -450,14 +598,17 @@ if (import.meta.vitest) {
});
test("extend()", () => {
const arr = [1, 2] as const;
const arr = [1, 2];
const copies = 2;
const expected: number[] = new globalThis.Array(copies + 1)
const expected: number[] = new globalThis.Array<number[]>(
copies + 1,
)
.fill(arr)
.flat(1);
expect($(arr).extend(copies).value).toMatchObject(expected);
expect($(arr).extend(0).value).toBe(arr);
});
test("map()", () => {
@@ -518,7 +669,8 @@ if (import.meta.vitest) {
i,
arr,
);
sum += arr[i]!;
sum += arr[i];
}
callback.mockClear();
@@ -526,7 +678,7 @@ if (import.meta.vitest) {
expect($(arr).reduce.right(0, callback).value).toBe(expected);
sum = 0;
for (let i = arr.length; i > 0; i--) {
for (let i: number = arr.length; i > 0; i--) {
expect(callback).toHaveBeenNthCalledWith(
arr.length - i + 1,
sum,
@@ -534,7 +686,7 @@ if (import.meta.vitest) {
i - 1,
arr,
);
sum += arr[i - 1]!;
sum += arr[i - 1];
}
});
@@ -580,4 +732,19 @@ if (import.meta.vitest) {
strings.toSorted(),
);
});
test("reverse()", () => {
const array = [1, 2, 3, 4, 5] as const;
expect($(array).reverse().value).toMatchObject(
array.toReversed(),
);
});
test("length()", () => {
const length = 3;
const array = new globalThis.Array<number>(length).fill(5);
expect($(array).length().value).toBe(length);
});
}
+6 -2
View File
@@ -10,13 +10,15 @@ export interface Base extends Mixin.HKT {
* modifying the value.
* @param callback The interospective callback
* @see `transform` to modify
* @from {@link Base `Base`}
* @example
* ```ts
* let x;
* const value = $(10).tap(v => { x = ++v }).value;
*
* expect(x).toBe(11);
* expect(value).toBe(10);
* @from {@link Base `Base`}
* ```
*/
tap(
callback: (value: Readonly<T>) => void,
@@ -26,12 +28,14 @@ export interface Base extends Mixin.HKT {
* `callback` using the outputted return value as a new value.
* A.K.A., _transform_ the current value using a callback
* @param callback The transformative callback
* @from {@link Base `Base`}
* @example
* ```ts
* const value = $("Hello")
* .transform(v => v.toUpperCase())
* .value;
* expect(value).toBe("HELLO");
* @from {@link Base `Base`}
* ```
*/
transform<U>(
callback: (value: T) => U,
-3
View File
@@ -1,3 +0,0 @@
export { Base } from "./base";
export { AsyncMixin as Async } from "./awaited";
export { Optional } from "./optional";
+297
View File
@@ -0,0 +1,297 @@
import { makeFluent } from "../base";
import type { HKT } from "../base/hkt";
import {
Mixin,
type Input,
type Props,
type Return,
} from "../base/mixin";
import { assertType } from "../internal";
import { applySwizzle, type Swizzle } from "./math/swizzle";
type Sign<T extends number> = T extends 0
? 0
: `${T}` extends `-${number}`
? -1
: 1;
export interface Math extends Mixin.HKT {
new: (t: HKT.T<this>) => Input<typeof t> extends infer T
? T extends number
? {
add: (other: number) => Return<number, typeof t>;
subtract: (
other: number,
) => Return<number, typeof t>;
multiply: (
factor: number,
) => Return<number, typeof t>;
pow: (
exponent: number,
) => Return<number, typeof t>;
sqrt: () => Return<number, typeof t>;
divide: (
divisor: number,
) => Return<number, typeof t>;
mod: (
divisor: number,
) => Return<number, typeof t>;
log: (base?: number) => Return<number, typeof t>;
isEqualApprox: (
other: number,
delta?: number,
) => Return<boolean, typeof t>;
floor: () => Return<number, typeof t>;
ceil: () => Return<number, typeof t>;
round: () => Return<number, typeof t>;
fround: () => Return<number, typeof t>;
abs: () => Return<number, typeof t>;
negate: () => Return<number, typeof t>;
sign: () => Return<Sign<T>, typeof t>;
min: (
...others: number[]
) => Return<number, typeof t>;
max: (
...others: number[]
) => Return<number, typeof t>;
clamp: (
min: number,
max: number,
) => Return<number, typeof t>;
snapped: (
multiple: number,
) => Return<number, typeof t>;
lerp: typeof t extends infer TProps extends Props
? (
b: number,
t: number,
) => Return<number, TProps>
: never;
sin: () => Return<number, typeof t>;
asin: () => Return<number, typeof t>;
cos: () => Return<number, typeof t>;
acos: () => Return<number, typeof t>;
tan: () => Return<number, typeof t>;
atan: () => Return<number, typeof t>;
atan2: (y: number) => Return<number, typeof t>;
sinh: () => Return<number, typeof t>;
asinh: () => Return<number, typeof t>;
cosh: () => Return<number, typeof t>;
acosh: () => Return<number, typeof t>;
tanh: () => Return<number, typeof t>;
atanh: () => Return<number, typeof t>;
to: {
radians: () => Return<number, typeof t>;
degrees: () => Return<number, typeof t>;
};
}
: T extends readonly number[]
? Swizzle<T, typeof t> & {
sum: () => Return<number, typeof t>;
}
: unknown
: never;
}
const M = globalThis.Math;
export const Math = Mixin<Math>((value, $, fluent) => {
if (typeof value === "number") {
$.add = (other: number) => fluent(value + other);
$.subtract = (other: number) => fluent(value - other);
$.multiply = (factor: number) => fluent(value * factor);
$.pow = (exponent: number) => fluent(M.pow(value, exponent));
$.sqrt = () => fluent(M.sqrt(value));
$.divide = (divisor: number) => fluent(value / divisor);
$.mod = (divisor: number) => fluent(value % divisor);
$.log = (base?: number) => {
if (base !== undefined)
return fluent(M.log(value) / M.log(base));
return fluent(M.log(value));
};
$.isEqualApprox = (
other: number,
delta: number = Number.EPSILON,
) => {
return fluent(M.abs(value - other) < delta);
};
$.floor = () => fluent(M.floor(value));
$.ceil = () => fluent(M.ceil(value));
$.round = () => fluent(M.round(value));
$.fround = () => fluent(M.fround(value));
$.abs = () => fluent(M.abs(value));
$.negate = () => fluent(-value);
$.sign = () => fluent(M.sign(value));
$.min = (...others: number[]) =>
fluent(M.min(value, ...others));
$.max = (...others: number[]) =>
fluent(M.max(value, ...others));
$.clamp = (min: number, max: number) =>
fluent(M.min(M.max(value, min), max));
$.snapped = (multiple: number) =>
fluent(M.round(value / multiple) * multiple);
$.lerp = (b: number, t: number) =>
fluent(value * (1 - t) + b * t);
$.sin = () => fluent(M.sin(value));
$.asin = () => fluent(M.asin(value));
$.cos = () => fluent(M.cos(value));
$.acos = () => fluent(M.acos(value));
$.tan = () => fluent(M.tan(value));
$.atan = () => fluent(M.atan(value));
$.atan2 = (y: number) => fluent(M.atan2(y, value));
$.sinh = () => fluent(M.sinh(value));
$.asinh = () => fluent(M.asinh(value));
$.cosh = () => fluent(M.cosh(value));
$.acosh = () => fluent(M.acosh(value));
$.tanh = () => fluent(M.tanh(value));
$.atanh = () => fluent(M.atanh(value));
const DEG_TO_RAD = M.PI / 180;
const RAD_TO_DEG = 180 / M.PI;
$.to ??= {};
assertType<object>($.to);
Object.assign($.to, {
radians: () => fluent(value * DEG_TO_RAD),
degrees: () => fluent(value * RAD_TO_DEG),
});
} else if (
Array.isArray(value) &&
value.every((x) => typeof x === "number")
) {
applySwizzle(value, $, fluent);
}
});
if (import.meta.vitest) {
const { test, expect } = import.meta.vitest;
const registry = [Math] as const;
const $ = makeFluent(registry);
const t = (f: string, a: number, ...args: unknown[]) => {
test(f + "()", () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
expect(($(a) as any)[f](...args.slice(0, -1)).value).toBe(
args[args.length - 1],
);
});
};
t("add", 4, 5, 4 + 5);
t("subtract", 7, 2, 7 - 2);
t("multiply", 3, 3, 3 * 3);
t("pow", 2, 4, 2 ** 4);
t("sqrt", 121, M.sqrt(121));
t("mod", 8, 3, 8 % 3);
test("log()", () => {
expect($(M.E).log().value).toBe(1);
expect($(1000).log(10).value).toBeCloseTo(M.log10(1000));
});
test("equalApprox()", () => {
const a = 0.1 + 0.2;
const target = 0.3;
expect($(a).isEqualApprox(target).value).toBe(true);
expect($(a).isEqualApprox(1, 1).value).toBe(true);
expect(
$(a).isEqualApprox(target + Number.EPSILON * 2).value,
).toBe(false);
});
t("floor", 1.7, 1);
t("ceil", 1.7, 2);
test("round()", () => {
expect($(1.3).round().value).toBe(1);
expect($(4.5).round().value).toBe(5);
});
t("fround", 6.45, M.fround(6.45));
test("abs()", () => {
const v = 4;
expect($(v).abs().value).toBe(v);
expect($(-v).abs().value).toBe(v);
});
test("negate()", () => {
const v = 7;
expect($(v).negate().value).toBe(-v);
expect($(v).negate().negate().value).toBe(v);
});
test("sign()", () => {
expect($(-4).sign().value).toBe(-1);
expect($(0).sign().value).toBe(0);
expect($(6).sign().value).toBe(1);
});
t("min", 3, 1, 2, 1);
t("max", 3, 1, 2, 3);
test("clamp()", () => {
const min = 1;
const max = 4;
const v = 3;
expect($(min - 4).clamp(min, max).value).toBe(min);
expect($(v).clamp(min, max).value).toBe(v);
expect($(max + 7).clamp(min, max).value).toBe(max);
});
t("snapped", 12.345, 0.1, 12.3);
test("lerp()", () => {
const a = 2;
const b = 4;
expect($(a).lerp(b, 0).value).toBe(a);
expect($(a).lerp(b, 0.5).value).toBe(3);
expect($(a).lerp(b, 1).value).toBe(b);
expect($(a).lerp(b, 2).value).toBe(b + (b - a));
expect($(a).lerp(b, -1).value).toBe(0);
});
t("sin", M.PI, M.sin(M.PI));
t("cos", M.PI, M.cos(M.PI));
t("acos", M.PI, M.acos(M.PI));
t("tan", M.PI, M.tan(M.PI));
t("atan", M.PI, M.atan(M.PI));
t("atan2", 10, M.PI, M.atan2(M.PI, 10));
t("sinh", M.PI, M.sinh(M.PI));
t("asinh", M.PI, M.asinh(M.PI));
t("cosh", M.PI, M.cosh(M.PI));
t("acosh", M.PI, M.acosh(M.PI));
t("tanh", M.PI, M.tanh(M.PI));
t("atanh", 0.5, M.atanh(0.5));
test(".to.radians()", () => {
const angle = 180;
expect($(angle).to.radians().value).toBe(M.PI);
});
test(".to.degrees()", () => {
const angle = M.PI;
expect($(angle).to.degrees().value).toBe(180);
});
}
+272
View File
@@ -0,0 +1,272 @@
import { makeFluent } from "../../base";
import type { Props, Return } from "../../base/mixin";
import { type MaxDepth } from "../../internal";
import { Math } from "../math";
type Axis = typeof axis;
type Vec<
T,
N extends number,
TOut extends T[] = [],
> = TOut["length"] extends N
? TOut
: TOut["length"] extends MaxDepth
? T[]
: Vec<T, N, [...TOut, T]>;
type Inc<N extends number | undefined> = N extends number
? [...Vec<null, N>, null]["length"]
: 0;
type Dec<N extends number> =
Vec<null, N> extends [...infer Rest extends null[], null]
? Rest["length"]
: -1;
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<Current> extends infer TInc
? TInc extends N
? Next<N, Rest, [...TOut, undefined]>
: [...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<Rest, `${TOut}${Axis[Current]}`>
: never;
type IsAscending<
State extends (number | undefined)[],
Prev extends number | undefined = undefined,
> = State extends readonly [
infer Current extends number,
...infer Rest extends number[],
]
? Current extends Inc<Prev>
? IsAscending<Rest, Inc<Prev>>
: false
: State extends readonly []
? true
: false;
type Pretty<T> = { [K in keyof T]: T[K] };
type SwizzlePermutations<
N extends number,
State extends Vec<number | undefined, N> = Vec<undefined, N>,
TOut extends Record<string, readonly number[]> = Record<
never,
never
>,
> =
Next<N, State> extends infer TNext
? TNext extends Vec<number | undefined, N>
? SwizzlePermutations<
N,
TNext,
TOut &
(IsAscending<State> extends true
? unknown
: Record<Key<State>, State>)
>
: Pretty<TOut & Record<Key<State>, 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<T, Rest, [...TOut, T[Current]]>
: 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<T[TOut["length"]], t>
> extends infer TAcc
? Primative<T, t, Rest, TAcc, [...TOut, TAcc]>
: never
: TOut;
type SwizzleCache = [
{ x: [0] },
SwizzlePermutations<2>,
SwizzlePermutations<3>,
SwizzlePermutations<4>,
];
export type Swizzle<
T extends readonly unknown[],
t extends Props,
> = number extends T["length"]
? unknown
: (
Dec<T["length"]> extends infer K extends 0 | 1 | 2 | 3
? { k: K; c: SwizzleCache[K] }
: SwizzleCache extends [...unknown[], infer Last]
? { k: Dec<SwizzleCache["length"]>; 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, C[K]>,
t
>;
},
Axis[number]
> &
Primative<T, t>[K]
: never;
const axis = ["x", "y", "z", "w"] as const;
export function applySwizzle(
value: number[],
$: object,
fluent: (value: unknown) => never,
) {
const length = value.length;
const state = new Array<number>(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;
}
let reachedEmpty = false;
for (const item of state) {
if (item === -1) {
reachedEmpty = true;
} else if (reachedEmpty) {
continue main;
}
}
const key = state
.filter((x) => x !== -1)
.map((x) => axis[x])
.join("");
if (key.length <= 1 || key in $) 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..?
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, expect } = import.meta.vitest;
const registry = [Math] as const;
const $ = makeFluent(registry);
test("Swizzling (.xx, .xy, etc.)", () => {
const x = 5 as const;
const y = 7 as const;
const vec = [x, y] as const;
expect($(vec).xx.value).toMatchObject([x, x]);
expect($(vec).yx.value).toMatchObject([y, x]);
expect($(vec).yy.value).toMatchObject([y, y]);
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]);
});
test(".x", () => {
const x = 5 as const;
const arr = [x] as const;
expect($(arr).x.value).toBe(x);
});
test(".y", () => {
const y = 2 as const;
const arr = [y, y] as const;
expect($(arr).y.value).toBe(y);
});
test(".z", () => {
const z = 9 as const;
const arr = [z, z, z] as const;
expect($(arr).z.value).toBe(z);
});
test(".w", () => {
const w = 0 as const;
const arr = [w, w, w, w] as const;
expect($(arr).w.value).toBe(w);
});
}
+19 -12
View File
@@ -85,7 +85,7 @@ export interface Optional extends Mixin.HKT {
* expect(b).toBe(10);
* @from {@link Optional `Optional`}
*/
or: (<U>(
or: (<const U>(
fallback: U,
) => Return<Some<T> | U, typeof t>) & {
/**
@@ -94,11 +94,13 @@ export interface Optional extends Mixin.HKT {
* only computes the fallback if the value is `null` or
* `undefined`
* @param callback
* @from {@link Optional `Optional`}
* @example
* ```ts
* const none = () => null as number | null;
* const some = () => 8 as number | null;
*
* const fallback = () => 22; // mocked
* const fallback = vi.fn(() => 22); // mocked
*
* const a = $(none()).or.else(fallback).value;
* expect(a).toBe(22);
@@ -107,7 +109,7 @@ export interface Optional extends Mixin.HKT {
* expect(b).toBe(8);
*
* expect(fallback).toHaveBeenCalledOnce()
* @from {@link Optional `Optional`}
* ```
*/
else: <U>(
callback: (v: None<T>) => U,
@@ -116,7 +118,10 @@ export interface Optional extends Mixin.HKT {
/**
* Assert that value is not `null` or `undefined`
* @param msg Reasoning to attach to the `AssertionError`
* @see `.assert.none()` for the inverse assertion
* @from {@link Optional `Optional`}
* @example
* ```ts
* const array = [1, 2, 3];
* const element: number | undefined = array[1];
*
@@ -126,8 +131,7 @@ export interface Optional extends Mixin.HKT {
* expect(() => {
* $(array[6]).assert()
* }).toThrow()
* @see `.assert.none()` for the inverse assertion
* @from {@link Optional `Optional`}
* ```
*/
assert: ((
msg?: string,
@@ -135,7 +139,9 @@ export interface Optional extends Mixin.HKT {
/**
* Assert that the value is either `null` or `undefined`
* @param msg Reasoning to attach to the `AssertionError`
* @from {@link Optional `Optional`}
* @example
* ```ts
* const array = [1, 2, 3];
* const element: number | undefined = array[5];
*
@@ -145,6 +151,7 @@ export interface Optional extends Mixin.HKT {
* expect(() => {
* $(array[1]).assert.none()
* }).toThrow()
* ```
*/
none: (
msg?: string,
@@ -170,13 +177,13 @@ export const Optional = Mixin<Optional>((value, $, fluent) => {
},
});
$.assert = () => {
return fluent(value);
$.assert = (msg?: string) => {
assert(false, msg);
};
assertType<object>($.assert);
Object.assign($.assert, {
none: (msg?: string) => {
assert(false, msg);
none: () => {
return fluent(value);
},
});
} else {
@@ -195,13 +202,13 @@ export const Optional = Mixin<Optional>((value, $, fluent) => {
},
});
$.assert = (msg?: string) => {
assert(false, msg);
$.assert = () => {
return fluent(value);
};
assertType<object>($.assert);
Object.assign($.assert, {
none: (msg?: string) => {
return fluent(value);
assert(false, msg);
},
});
}
+5 -1
View File
@@ -1,6 +1,9 @@
import { Mixin, type Instansiate } from "./base/mixin";
import { Async, Base, Optional } from "./mixin";
import { Array } from "./mixin/array";
import { AsyncMixin as Async } from "./mixin/awaited";
import { Base } from "./mixin/base";
import { Math } from "./mixin/math";
import { Optional } from "./mixin/optional";
export type Registry = readonly Mixin[];
@@ -9,6 +12,7 @@ export const DEFAULT_REGISTRY = [
Array,
Base,
Optional,
Math,
] as const satisfies Registry;
export type DefaultRegistry = typeof DEFAULT_REGISTRY;