This commit is contained in:
2025-01-04 00:34:03 +01:00
parent 41829408dc
commit 0ca14bbc19
18111 changed files with 1871397 additions and 0 deletions

View File

@@ -0,0 +1,92 @@
import {getType, objectsEqual} from "../utils/helpers.mjs";
/**
* Flatten nested arrays by concatenating their contents
* @returns {any[]} An array containing the concatenated inner values
*/
export function deepFlatten() {
return this.reduce((acc, val) => Array.isArray(val) ? acc.concat(val.deepFlatten()) : acc.concat(val), []);
}
/**
* Test element-wise equality of the values of this array against the values of another array
* @param {any[]} other Some other array against which to test equality
* @returns {boolean} Are the two arrays element-wise equal?
*/
export function equals(other) {
if ( !(other instanceof Array) || (other.length !== this.length) ) return false;
return this.every((v0, i) => {
const v1 = other[i];
const t0 = getType(v0);
const t1 = getType(v1);
if ( t0 !== t1 ) return false;
if ( v0?.equals instanceof Function ) return v0.equals(v1);
if ( t0 === "Object" ) return objectsEqual(v0, v1);
return v0 === v1;
});
}
/**
* Partition an original array into two children array based on a logical test
* Elements which test as false go into the first result while elements testing as true appear in the second
* @param rule {Function}
* @returns {Array} An Array of length two whose elements are the partitioned pieces of the original
*/
export function partition(rule) {
return this.reduce((acc, val) => {
let test = rule(val);
acc[Number(test)].push(val);
return acc;
}, [[], []]);
}
/**
* Join an Array using a string separator, first filtering out any parts which return a false-y value
* @param {string} sep The separator string
* @returns {string} The joined string, filtered of any false values
*/
export function filterJoin(sep) {
return this.filter(p => !!p).join(sep);
}
/**
* Find an element within the Array and remove it from the array
* @param {Function} find A function to use as input to findIndex
* @param {*} [replace] A replacement for the spliced element
* @returns {*|null} The replacement element, the removed element, or null if no element was found.
*/
export function findSplice(find, replace) {
const idx = this.findIndex(find);
if ( idx === -1 ) return null;
if ( replace !== undefined ) {
this.splice(idx, 1, replace);
return replace;
} else {
const item = this[idx];
this.splice(idx, 1);
return item;
}
}
/**
* Create and initialize an array of length n with integers from 0 to n-1
* @memberof Array
* @param {number} n The desired array length
* @param {number} [min=0] A desired minimum number from which the created array starts
* @returns {number[]} An array of integers from min to min+n
*/
export function fromRange(n, min=0) {
return Array.from({length: n}, (v, i) => i + min);
}
// Define primitives on the Array prototype
Object.defineProperties(Array.prototype, {
deepFlatten: {value: deepFlatten},
equals: {value: equals},
filterJoin: {value: filterJoin},
findSplice: {value: findSplice},
partition: {value: partition}
});
Object.defineProperties(Array,{
fromRange: {value: fromRange}
});

View File

@@ -0,0 +1,35 @@
/**
* Test whether a Date instance is valid.
* A valid date returns a number for its timestamp, and NaN otherwise.
* NaN is never equal to itself.
* @returns {boolean}
*/
export function isValid() {
return this.getTime() === this.getTime();
}
/**
* Return a standard YYYY-MM-DD string for the Date instance.
* @returns {string} The date in YYYY-MM-DD format
*/
export function toDateInputString() {
const yyyy = this.getFullYear();
const mm = (this.getMonth() + 1).paddedString(2);
const dd = this.getDate().paddedString(2);
return `${yyyy}-${mm}-${dd}`;
}
/**
* Return a standard H:M:S.Z string for the Date instance.
* @returns {string} The time in H:M:S format
*/
export function toTimeInputString() {
return this.toTimeString().split(" ")[0];
}
// Define primitives on the Date prototype
Object.defineProperties(Date.prototype, {
isValid: {value: isValid},
toDateInputString: {value: toDateInputString},
toTimeInputString: {value: toTimeInputString}
});

View File

@@ -0,0 +1,165 @@
/**
* √3
* @type {number}
*/
export const SQRT3 = 1.7320508075688772;
/**
* √⅓
* @type {number}
*/
export const SQRT1_3 = 0.5773502691896257;
/**
* Bound a number between some minimum and maximum value, inclusively.
* @param {number} num The current value
* @param {number} min The minimum allowed value
* @param {number} max The maximum allowed value
* @return {number} The clamped number
* @memberof Math
*/
export function clamp(num, min, max) {
return Math.min(Math.max(num, min), max);
}
/**
* @deprecated since v12
* @ignore
*/
export function clamped(num, min, max) {
const msg = "Math.clamped is deprecated in favor of Math.clamp.";
foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
return clamp(num, min, max);
}
/**
* Linear interpolation function
* @param {number} a An initial value when weight is 0.
* @param {number} b A terminal value when weight is 1.
* @param {number} w A weight between 0 and 1.
* @return {number} The interpolated value between a and b with weight w.
*/
export function mix(a, b, w) {
return a * (1 - w) + b * w;
}
/**
* Transform an angle in degrees to be bounded within the domain [0, 360)
* @param {number} degrees An angle in degrees
* @returns {number} The same angle on the range [0, 360)
*/
export function normalizeDegrees(degrees, base) {
const d = degrees % 360;
if ( base !== undefined ) {
const msg = "Math.normalizeDegrees(degrees, base) is deprecated.";
foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
if ( base === 360 ) return d <= 0 ? d + 360 : d;
}
return d < 0 ? d + 360 : d;
}
/**
* Transform an angle in radians to be bounded within the domain [-PI, PI]
* @param {number} radians An angle in degrees
* @return {number} The same angle on the range [-PI, PI]
*/
export function normalizeRadians(radians) {
const pi = Math.PI;
const pi2 = pi * 2;
return radians - (pi2 * Math.floor((radians + pi) / pi2));
}
/**
* @deprecated since v12
* @ignore
*/
export function roundDecimals(number, places) {
const msg = "Math.roundDecimals is deprecated.";
foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
places = Math.max(Math.trunc(places), 0);
let scl = Math.pow(10, places);
return Math.round(number * scl) / scl;
}
/**
* Transform an angle in radians to a number in degrees
* @param {number} angle An angle in radians
* @return {number} An angle in degrees
*/
export function toDegrees(angle) {
return angle * (180 / Math.PI);
}
/**
* Transform an angle in degrees to an angle in radians
* @param {number} angle An angle in degrees
* @return {number} An angle in radians
*/
export function toRadians(angle) {
return angle * (Math.PI / 180);
}
/**
* Returns the value of the oscillation between `a` and `b` at time `t`.
* @param {number} a The minimium value of the oscillation
* @param {number} b The maximum value of the oscillation
* @param {number} t The time
* @param {number} [p=1] The period (must be nonzero)
* @param {(x: number) => number} [f=Math.cos] The periodic function (its period must be 2π)
* @returns {number} `((b - a) * (f(2π * t / p) + 1) / 2) + a`
*/
export function oscillation(a, b, t, p=1, f=Math.cos) {
return ((b - a) * (f((2 * Math.PI * t) / p) + 1) / 2) + a;
}
// Define properties on the Math environment
Object.defineProperties(Math, {
SQRT3: {value: SQRT3},
SQRT1_3: {value: SQRT1_3},
clamp: {
value: clamp,
configurable: true,
writable: true
},
clamped: {
value: clamped,
configurable: true,
writable: true
},
mix: {
value: mix,
configurable: true,
writable: true
},
normalizeDegrees: {
value: normalizeDegrees,
configurable: true,
writable: true
},
normalizeRadians: {
value: normalizeRadians,
configurable: true,
writable: true
},
roundDecimals: {
value: roundDecimals,
configurable: true,
writable: true
},
toDegrees: {
value: toDegrees,
configurable: true,
writable: true
},
toRadians: {
value: toRadians,
configurable: true,
writable: true
},
oscillation: {
value: oscillation,
configurable: true,
writable: true
}
});

View File

@@ -0,0 +1,10 @@
/** @module primitives */
export * as Array from "./array.mjs";
export * as Date from "./date.mjs";
export * as Math from "./math.mjs";
export * as Number from "./number.mjs";
export * as Set from "./set.mjs";
export * as String from "./string.mjs";
export * as RegExp from "./regexp.mjs";
export * as URL from "./url.mjs";

View File

@@ -0,0 +1,128 @@
/**
* Test for near-equivalence of two numbers within some permitted epsilon
* @param {number} n Some other number
* @param {number} e Some permitted epsilon, by default 1e-8
* @returns {boolean} Are the numbers almost equal?
*/
export function almostEqual(n, e=1e-8) {
return Math.abs(this - n) < e;
}
/**
* Transform a number to an ordinal string representation. i.e.
* 1 => 1st
* 2 => 2nd
* 3 => 3rd
* @returns {string}
*/
export function ordinalString() {
const s = ["th","st","nd","rd"];
const v = this % 100;
return this + (s[(v-20)%10]||s[v]||s[0]);
}
/**
* Return a string front-padded by zeroes to reach a certain number of numeral characters
* @param {number} digits The number of characters desired
* @returns {string} The zero-padded number
*/
export function paddedString(digits) {
return this.toString().padStart(digits, "0");
}
/**
* Return a string prefaced by the sign of the number (+) or (-)
* @returns {string} The signed number as a string
*/
export function signedString() {
return (( this < 0 ) ? "" : "+") + this;
}
/**
* Round a number to the closest number which is a multiple of the provided interval.
* This is a convenience function intended to humanize issues of floating point precision.
* The interval is treated as a standard string representation to determine the amount of decimal truncation applied.
* @param {number} interval The interval to round the number to the nearest multiple of
* @param {string} [method=round] The rounding method in: round, ceil, floor
* @returns {number} The rounded number
*
* @example Round a number to the nearest step interval
* ```js
* let n = 17.18;
* n.toNearest(5); // 15
* n.toNearest(10); // 20
* n.toNearest(10, "floor"); // 10
* n.toNearest(10, "ceil"); // 20
* n.toNearest(0.25); // 17.25
* ```
*/
export function toNearest(interval=1, method="round") {
if ( interval < 0 ) throw new Error(`Number#toNearest interval must be positive`);
const float = Math[method](this / interval) * interval;
const trunc = Number.isInteger(interval) ? 0 : String(interval).length - 2;
return Number(float.toFixed(trunc));
}
/**
* A faster numeric between check which avoids type coercion to the Number object.
* Since this avoids coercion, if non-numbers are passed in unpredictable results will occur. Use with caution.
* @param {number} a The lower-bound
* @param {number} b The upper-bound
* @param {boolean} inclusive Include the bounding values as a true result?
* @return {boolean} Is the number between the two bounds?
*/
export function between(a, b, inclusive=true) {
const min = Math.min(a, b);
const max = Math.max(a, b);
return inclusive ? (this >= min) && (this <= max) : (this > min) && (this < max);
}
/**
* @see Number#between
* @ignore
*/
Number.between = function(num, a, b, inclusive=true) {
let min = Math.min(a, b);
let max = Math.max(a, b);
return inclusive ? (num >= min) && (num <= max) : (num > min) && (num < max);
}
/**
* Test whether a value is numeric.
* This is the highest performing algorithm currently available, per https://jsperf.com/isnan-vs-typeof/5
* @memberof Number
* @param {*} n A value to test
* @return {boolean} Is it a number?
*/
export function isNumeric(n) {
if ( n instanceof Array ) return false;
else if ( [null, ""].includes(n) ) return false;
return +n === +n;
}
/**
* Attempt to create a number from a user-provided string.
* @memberof Number
* @param {string|number} n The value to convert; typically a string, but may already be a number.
* @return {number} The number that the string represents, or NaN if no number could be determined.
*/
export function fromString(n) {
if ( typeof n === "number" ) return n;
if ( (typeof n !== "string") || !n.length ) return NaN;
n = n.replace(/\s+/g, "");
return Number(n);
}
// Define properties on the Number environment
Object.defineProperties(Number.prototype, {
almostEqual: {value: almostEqual},
between: {value: between},
ordinalString: {value: ordinalString},
paddedString: {value: paddedString},
signedString: {value: signedString},
toNearest: {value: toNearest}
});
Object.defineProperties(Number, {
isNumeric: {value: isNumeric},
fromString: {value: fromString}
});

View File

@@ -0,0 +1,13 @@
/**
* Escape a given input string, prefacing special characters with backslashes for use in a regular expression
* @param {string} string The un-escaped input string
* @returns {string} The escaped string, suitable for use in regular expression
*/
export function escape(string) {
return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
}
// Define properties on the RegExp environment
Object.defineProperties(RegExp, {
escape: {value: escape}
});

View File

@@ -0,0 +1,232 @@
/**
* Return the difference of two sets.
* @param {Set} other Some other set to compare against
* @returns {Set} The difference defined as objects in this which are not present in other
*/
export function difference(other) {
if ( !(other instanceof Set) ) throw new Error("Some other Set instance must be provided.");
const difference = new Set();
for ( const element of this ) {
if ( !other.has(element) ) difference.add(element);
}
return difference;
}
/**
* Return the symmetric difference of two sets.
* @param {Set} other Another set.
* @returns {Set} The set of elements that exist in this or other, but not both.
*/
export function symmetricDifference(other) {
if ( !(other instanceof Set) ) throw new Error("Some other Set instance must be provided.");
const difference = new Set(this);
for ( const element of other ) {
if ( difference.has(element) ) difference.delete(element);
else difference.add(element);
}
return difference
}
/**
* Test whether this set is equal to some other set.
* Sets are equal if they share the same members, independent of order
* @param {Set} other Some other set to compare against
* @returns {boolean} Are the sets equal?
*/
export function equals(other) {
if ( !(other instanceof Set ) ) return false;
if ( other.size !== this.size ) return false;
for ( let element of this ) {
if ( !other.has(element) ) return false;
}
return true;
}
/**
* Return the first value from the set.
* @returns {*} The first element in the set, or undefined
*/
export function first() {
return this.values().next().value;
}
/**
* Return the intersection of two sets.
* @param {Set} other Some other set to compare against
* @returns {Set} The intersection of both sets
*/
export function intersection(other) {
const n = new Set();
for ( let element of this ) {
if ( other.has(element) ) n.add(element);
}
return n;
}
/**
* Test whether this set has an intersection with another set.
* @param {Set} other Another set to compare against
* @returns {boolean} Do the sets intersect?
*/
export function intersects(other) {
for ( let element of this ) {
if ( other.has(element) ) return true;
}
return false;
}
/**
* Return the union of two sets.
* @param {Set} other The other set.
* @returns {Set}
*/
export function union(other) {
if ( !(other instanceof Set) ) throw new Error("Some other Set instance must be provided.");
const union = new Set(this);
for ( const element of other ) union.add(element);
return union;
}
/**
* Test whether this set is a subset of some other set.
* A set is a subset if all its members are also present in the other set.
* @param {Set} other Some other set that may be a subset of this one
* @returns {boolean} Is the other set a subset of this one?
*/
export function isSubset(other) {
if ( !(other instanceof Set ) ) return false;
if ( other.size < this.size ) return false;
for ( let element of this ) {
if ( !other.has(element) ) return false;
}
return true;
}
/**
* Convert a set to a JSON object by mapping its contents to an array
* @returns {Array} The set elements as an array.
*/
export function toObject() {
return Array.from(this);
}
/**
* Test whether every element in this Set satisfies a certain test criterion.
* @see Array#every
* @param {function(*,number,Set): boolean} test The test criterion to apply. Positional arguments are the value,
* the index of iteration, and the set being tested.
* @returns {boolean} Does every element in the set satisfy the test criterion?
*/
export function every(test) {
let i = 0;
for ( const v of this ) {
if ( !test(v, i, this) ) return false;
i++;
}
return true;
}
/**
* Filter this set to create a subset of elements which satisfy a certain test criterion.
* @see Array#filter
* @param {function(*,number,Set): boolean} test The test criterion to apply. Positional arguments are the value,
* the index of iteration, and the set being filtered.
* @returns {Set} A new Set containing only elements which satisfy the test criterion.
*/
export function filter(test) {
const filtered = new Set();
let i = 0;
for ( const v of this ) {
if ( test(v, i, this) ) filtered.add(v);
i++;
}
return filtered;
}
/**
* Find the first element in this set which satisfies a certain test criterion.
* @see Array#find
* @param {function(*,number,Set): boolean} test The test criterion to apply. Positional arguments are the value,
* the index of iteration, and the set being searched.
* @returns {*|undefined} The first element in the set which satisfies the test criterion, or undefined.
*/
export function find(test) {
let i = 0;
for ( const v of this ) {
if ( test(v, i, this) ) return v;
i++;
}
return undefined;
}
/**
* Create a new Set where every element is modified by a provided transformation function.
* @see Array#map
* @param {function(*,number,Set): boolean} transform The transformation function to apply.Positional arguments are
* the value, the index of iteration, and the set being transformed.
* @returns {Set} A new Set of equal size containing transformed elements.
*/
export function map(transform) {
const mapped = new Set();
let i = 0;
for ( const v of this ) {
mapped.add(transform(v, i, this));
i++;
}
if ( mapped.size !== this.size ) {
throw new Error("The Set#map operation illegally modified the size of the set");
}
return mapped;
}
/**
* Create a new Set with elements that are filtered and transformed by a provided reducer function.
* @see Array#reduce
* @param {function(*,*,number,Set): *} reducer A reducer function applied to each value. Positional
* arguments are the accumulator, the value, the index of iteration, and the set being reduced.
* @param {*} accumulator The initial value of the returned accumulator.
* @returns {*} The final value of the accumulator.
*/
export function reduce(reducer, accumulator) {
let i = 0;
for ( const v of this ) {
accumulator = reducer(accumulator, v, i, this);
i++;
}
return accumulator;
}
/**
* Test whether any element in this Set satisfies a certain test criterion.
* @see Array#some
* @param {function(*,number,Set): boolean} test The test criterion to apply. Positional arguments are the value,
* the index of iteration, and the set being tested.
* @returns {boolean} Does any element in the set satisfy the test criterion?
*/
export function some(test) {
let i = 0;
for ( const v of this ) {
if ( test(v, i, this) ) return true;
i++;
}
return false;
}
// Assign primitives to Set prototype
Object.defineProperties(Set.prototype, {
difference: {value: difference},
symmetricDifference: {value: symmetricDifference},
equals: {value: equals},
every: {value: every},
filter: {value: filter},
find: {value: find},
first: {value: first},
intersection: {value: intersection},
intersects: {value: intersects},
union: {value: union},
isSubset: {value: isSubset},
map: {value: map},
reduce: {value: reduce},
some: {value: some},
toObject: {value: toObject}
});

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,16 @@
/**
* Attempt to parse a URL without throwing an error.
* @param {string} url The string to parse.
* @returns {URL|null} The parsed URL if successful, otherwise null.
*/
export function parseSafe(url) {
try {
return new URL(url);
} catch (err) {}
return null;
}
// Define properties on the URL environment
Object.defineProperties(URL, {
parseSafe: {value: parseSafe}
});