feat: more math & refactor

This commit is contained in:
2026-07-10 18:28:43 +02:00
parent b323482c22
commit ba5595a783
19 changed files with 2055 additions and 1491 deletions
+1
View File
@@ -46,6 +46,7 @@ export default defineConfig([
},
],
"@typescript-eslint/no-namespace": "off",
"@typescript-eslint/no-empty-object-type": "off",
},
languageOptions: {
parserOptions: {
+1 -1
View File
@@ -21,7 +21,7 @@
"jiti": "^2.7.0",
"madge": "^8.0.0",
"prettier": "^3.8.4",
"typescript": "~6.0.2",
"typescript": "6",
"typescript-eslint": "^8.59.2",
"vitest": "^4.1.9"
}
+1 -1
View File
@@ -39,7 +39,7 @@ importers:
specifier: ^3.8.4
version: 3.8.4
typescript:
specifier: ~6.0.2
specifier: '6'
version: 6.0.3
typescript-eslint:
specifier: ^8.59.2
+4 -3
View File
@@ -1,18 +1,19 @@
export interface HKT<In = unknown, Out = unknown> {
readonly _meta: In;
readonly _in: In;
readonly _out: Out;
readonly _t: unknown;
new: (t: never) => Out;
}
type Param<
This extends HKT,
U = This["_meta"],
U = This["_in"],
> = This["_t"] extends infer T ? (T extends U ? T : U) : never;
export namespace HKT {
export type T<
This extends HKT,
T = This extends { _meta: infer I } ? I : unknown,
T = This["_in"], //This extends { _in: infer I } ? I : unknown,
> = Param<This, T>;
export type Apply<T extends HKT, t extends T["_t"]> = ReturnType<
(T & { _t: t })["new"]
+1 -2
View File
@@ -33,8 +33,7 @@ export function makeFluent<const Reg extends Registry>(
const f = { value } as unknown as Fluent<T, Reg>;
for (const mixin of registry) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
mixin.fn(value, f, fluent as (value: unknown) => never);
mixin.fn(value, f, fluent);
}
return f;
+26 -10
View File
@@ -1,6 +1,7 @@
import type { Fluent } from ".";
import { never } from "../internal";
import type { Registry } from "../registry";
import type { RecPartial } from "../utility";
import type { HKT } from "./hkt";
export interface Props {
@@ -8,28 +9,43 @@ export interface Props {
readonly meta: { registry: Registry };
}
type MixinHKT = HKT<Props>;
type MixinFn = (
value: unknown,
fluent: Record<PropertyKey, unknown>,
callback: (value: unknown) => never,
type MixinHKT<I> = HKT<Props, RecPartial<I>>;
type MixinFn<I> = <T>(
value: T,
fluent: object & RecPartial<I>,
callback: <U>(value: U) => Fluent<U, Registry>,
) => void;
export interface Mixin<T extends MixinHKT = MixinHKT> {
export interface Mixin<
I = unknown,
T extends MixinHKT<I> = MixinHKT<I>,
> {
interface: I;
hkt: T;
fn: MixinFn;
fn: MixinFn<I>;
}
export function Mixin<T extends MixinHKT>(fn: MixinFn): Mixin<T> {
export function Mixin<I, T extends MixinHKT<I>>(
fn: MixinFn<I>,
): Mixin<I, T> {
return {
interface: never,
hkt: never,
fn,
};
}
export namespace Mixin {
export type HKT = MixinHKT;
export type Function = MixinFn;
export type HKT<I> = MixinHKT<I>;
export function partial<I, T>(
callback: (
value: T,
fluent: object & RecPartial<I>,
callback: <P>(value: P) => Fluent<P, Registry>,
) => void,
): typeof callback {
return callback;
}
}
export declare const shim: unique symbol;
+8 -2
View File
@@ -7,6 +7,10 @@ export interface Identity extends HKT {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
export const never = undefined as never;
export function isArray(v: unknown): v is unknown[] {
return typeof v === "object" && v !== null && Array.isArray(v);
}
export class AssertionError extends Error {
public constructor(msg?: string) {
super(msg);
@@ -26,10 +30,12 @@ export function assert(
);
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters, @typescript-eslint/no-unused-vars
export function assertType<T>(_v: unknown): asserts _v is T {
export namespace assert {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function type<T>(_v: unknown): asserts _v is NoInfer<T> {
/* empty */
}
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
type FunctionProto = Function;
+83 -87
View File
@@ -1,7 +1,12 @@
import { makeFluent } from "../base";
import type { HKT } from "../base/hkt";
import { Mixin, type Input, type Return } from "../base/mixin";
import { assert, assertType, type HidePrototype } from "../internal";
import {
Mixin,
type Input,
type Props,
type Return,
} from "../base/mixin";
import { assert, type HidePrototype } from "../internal";
import type { MaxDepth, At } from "../utility";
type IterArgs<T extends readonly unknown[] = unknown[]> = [
@@ -50,10 +55,10 @@ function isArray(v: unknown): v is readonly unknown[] {
return typeof v === "object" && globalThis.Array.isArray(v);
}
export interface Array extends Mixin.HKT {
new: (t: HKT.T<this>) => Input<typeof t> extends infer T
? T extends readonly (infer Item)[]
? {
interface IArray<
T extends readonly unknown[] = readonly unknown[],
t extends Props = Props,
> {
/**
* Index the array using the specified zero-based index.
* Negative indexes start at the end of the array.
@@ -78,7 +83,7 @@ export interface Array extends Mixin.HKT {
at: <const K extends keyof T & number>(
/** Zero-based index */
index: K,
) => Return<At<T, K>, typeof t>;
) => Return<At<T, K>, t>;
/**
* Interospect each item of the array using the specified
* callback
@@ -103,7 +108,7 @@ export interface Array extends Mixin.HKT {
* @param array The array being iterated
*/
callback: (...args: IterArgs<T>) => void,
) => Return<T, typeof t>;
) => Return<T, t>;
/**
* Transform each item of the array using the specified callback
* @param callback Predicate to compute each new value with
@@ -125,7 +130,7 @@ export interface Array extends Mixin.HKT {
* @return The new value to use for this element
*/
callback: (...args: IterArgs<T>) => U,
) => Return<Map<T, U>, typeof t>;
) => Return<Map<T, U>, t>;
/**
* Extend the array by repeating its current contents n times
* @param count The amount of repetitions to extend the array by
@@ -141,7 +146,7 @@ export interface Array extends Mixin.HKT {
extend: <const N extends number>(
/** The amount of repetitions to extend the array by */
count: N,
) => Return<Repeat<T, N>, typeof t>;
) => Return<Repeat<T, N>, t>;
/**
* Filter the array to contain only items satisfying the
* specified callback conditional
@@ -164,7 +169,7 @@ export interface Array extends Mixin.HKT {
* @return `true` if the value should be kept and `false` if it should not
*/
callback: (...args: IterArgs<T>) => boolean,
) => Return<Unordered<T>, typeof t>) & {
) => Return<Unordered<T>, t>) & {
/**
* Filter the array to only contain items which are not
* `null` or `undefined`.
@@ -183,15 +188,9 @@ export interface Array extends Mixin.HKT {
*/
some: () => Return<
T extends unknown[]
? Exclude<
T[number],
null | undefined
>[]
: readonly Exclude<
T[number],
null | undefined
>[],
typeof t
? Exclude<T[number], null | undefined>[]
: readonly Exclude<T[number], null | undefined>[],
t
>;
} & HidePrototype;
/**
@@ -221,11 +220,8 @@ export interface Array extends Mixin.HKT {
* @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>) & {
callback: (value: U, ...args: IterArgs<T>) => U,
) => Return<U, t>) & {
/**
* Collapse the array using the specified accumulator function, starting from the right.
* @param initial The initial value of the accumulator
@@ -253,11 +249,8 @@ export interface Array extends Mixin.HKT {
* @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>;
callback: (value: U, ...args: IterArgs<T>) => U,
) => Return<U, t>;
} & HidePrototype;
/**
* Get the number of items in the array
@@ -269,7 +262,7 @@ export interface Array extends Mixin.HKT {
* expect(count).toBe(3);
* ```
*/
length: () => Return<T["length"], typeof t>;
length: () => Return<T["length"], t>;
/**
* Rearrange the items in the array in accordance to the
@@ -299,12 +292,12 @@ export interface Array extends Mixin.HKT {
* and `0` if `a` and `b` are considered the same.
*/
callback: (
a: Item,
b: Item,
a: T[number],
b: T[number],
arr: Readonly<T>,
) => number,
) => Return<Unordered<T>, typeof t>) &
([Item] extends [string]
) => Return<Unordered<T>, t>) &
([T[number]] extends [string]
? {
/**
* @param via A callback to compute a
@@ -316,11 +309,8 @@ export interface Array extends Mixin.HKT {
* @param v The current element
* @return A string representation of `v`
*/
via?: (v: Item) => string,
) => Return<
Unordered<T>,
typeof t
>;
via?: (v: T[number]) => string,
) => Return<Unordered<T>, t>;
}
: {
/**
@@ -333,36 +323,21 @@ export interface Array extends Mixin.HKT {
* @param v The current element
* @return The string representation of `v`
*/
via: (v: Item) => string,
) => Return<
Unordered<T>,
typeof t
>;
via: (v: T[number]) => string,
) => Return<Unordered<T>, t>;
}) &
([Item] extends [number]
([T[number]] extends [number]
? {
ascending: () => Return<
Unordered<T>,
typeof t
>;
descending: () => Return<
Unordered<T>,
typeof t
>;
ascending: () => Return<Unordered<T>, t>;
descending: () => Return<Unordered<T>, t>;
}
: {
ascending: (
via: (v: Item) => number,
) => Return<
Unordered<T>,
typeof t
>;
via: (v: T[number]) => number,
) => Return<Unordered<T>, t>;
descending: (
via: (v: Item) => number,
) => Return<
Unordered<T>,
typeof t
>;
via: (v: T[number]) => number,
) => Return<Unordered<T>, t>;
}) & {
/**
* Sort alphabetically (A-Z) by Unicode code points
@@ -376,12 +351,19 @@ export interface Array extends Mixin.HKT {
*/
alpha: unknown;
} & HidePrototype;
reverse: () => Return<Reverse<T>, typeof t>;
reverse: () => Return<Reverse<T>, t>;
}
: unknown
export interface Array extends Mixin.HKT<IArray> {
new: (
t: HKT.T<this>,
) => Input<typeof t> extends infer T
? T extends readonly unknown[]
? IArray<T, typeof t>
: {}
: never;
}
export const Array = Mixin<Array>((value, $, fluent) => {
export const Array = Mixin<IArray, Array>((value, $, fluent) => {
if (!isArray(value)) return;
$.at = (index: number) => {
@@ -407,39 +389,47 @@ export const Array = Mixin<Array>((value, $, fluent) => {
return fluent(value.map(callback));
};
$.filter = (callback: (...args: IterArgs) => boolean) => {
$.filter = Object.assign(
(callback: (...args: IterArgs) => boolean) => {
return fluent(value.filter(callback));
};
assertType<object>($.filter);
Object.assign($.filter, {
},
{
some: () => {
return fluent(
value.filter((v) => v !== null && v !== undefined),
value.filter(
(v) => v !== null && v !== undefined,
),
);
},
});
},
);
$.reduce = (
$.reduce = Object.assign(
(
initial: unknown,
callback: (value: unknown, ...args: IterArgs) => unknown,
) => {
return fluent(value.reduce(callback, initial));
};
assertType<object>($.reduce);
Object.assign($.reduce, {
},
{
right: (
initial: unknown,
callback: (value: unknown, ...args: IterArgs) => unknown,
callback: (
value: unknown,
...args: IterArgs
) => unknown,
) => {
return fluent(value.reduceRight(callback, initial));
},
});
},
);
$.length = () => {
return fluent(value.length);
};
$.sort = (
$.sort = Object.assign(
(
callback: (
a: unknown,
b: unknown,
@@ -449,9 +439,8 @@ export const Array = Mixin<Array>((value, $, fluent) => {
return fluent(
value.toSorted((a, b) => callback(a, b, value)),
);
};
assertType<object>($.sort);
Object.assign($.sort, {
},
{
alpha: (
via: (v: unknown) => string = (v) => (
assert(typeof v === "string"),
@@ -459,7 +448,9 @@ 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: (
@@ -468,7 +459,9 @@ export const Array = Mixin<Array>((value, $, fluent) => {
v
),
) => {
return fluent(value.toSorted((a, b) => via(a) - via(b)));
return fluent(
value.toSorted((a, b) => via(a) - via(b)),
);
},
descending: (
via: (v: unknown) => number = (v) => (
@@ -476,9 +469,12 @@ export const Array = Mixin<Array>((value, $, fluent) => {
v
),
) => {
return fluent(value.toSorted((a, b) => via(b) - via(a)));
return fluent(
value.toSorted((a, b) => via(b) - via(a)),
);
},
});
},
);
$.reverse = () => {
return fluent(value.toReversed());
+33 -22
View File
@@ -1,7 +1,13 @@
import { makeFluent } from "../base";
import type { HKT } from "../base/hkt";
import { Mixin, shim, type Input, type Return } from "../base/mixin";
import { assert, assertType, never } from "../internal";
import {
Mixin,
shim,
type Input,
type Props,
type Return,
} from "../base/mixin";
import { assert, never } from "../internal";
import { Base } from "./base";
interface AwaitedIdentitity extends HKT {
@@ -23,20 +29,23 @@ class Awaited<T extends Promise<unknown>> {
}
}
export interface AsyncMixin extends Mixin.HKT {
new: (t: HKT.T<this>) => Input<typeof t> extends infer T
? T extends Promise<unknown>
? {
readonly awaited: Return<Awaited<T>, typeof t>;
interface IAsync<
T extends Promise<unknown> = Promise<unknown>,
t extends Props = Props,
> {
readonly awaited: Return<Awaited<T>, t>;
then: <U>(
callback: (
value: T extends Promise<infer T>
? T
: never,
) => U,
) => Return<Promise<U>, typeof t>;
fn: (value: T extends Promise<infer O> ? O : never) => U,
) => Return<Promise<U>, t>;
}
: unknown
export interface AsyncMixin extends Mixin.HKT<IAsync> {
new: (
t: HKT.T<this>,
) => Input<typeof t> extends infer T
? T extends Promise<unknown>
? IAsync<T, typeof t>
: {}
: never;
}
@@ -45,7 +54,8 @@ interface BaseFluent<T> {
[K: PropertyKey]: unknown;
}
export const AsyncMixin = Mixin<AsyncMixin>((value, $, fluent) => {
export const AsyncMixin = Mixin<IAsync, AsyncMixin>(
(value, $, fluent) => {
if (!(value instanceof Promise)) return;
$.then = (callback: (value: unknown) => unknown) => {
@@ -55,8 +65,8 @@ export const AsyncMixin = Mixin<AsyncMixin>((value, $, fluent) => {
Object.defineProperty($, "awaited", {
enumerable: true,
get() {
let v: Promise<BaseFluent<unknown>> = value.then((v) =>
fluent(v),
let v: Promise<BaseFluent<unknown>> = value.then(
(v) => fluent(v),
);
const path: PropertyKey[] = [];
@@ -77,15 +87,15 @@ export const AsyncMixin = Mixin<AsyncMixin>((value, $, fluent) => {
obj !== null &&
node in obj,
);
assertType<Record<PropertyKey, unknown>>(
obj,
);
assert.type<
Record<PropertyKey, unknown>
>(obj);
obj = obj[node];
}
assert(typeof obj === "function");
assertType<
assert.type<
(
...args: unknown[]
) => BaseFluent<unknown>
@@ -100,7 +110,8 @@ export const AsyncMixin = Mixin<AsyncMixin>((value, $, fluent) => {
return proxy;
},
});
});
},
);
if (import.meta.vitest) {
const { test, expect } = import.meta.vitest;
+51 -13
View File
@@ -1,10 +1,13 @@
import { makeFluent } from "../base";
import type { HKT } from "../base/hkt";
import { Mixin, type Input, type Return } from "../base/mixin";
import {
Mixin,
type Input,
type Props,
type Return,
} from "../base/mixin";
export interface Base extends Mixin.HKT {
new: (t: HKT.T<this>) => Input<typeof t> extends infer T
? {
interface IBase<T = unknown, t extends Props = Props> {
/**
* Interospect value using the specified `callback` without
* modifying the value.
@@ -20,9 +23,7 @@ export interface Base extends Mixin.HKT {
* expect(value).toBe(10);
* ```
*/
tap(
callback: (value: Readonly<T>) => void,
): Return<T, typeof t>;
tap: (callback: (value: Readonly<T>) => void) => Return<T, t>;
/**
* Put value through or pipe value through the specified
* `callback` using the outputted return value as a new value.
@@ -37,14 +38,34 @@ export interface Base extends Mixin.HKT {
* expect(value).toBe("HELLO");
* ```
*/
transform<U>(
callback: (value: T) => U,
): Return<U, typeof t>;
}
: never;
transform: <U>(callback: (value: T) => U) => Return<U, t>;
/**
* Set value to a fallback if the specified callback returns `false`
* @param callback Guarding callback returning `true` to keep the value
* @param fallback The fallback value, `null` by default
* @from {@link Base `Base`}
* @example
* const isObject = (value: unknown) => typeof value === 'object';
*
* const a = $({ version: '1.0.0' }).where(isObject).value;
* const b = $(42).where(isObject).value;
* const c = $(42).where(isObject, { version: '0.1.0' }).value;
*
* expect(a).toMatchObject({ version: '1.0.0' });
* expect(b).toBe(null);
* expect(c).toMatchObject({ version: '0.1.0' });
*/
where: <const U = null>(
callback: (v: T) => boolean,
fallback?: U,
) => Return<T | U, t>;
}
export const Base = Mixin<Base>((value, $, fluent) => {
export interface Base extends Mixin.HKT<IBase> {
new: (t: HKT.T<this>) => IBase<Input<typeof t>, typeof t>;
}
export const Base = Mixin<IBase, Base>((value, $, fluent) => {
$.tap = (callback: (value: unknown) => void) => {
callback(value);
return fluent(value);
@@ -53,6 +74,11 @@ export const Base = Mixin<Base>((value, $, fluent) => {
$.transform = (callback: (value: unknown) => unknown) => {
return fluent(callback(value));
};
$.where = (
callback: (value: unknown) => boolean,
fallback: unknown = null,
) => fluent(callback(value) ? value : fallback);
});
if (import.meta.vitest) {
@@ -83,4 +109,16 @@ if (import.meta.vitest) {
$(value).transform(increment).transform(increment).value,
).toBe(increment(increment(value)));
});
test("where()", () => {
const even = 4 as const;
const odd = 7 as const;
const isEven = (v: number) => v % 2 === 0;
expect($(even).where(isEven).value).toBe(even);
expect($(odd).where(isEven).value).toBe(null);
expect($(odd).where(isEven, -1).value).toBe(-1);
});
}
+29 -712
View File
File diff suppressed because it is too large Load Diff
+195
View File
@@ -0,0 +1,195 @@
import type { Inc, Vec } from "../../utility";
export type Floor<T extends number> =
`${T}` extends `${infer N extends number}.${number}` ? N : T;
export type Ceil<T extends number> =
`${T}` extends `${infer N extends number}.${number}` ? Inc<N> : T;
export type Round<T extends number> =
`${T}` extends `${infer N extends number}.${0 | 1 | 2 | 3 | 4}${number | ""}`
? N
: Ceil<T>;
export type Sign<T extends number> = T extends 0
? 0
: `${T}` extends `-${number}`
? -1
: 1;
export type Abs<T extends number> =
`${T}` extends `-${infer N extends number}` ? N : T;
export type Negate<T extends number> = T extends 0
? 0
: `${T}` extends `-${infer N extends number}`
? N
: `-${T}` extends `${infer N extends number}`
? N
: number;
type Fn<
N extends number = typeof Infinity,
Max extends boolean = false,
> = (
...args: [...Vec<never, N>, ...(Max extends true ? [] : never[])]
) => unknown;
interface IBase {
/**
* `x + y`
* @param other The second term of the addition (`y`)
*/
add: Fn<1>;
/**
* `x - y`
* @param other The second term of the subtraction (`y`)
*/
subtract: Fn<1>;
/**
* `x * y`
* @param factor Factor term of the multiplication (`y`)
*/
multiply: Fn<1>;
/**
* `x ** y`
* @param exponent Exponent term of the power (`y`)
*/
pow: Fn<1, true>;
/**
* `Math.sqrt(x)` (or `Math.log2(x)`)
*/
sqrt: Fn<0, true>;
/**
* `x / y`
* @param divisor Divisor term of the division (`y`)
*/
divide: Fn<1, true>;
/**
* `x mod y`, not to be confused with `x % y` (remainder operation)
* @param divisor Divisor term of the modulo operation (`y`)
* @see `.rem()` for the remainder operation
*/
mod: Fn<1, true>;
/**
* `x % y` remainder operation, not to be confused with `x mod y` (modulo)
* @param divisor Divisor term of the remainder operation (`y`)
* @see `.mod()` for the true modulo operation
*/
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
*/
log: Fn<0, true>;
/**
* `Math.floor(x)`
*/
floor: Fn<0, true>;
/**
* `Math.ceil(x)`
*/
ceil: Fn<0, true>;
/**
* `Math.round(x)` or `Math.round(x / multiple) * multiple` to round to the
* specified multiple
* @param multiple Multiple to round to, 1 if unspecified
*/
round: Fn<1, true>;
/**
* `Math.fround(x)`, round to the nearest 32-bit float
* approximation of value
*/
fround: Fn<0, true>;
/**
* `Math.f16round(x)`, round to the nearest 16-bit float
* approximation of value
*/
ffround: Fn<0, true>;
/**
* `Math.abs(x)`, calculate the absolute value (distance
* from 0).
*/
abs: Fn<0, true>;
/**
* `Math.sign(x)`, returns the sign (1 or -1) of the value or zero
*/
sign: Fn<0, true>;
/**
* `-x` or alternatively `x * -1`
*/
negate: Fn<0, true>;
/**
* `Math.min(x, ...values)`
* @param values Other canidates
*/
min: Fn<0>;
/**
* `Math.min(x, ...values)`
* @param values Other canidates
*/
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
*/
clamp: Fn<2, true>;
/**
* Clamps value into the range [0.0, 1.0]
* @see `.clamp()` for a customizable range
*/
saturate: Fn<0, true>;
/**
* Calculate the value at the specified point `t` on the line from the
* current value (`a`) corresponding to 0.0 to the target value (`b`)
* corresponding to 1.0. Linear interpolation from `a` to `b` at `t`.
*
* `t` is not constrained or clamped to the range [0.0, 1.0], values beyond
* that range extrapolate linearly beyond `a` and `b`.
*
* Equivalent to `lerp(a, b, t)`, `a * (1 - t) + b * t`, and `a + (b - a) *
* t`
* @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`)
*/
lerp: Fn<2, true>;
/**
* Normalize the value to the interpolation factor between the specified `a`
* and `b` range. This effectively computes the interpolation factor (`t`)
* of the linear interpolation `lerp(a, b, t)`.
*
* If the value is outside the bounds of the specified range, it will not be
* constrained or clamped to the range [0.0, 1.0].
*
* Equivalent to a range normalization or `(v - a) / (b - a)`
* @param a The 0.0 point of the range
* @param b The 1.0 point of the range
* @see `.lerp()` for the inverse operation
*/
inverseLerp: Fn<2, true>;
}
export type Base<
I extends IBase,
OmitDocKeys extends keyof IBase = never,
> = Omit<{ [K in keyof IBase]: unknown }, OmitDocKeys> & I;
export namespace Base {
export interface Interface extends IBase {}
}
export function log(x: number, base: number) {
if (base === 2) return Math.log2(x);
if (base === 10) return Math.log10(x);
if (base === Math.E) return Math.log(x);
return Math.log(x) / Math.log(base);
}
export function clamp(x: number, min: number, max: number) {
return Math.min(Math.max(x, min), max);
}
+277
View File
@@ -0,0 +1,277 @@
import { makeFluent } from "../../base";
import type { HKT } from "../../base/hkt";
import { Mixin, type Props, type Return } from "../../base/mixin";
import { assert, type HidePrototype } from "../../internal";
import type { Apply, Vec } from "../../utility";
import { Math as M } from "../math";
import {
clamp,
log,
type Abs,
type Base,
type Ceil,
type Floor,
type Negate,
type Round,
type Sign,
} from "./base";
type Arg<T extends readonly number[]> = Vec<number, T["length"]>;
interface FloorHkt extends HKT<number, number> {
new: (t: HKT.T<this>) => Floor<typeof t>;
}
interface CeilHkt extends HKT<number, number> {
new: (t: HKT.T<this>) => Ceil<typeof t>;
}
interface RoundHkt extends HKT<number, number> {
new: (t: HKT.T<this>) => Round<typeof t>;
}
interface AbsHkt extends HKT<number, number> {
new: (t: HKT.T<this>) => Abs<typeof t>;
}
interface SignHkt extends HKT<number, number> {
new: (t: HKT.T<this>) => Sign<typeof t>;
}
interface NegateHkt extends HKT<number, number> {
new: (t: HKT.T<this>) => Negate<typeof t>;
}
namespace IList {
export type BaseImpl<
T extends readonly number[],
t extends Props,
> = Base<{
add: (
other: number extends T["length"]
? number
: number | Arg<T>,
) => Return<Arg<T>, t>;
subtract: (
other: number extends T["length"]
? number
: number | Arg<T>,
) => Return<Arg<T>, t>;
multiply: (factor: number) => Return<Arg<T>, t>;
pow: (exponent: number) => Return<Arg<T>, t>;
sqrt: () => Return<Arg<T>, t>;
divide: (divisor: number) => Return<Arg<T>, t>;
mod: (divisor: number) => Return<Arg<T>, t>;
rem: (divisor: number) => Return<Arg<T>, t>;
log: (base?: number) => Return<Arg<T>, t>;
floor: () => Return<Apply<FloorHkt, T>, t>;
ceil: () => Return<Apply<CeilHkt, T>, t>;
round: <N extends readonly [number] | readonly []>(
...multiple: N
) => Return<
N extends readonly [number] ? Arg<T> : Apply<RoundHkt, T>,
t
>;
fround: () => Return<Arg<T>, t>;
ffround: () => Return<Arg<T>, t>;
abs: () => Return<Apply<AbsHkt, T>, t>;
sign: () => Return<Apply<SignHkt, T>, t>;
negate: () => Return<Apply<NegateHkt, T>, t>;
min: <U extends number[]>(
...others: U
) => Return<T[number] | U[number], t>;
max: <U extends number[]>(
...others: U
) => Return<T[number] | U[number], t>;
clamp: <Min extends number, Max extends number>(
min: Min,
max: Max,
) => Return<Apply<Apply.Union<Min | Max>, T>, t>;
saturate: () => Return<Arg<T>, t>;
lerp: (b: Arg<T>, t: number) => Return<Arg<T>, t>;
inverseLerp: (a: Arg<T>, b: Arg<T>) => Return<Arg<T>, t>;
}>;
}
export type IList<
T extends readonly number[] = readonly number[],
t extends Props = Props,
> = T extends readonly [] | readonly [number]
? {}
: IList.BaseImpl<T, t> & {
sum: () => Return<number, t>;
product: () => Return<number, t>;
findMin: () => Return<keyof T, t>;
findMax: () => Return<keyof T, t>;
average: () => Return<number, t>;
median: () => Return<number, t>;
mode: () => Return<number[], t>;
range: () => Return<number, t>;
variance: (() => Return<number, t>) & {
sample: () => Return<number, t>;
} & HidePrototype;
deviation: (() => Return<number, t>) & {
sample: () => Return<number, t>;
} & HidePrototype;
};
export const applyList = Mixin.partial<IList, readonly number[]>(
(value, $, fluent) => {
$.add = (other: number | number[]) => {
if (typeof other === "object") {
assert(
value.length === other.length,
"List.add() parameters must be of equal length",
);
return fluent(value.map((x, idx) => x + other[idx]));
}
return fluent(value.map((x) => x + other));
};
$.subtract = (other: number | number[]) => {
if (typeof other === "object") {
assert(
value.length === other.length,
"List.subtract() parameters must be of equal length",
);
return fluent(value.map((x, idx) => x - other[idx]));
}
return fluent(value.map((x) => x - other));
};
$.multiply = (factor: number) =>
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));
$.mod = (divisor: number) =>
fluent(
value.map((x) => ((x % divisor) + divisor) % divisor),
);
$.rem = (divisor: number) =>
fluent(value.map((x) => x % divisor));
$.log = (base: number = Math.E) =>
fluent(value.map((x) => log(x, base)));
$.floor = () => fluent(value.map(Math.floor));
$.ceil = () => fluent(value.map(Math.ceil));
$.round = (multiple?: number) =>
fluent(
multiple !== undefined
? value.map(
(x) =>
Math.round(x / multiple) * multiple,
)
: value.map(Math.floor),
);
$.fround = () => fluent(value.map(Math.fround));
$.ffround = () => fluent(value.map(Math.f16round));
$.abs = () => fluent(value.map(Math.abs));
$.sign = () => fluent(value.map(Math.sign));
$.negate = () => fluent(value.map((x) => -x));
$.min = (...others: number[]) =>
fluent(Math.min(...value, ...others));
$.max = (...others: number[]) =>
fluent(Math.max(...value, ...others));
$.clamp = (min: number, max: number) =>
fluent(value.map((x) => clamp(x, min, max)));
$.saturate = () => fluent(value.map((x) => clamp(x, 0, 1)));
$.lerp = (b: number[], t: number) => {
assert(
value.length === b.length,
"List.lerp() a and b parameters must be of equal length",
);
return fluent(
value.map((x, idx) => x + (b[idx] - x) * t),
);
};
$.inverseLerp = (a: number[], b: number[]) => {
assert(
value.length === a.length && a.length === b.length,
"List.inverseLerp() a and b parameters must be of equal length",
);
return fluent(
value.map(
(x, idx) => (x - a[idx]) / (b[idx] - a[idx]),
),
);
};
$.sum = () => fluent(value.reduce((a, b) => a + b, 0));
$.product = () => fluent(value.reduce((a, b) => a * b, 1));
$.findMin = () => fluent(value.indexOf(Math.min(...value)));
$.findMax = () => fluent(value.indexOf(Math.max(...value)));
$.average = () =>
fluent(value.reduce((a, b) => a + b, 0) / value.length);
$.median = () => {
const sorted = value.toSorted((a, b) => a - b);
const middle = (sorted.length - 1) / 2;
if (sorted.length % 2 === 1) return sorted[middle];
const a = sorted[Math.floor(middle)];
const b = sorted[Math.ceil(middle)];
return (a + b) / 2;
};
$.mode = () => {
const occurances = new Map<number, number>();
let max = 0;
for (const v of value) {
const count = (occurances.get(v) ?? 0) + 1;
max = Math.max(max, count);
occurances.set(v, count);
}
const output: number[] = [];
occurances.forEach((count, value) => {
if (count === max) output.push(value);
});
return fluent(output);
};
$.range = () =>
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;
const mean =
value.reduce((a, b) => a + b, 0) / value.length;
const deviations = value.map((x) => (x - mean) ** 2);
const sum = deviations.reduce((a, b) => a + b, 0);
if (sample) return sum / (deviations.length - 1);
return sum / deviations.length;
};
$.variance = Object.assign(() => fluent(variance(false)), {
sample: () => fluent(variance(true)),
});
$.deviation = Object.assign(
() => fluent(Math.sqrt(variance(false))),
{
sample: () => fluent(Math.sqrt(variance(true))),
},
);
},
);
if (import.meta.vitest) {
const { test, expect, expectTypeOf, describe } = import.meta
.vitest;
const registry = [M] as const;
const $ = makeFluent(registry);
}
File diff suppressed because it is too large Load Diff
+44 -14
View File
@@ -1,5 +1,5 @@
import { makeFluent } from "../../base";
import type { Props, Return } from "../../base/mixin";
import { Mixin, type Props, type Return } from "../../base/mixin";
import { Math } from "../math";
import type { Vec, Dec, Inc as __Inc } from "../../utility";
@@ -98,23 +98,37 @@ type Primative<
: never
: TOut;
interface Swizzle4D extends SwizzlePermutations<4> {}
type SwizzleCache = [
{ x: [0] },
SwizzlePermutations<2>,
SwizzlePermutations<3>,
SwizzlePermutations<4>,
Swizzle4D,
];
export type Swizzle<
T extends readonly unknown[],
t extends Props,
> = number extends T["length"]
? unknown
: T extends readonly []
? unknown
: (
Dec<T["length"]> extends infer K extends 0 | 1 | 2 | 3
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 }
: SwizzleCache extends [
...unknown[],
infer Last,
]
? {
k: Dec<SwizzleCache["length"]>;
c: Last;
}
: never
) extends {
k: infer K extends number;
@@ -135,13 +149,22 @@ export type Swizzle<
Primative<T, t>[K]
: never;
export type ISwizzle<
T extends readonly unknown[] = readonly unknown[],
t extends Props = Props,
> = {
readonly [K in keyof Swizzle4D]: Return<
Sequence<T, Swizzle4D[K]>,
t
>;
} & Primative<T, t>[3];
const axis = ["x", "y", "z", "w"] as const;
export function applySwizzle(
value: number[],
$: object,
fluent: (value: unknown) => never,
) {
export const applySwizzle = Mixin.partial<
ISwizzle,
readonly unknown[]
>((value, $, fluent) => {
const length = globalThis.Math.min(value.length, axis.length);
const state = new Array<number>(length).fill(-1);
@@ -197,10 +220,11 @@ export function applySwizzle(
return fluent(value[3]);
},
});
}
});
if (import.meta.vitest) {
const { test, expect, expectTypeOf } = import.meta.vitest;
const { test, describe, expect, expectTypeOf } = import.meta
.vitest;
const registry = [Math] as const;
const $ = makeFluent(registry);
@@ -263,6 +287,7 @@ if (import.meta.vitest) {
expectTypeOf(v).toEqualTypeOf(w);
});
describe("types", () => {
test("Next", () => {
type N = 3;
@@ -278,7 +303,9 @@ if (import.meta.vitest) {
});
test("Key", () => {
expectTypeOf<Key<[1, 2, undefined]>>().toEqualTypeOf<"yz">();
expectTypeOf<
Key<[1, 2, undefined]>
>().toEqualTypeOf<"yz">();
expectTypeOf<Key<[1, 1, 1]>>().toEqualTypeOf<"yyy">();
});
@@ -287,7 +314,9 @@ if (import.meta.vitest) {
type Out = [5, 4, 3, 2, 1];
type Reverse = [4, 3, 2, 1, 0];
expectTypeOf<Sequence<In, Reverse>>().toEqualTypeOf<Out>();
expectTypeOf<
Sequence<In, Reverse>
>().toEqualTypeOf<Out>();
});
test("Primative", () => {
@@ -302,4 +331,5 @@ if (import.meta.vitest) {
expectTypeOf<V>().toHaveProperty("y");
expectTypeOf<V>().not.toHaveProperty("z");
});
});
}
+44 -87
View File
@@ -6,7 +6,7 @@ import {
type Props,
type Return,
} from "../base/mixin";
import { assert, assertType, type HidePrototype } from "../internal";
import { assert, type HidePrototype } from "../internal";
type NoneSentinel = null | undefined;
type None<T> = Extract<T, NoneSentinel>;
@@ -16,35 +16,7 @@ function isNone<T>(v: T): v is None<T> {
return v === null || v === undefined;
}
interface Where<T, t extends Props> {
/**
* Set a value to a fallback if it does not conform to some conditional
* @param callback Callback returning a boolean, `false` sets the value to the fallback
* @param fallback The fallback value, `null` by default
* @example
* const parse = () => { version: "1.2.4" } as unknown;
*
* const version = $(parse())
* .where(v => typeof v === 'object' &&
* v !== null &&
* 'version' in v)
* .and(v => v.version)
* .or("1.0.0")
* .value;
* expect(version).toBe("1.2.4");
* @from {@link Optional `Optional`}
*/
where: <const U = null>(
callback: (v: T) => boolean,
fallback?: U,
) => Return<T | U, t>;
}
export interface Optional extends Mixin.HKT {
new: (t: HKT.T<this>) => Input<typeof t> extends infer T
? None<T> extends never
? Where<T, typeof t>
: {
interface IOptional<T = unknown, t extends Props = Props> {
/**
* Transform a value via callback if it is not `null` or
* `undefined`
@@ -65,9 +37,7 @@ export interface Optional extends Mixin.HKT {
* expect(b).toBe(15);
* @from {@link Optional `Optional`}
*/
and: <U>(
callback: (v: Some<T>) => U,
) => Return<None<T> | U, typeof t>;
and: <U>(callback: (v: Some<T>) => U) => Return<None<T> | U, t>;
/**
* Set value to a fallback if it is `null` or `undefined`.
* @param fallback The fallback value to use. To defer
@@ -85,9 +55,7 @@ export interface Optional extends Mixin.HKT {
* expect(b).toBe(10);
* @from {@link Optional `Optional`}
*/
or: (<const U>(
fallback: U,
) => Return<Some<T> | U, typeof t>) & {
or: (<const U>(fallback: U) => Return<Some<T> | U, t>) & {
/**
* Set value to the result of `callback` if it is `null`
* or `undefined`. Unlike the normal `.or()`, this method
@@ -113,7 +81,7 @@ export interface Optional extends Mixin.HKT {
*/
else: <U>(
callback: (v: None<T>) => U,
) => Return<Some<T> | U, typeof t>;
) => Return<Some<T> | U, t>;
} & HidePrototype;
/**
* Assert that value is not `null` or `undefined`
@@ -133,9 +101,7 @@ export interface Optional extends Mixin.HKT {
* }).toThrow()
* ```
*/
assert: ((
msg?: string,
) => Return<Some<T>, typeof t>) & {
assert: ((msg?: string) => Return<Some<T>, t>) & {
/**
* Assert that the value is either `null` or `undefined`
* @param msg Reasoning to attach to the `AssertionError`
@@ -153,74 +119,77 @@ export interface Optional extends Mixin.HKT {
* }).toThrow()
* ```
*/
none: (
msg?: string,
) => Return<None<T>, typeof t>;
none: (msg?: string) => Return<None<T>, t>;
} & HidePrototype;
} & Where<T, typeof t>
}
export interface Optional extends Mixin.HKT<IOptional> {
new: (
t: HKT.T<this>,
) => Input<typeof t> extends infer T
? None<T> extends never
? {}
: IOptional<T, typeof t>
: never;
}
export const Optional = Mixin<Optional>((value, $, fluent) => {
export const Optional = Mixin<IOptional, Optional>(
(value, $, fluent) => {
if (isNone(value)) {
$.and = () => {
return fluent(value);
};
$.or = (fallback: unknown) => {
$.or = Object.assign(
(fallback: unknown) => {
return fluent(fallback);
};
assertType<object>($.or);
Object.assign($.or, {
},
{
else: (callback: (v: unknown) => unknown) => {
return fluent(callback(value));
},
});
},
);
$.assert = (msg?: string) => {
$.assert = Object.assign(
(msg?: string) => {
assert(false, msg);
};
assertType<object>($.assert);
Object.assign($.assert, {
},
{
none: () => {
return fluent(value);
},
});
},
);
} else {
$.and = (callback: (v: unknown) => unknown) => {
return fluent(callback(value));
};
$.or = () => {
$.or = Object.assign(
() => {
return fluent(value);
};
assertType<object>($.or);
Object.assign($.or, {
},
{
else: () => {
return fluent(value);
},
});
},
);
$.assert = () => {
$.assert = Object.assign(
() => {
return fluent(value);
};
assertType<object>($.assert);
Object.assign($.assert, {
},
{
none: (msg?: string) => {
assert(false, msg);
},
});
},
);
}
$.where = (
callback: (v: unknown) => boolean,
fallback = null,
) => {
if (callback(value)) return fluent(value);
return fluent(fallback);
};
});
},
);
if (import.meta.vitest) {
const { test, expect, expectTypeOf, vi } = import.meta.vitest;
@@ -272,16 +241,4 @@ if (import.meta.vitest) {
expect(callback).toHaveBeenCalledOnce();
});
test("where()", () => {
const even = 4 as const;
const odd = 7 as const;
const isEven = (v: number) => v % 2 === 0;
expect($(even).where(isEven).value).toBe(even);
expect($(odd).where(isEven).value).toBe(null);
expect($(odd).where(isEven, -1).value).toBe(-1);
});
}
+34
View File
@@ -1,8 +1,13 @@
import type { HKT } from "./base/hkt";
export type MaxDepth = 50;
export type Pretty<T> = { [K in keyof T]: T[K] };
export type Constrain<T, U> = T extends U ? T : never;
export type IsReadonly<T extends readonly unknown[]> =
T extends unknown[] ? true : false;
namespace Vec {
// prettier-ignore
type Lookup<T, N extends number> = N extends 0
@@ -181,6 +186,35 @@ namespace Flat {
}
export type Flat<T extends readonly unknown[]> = Flat.Flat<T>;
export type RecPartial<T> = {
[K in keyof T]?: RecPartial<T[K]>;
};
export namespace Apply {
export interface Union<U> extends HKT {
new: (t: HKT.T<this>) => typeof t | U;
}
export interface Intersection<U> extends HKT {
new: (t: HKT.T<this>) => typeof t & U;
}
}
export type Apply<
Hkt extends HKT,
T extends readonly Hkt["_in"][],
TOut extends Hkt["_out"][] = [],
> = number extends T["length"]
? Hkt["_out"][]
: TOut["length"] extends MaxDepth
? (TOut[number] | Hkt["_out"])[]
: T extends readonly [
infer Curr extends Hkt["_in"],
...infer Rest extends Hkt["_in"][],
]
? Apply<Hkt, Rest, [...TOut, HKT.Apply<Hkt, Curr>]>
: TOut;
if (import.meta.vitest) {
const { test, expectTypeOf } = import.meta.vitest;
-1
View File
@@ -18,7 +18,6 @@
/* Linting */
"noUnusedLocals": false,
"noUncheckedIndexedAccess": false,
"erasableSyntaxOnly": true,
},
"include": ["src", "tests"]
}
+1
View File
@@ -12,6 +12,7 @@
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"checkJs": true,
"noEmit": true,
/* Linting */