shelving
Version:
Toolkit for using data in JavaScript.
167 lines (166 loc) • 6.61 kB
JavaScript
import { RequiredError } from "../error/RequiredError.js";
import { ValueError } from "../error/ValueError.js";
/** Is a value a finite number? */
export function isNumber(value, min = Number.NEGATIVE_INFINITY, max = Number.POSITIVE_INFINITY) {
return Number.isFinite(value) && value >= min && value <= max;
}
/** Assert that a value is a finite number. */
export function assertNumber(value, min, max, caller = assertNumber) {
if (!isNumber(value, min, max))
throw new RequiredError(`Must be finite number${max !== undefined ? ` between ${min ?? 0} and ${max}` : min !== undefined ? ` above ${min}` : ""}`, { received: value, caller });
}
/**
* Convert an unknown value to a finite number, or return `undefined` if it cannot be converted.
* - Note: numbers can be non-finite numbers like `NaN` or `Infinity`. These are detected and will always return `undefined`
*
* Conversion rules:
* - Finite numbers return numbers.
* - `-0` is normalised to `0`
* - Strings are parsed as numbers using `Number.parseFloat()` after removing all non-numeric characters.
* - Dates return their milliseconds (e.g. `date.getTime()`).
* - Everything else returns `undefined`
*/
export function getNumber(value) {
if (typeof value === "number" && Number.isFinite(value))
return value === 0 ? 0 : value;
if (typeof value === "string")
return getNumber(Number.parseFloat(value.replace(NOT_NUMERIC_REGEXP, "")));
if (value instanceof Date)
getNumber(value.getTime());
}
const NOT_NUMERIC_REGEXP = /[^0-9-.]/g;
/**
* Convert a possible number to a finite number, or throw `ValueError` if the value cannot be converted.
*/
export function requireNumber(value, min, max, caller) {
const num = getNumber(value);
assertNumber(num, min, max, caller);
return num;
}
/** Is an unknown value an integer (optionally with specified min/max values). */
export function isInteger(value, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) {
return Number.isInteger(value) && value >= min && value <= max;
}
/** Assert that a value is an integer. */
export function assertInteger(value, min, max, caller = assertInteger) {
if (!isInteger(value, min, max))
throw new RequiredError(`Must be integer${max !== undefined ? ` between ${min ?? 0} and ${max}` : min !== undefined ? ` above ${min}` : ""}`, { received: value, caller });
}
/**
* Convert an unknown value to an integer, or return `undefined` if it cannot be converted.
*
* Conversion rules:
* - Integers return integers.
* - `-0` is normalised to `0`
* - Strings are parsed as integers using `parseInt()` after removing non-numeric characters.
* - Dates return their milliseconds (e.g. `date.getTime()`).
* - Everything else returns `undefined`
*/
export function getInteger(value) {
if (typeof value === "number" && Number.isInteger(value))
return value === 0 ? 0 : value;
if (typeof value === "string")
return getInteger(Number.parseInt(value.replace(NOT_NUMERIC_REGEXP, ""), 10));
if (value instanceof Date)
return getInteger(value.getTime());
}
/** Convert a possible number to an integer, or throw `ValueError` if the value cannot be converted. */
export function requireInteger(value, min, max, caller = requireInteger) {
const num = getNumber(value);
assertInteger(num, min, max, caller);
return num;
}
/**
* Is a number within a specified range?
*
* @param num The number to test, e.g. `17`
* @param min The start of the range, e.g. `10`
* @param max The end of the range, e.g. `20`
*/
export function isBetween(num, min, max) {
return num >= min && num <= max;
}
/**
* Round numbers to a given step.
*
* @param num The number to round.
* @param step The rounding to round to, e.g. `2` or `0.1` (defaults to `1`, i.e. round numbers).
*
* @returns The number rounded to the specified step.
*/
export function roundStep(num, step = 1) {
return Math.round(num / step) * step;
}
/**
* Round a number to a specified set of decimal places.
* - Better than `Math.round()` because it allows a `precision` argument.
* - Better than `num.toFixed()` because it trims excess `0` zeroes.
*
* @param num The number to round.
* @param precision Maximum number of digits shown after the decimal point (defaults to 10).
*
* @returns The number rounded to the specified precision.
*/
export function roundNumber(num, precision = 0) {
return Math.round(num * 10 ** precision) / 10 ** precision;
}
/**
* Truncate a number to a specified set of decimal places.
* - Better than `Math.trunc()` because it allows a `precision` argument.
*
* @param num The number to truncate.
* @param precision Maximum number of digits shown after the decimal point (defaults to 0).
* @returns The number truncated to the specified precision.
*/
export function truncateNumber(num, precision = 0) {
return Math.trunc(num * 10 ** precision) / 10 ** precision;
}
/**
* Bound a number between two values.
* - e.g. `12` bounded by `2` and `8` is `8`
*/
export function boundNumber(num, min, max) {
if (max < min)
throw new ValueError("Max must be more than min", { min, max, caller: wrapNumber });
return Math.max(min, Math.min(max, num));
}
/**
* Wrap a number between two values.
* - Numbers wrap around between min and max (like a clock).
* - e.g. `12` bounded by `2` and `8` is `6`
* - Words in both directions.
* - e.g. `-2` bounded by `2` and `8` is `4`
*/
export function wrapNumber(num, min, max) {
if (max < min)
throw new ValueError("Max must be more than min", { min, max, caller: wrapNumber });
if (num >= max)
return ((num - max) % (max - min)) + min;
if (num < min)
return ((num - min) % (min - max)) + max;
return num;
}
/**
* Get a number as a percentage of another number.
*
* @param numerator Number representing the amount of progress.
* @param denumerator The number representing the whole amount.
*/
export function getPercent(numerator, denumerator) {
return Math.max(0, Math.min(100, (100 / denumerator) * numerator));
}
/** Sum an iterable set of numbers and return the total. */
export function sumNumbers(nums) {
let sum = 0;
for (const num of nums)
sum += num;
return sum;
}
/** Find the number that's closest to a target in an iterable set of numbers. */
export function getClosestNumber(nums, target) {
let closest = undefined;
for (const item of nums)
if (closest === undefined || Math.abs(item - target) < Math.abs(closest - target))
closest = item;
return closest;
}