feat: progress
This commit is contained in:
+15
-2
@@ -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
|
||||
|
||||
@@ -2,3 +2,4 @@ import { makeFluent } from "./base";
|
||||
import { DEFAULT_REGISTRY } from "./registry";
|
||||
|
||||
export const fluent = makeFluent(DEFAULT_REGISTRY);
|
||||
export const $ = fluent;
|
||||
|
||||
+1
-1
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export { Base } from "./base";
|
||||
export { AsyncMixin as Async } from "./awaited";
|
||||
export { Optional } from "./optional";
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user