@mathigon/fermat
Version:
Powerful mathematics and statistics library for JavaScript.
267 lines (209 loc) • 7.87 kB
text/typescript
// ============================================================================
// Fermat.js | Utility Functions
// (c) Mathigon
// ============================================================================
const PRECISION = 0.000001;
// -----------------------------------------------------------------------------
// Checks and Comparisons
/** Checks if two numbers are nearly equals. */
export function nearlyEquals(a: number, b: number, t = PRECISION) {
if (isNaN(a) || isNaN(b)) return false;
return Math.abs(a - b) < t;
}
/* Checks if an object is an integer. */
export function isInteger(x: number, t = PRECISION) {
return nearlyEquals(x, Math.round(x), t);
}
/** Checks if a number x is between two numbers a and b. */
export function isBetween(value: number, a: number, b: number, t = PRECISION) {
if (a > b) [a, b] = [b, a];
return value > a + t && value < b - t;
}
/** Returns the sign of a number x, as +1, 0 or –1. */
export function sign(value: number, t = PRECISION) {
return nearlyEquals(value, 0, t) ? 0 : (value > 0 ? 1 : -1);
}
// -----------------------------------------------------------------------------
// String Conversion
const NUM_REGEX = /(\d+)(\d{3})/;
const POWER_SUFFIX = ['', 'k', 'm', 'b', 't', 'q'];
function addThousandSeparators(x: string) {
let [n, dec] = x.split('.');
while (NUM_REGEX.test(n)) {
n = n.replace(NUM_REGEX, '$1,$2');
}
return n + (dec ? `.${dec}` : '');
}
function addPowerSuffix(n: number, places = 6) {
if (!places) return `${n}`;
// Trim short numbers to the appropriate number of decimal places.
const digits = (`${Math.abs(Math.floor(n))}`).length;
const chars = digits + (n < 0 ? 1 : 0);
if (chars <= places) return `${round(n, places - chars)}`;
// Append a power suffix to longer numbers.
const x = Math.floor(Math.log10(Math.abs(n)) / 3);
const suffix = POWER_SUFFIX[x];
const decimalPlaces = places - ((digits % 3) || 3) - (suffix ? 1 : 0) - (n < 0 ? 1 : 0);
return round(n / Math.pow(10, 3 * x), decimalPlaces) + suffix;
}
/**
* Converts a number to a clean string, by rounding, adding power suffixes, and
* adding thousands separators. `places` is the number of digits to show in the
* result.
*/
export function numberFormat(n: number, places = 0, separators = true) {
const str = addPowerSuffix(n, places).replace('-', '–');
return separators ? addThousandSeparators(str) : str;
}
export function scientificFormat(value: number, places = 6) {
const abs = Math.abs(value);
if (isBetween(abs, Math.pow(10, -places), Math.pow(10, places))) {
return numberFormat(value, places);
}
// TODO Decide how we want to handle these special cases
if (abs > Number.MAX_VALUE) return `${Math.sign(value) < 0 ? '–' : ''}∞`;
if (abs < Number.MIN_VALUE) return '0';
const [str, exponent] = value.toExponential().split('e');
const top = exponent.replace('+', '').replace('-', '–');
const isNegative = top.startsWith('–');
return `${str.slice(0, 5)} × 10^${(isNegative ? '(' : '') + top + (isNegative ? ')' : '')}`;
}
// Numbers like 0,123 are decimals, even though they match POINT_DECIMAL.
const SPECIAL_DECIMAL = /^-?0,[0-9]+$/;
// Points as decimal points, Commas as 1k separators, allow starting .
const POINT_DECIMAL = /^-?([0-9]+(,[0-9]{3})*)?\.?[0-9]*$/;
// Commas as decimal points, Points as 1k separators, don't allow starting ,
const COMMA_DECIMAL = /^-?[0-9]+(\.[0-9]{3})*,?[0-9]*$/;
/**
* Converts a number to a string, including . or , decimal points and
* thousands separators.
* @param {string} str
* @returns {number}
*/
export function parseNumber(str: string) {
str = str.replace(/^–/, '-').trim();
if (!str || str.match(/[^0-9.,-]/)) return NaN;
if (SPECIAL_DECIMAL.test(str)) {
return parseFloat(str.replace(/,/, '.'));
}
if (POINT_DECIMAL.test(str)) {
return parseFloat(str.replace(/,/g, ''));
}
if (COMMA_DECIMAL.test(str)) {
return parseFloat(str.replace(/\./g, '').replace(/,/, '.'));
}
return NaN;
}
/**
* Converts a number to an ordinal.
* @param {number} x
* @returns {string}
*/
export function toOrdinal(x: number) {
if (Math.abs(x) % 100 >= 11 && Math.abs(x) % 100 <= 13) {
return `${x}th`;
}
switch (x % 10) {
case 1:
return `${x}st`;
case 2:
return `${x}nd`;
case 3:
return `${x}rd`;
default:
return `${x}th`;
}
}
// TODO Translate this function into other languages.
const ONES = ['', 'one', 'two', 'three', 'four', 'five', 'six', 'seven',
'eight', 'nine', 'ten', 'eleven', 'twelve', 'thirteen', 'fourteen',
'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen'];
const TENS = ['', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty',
'seventy', 'eighty', 'ninety'];
const MULTIPLIERS = ['', ' thousand', ' million', ' billion', ' trillion',
' quadrillion', ' quintillion', ' sextillion'];
function toWordSingle(number: string) {
const [h, t, o] = number.split('');
const hundreds = (h === '0') ? '' : ` ${ONES[+h]} hundred`;
if (t + o === '00') return hundreds;
if (+t < 2) return `${hundreds} ${ONES[+(t + o)]}`;
if (o === '0') return `${hundreds} ${TENS[+t]}`;
return `${hundreds} ${TENS[+t]}-${ONES[+o]}`;
}
/** Spells a number as an English word. */
export function toWord(n: number) {
if (n === 0) return 'zero';
const str = Math.round(Math.abs(n)).toString();
const chunks = Math.ceil(str.length / 3);
const padded = str.padStart(3 * chunks, '0');
let result = '';
for (let i = 0; i < chunks; i += 1) {
const chunk = padded.substr(i * 3, 3);
if (chunk === '000') continue;
result += toWordSingle(chunk) + MULTIPLIERS[chunks - 1 - i];
}
return result.trim();
}
// -----------------------------------------------------------------------------
// Rounding, Decimals and Fractions
/** Returns the digits of a number n. */
export function digits(n: number) {
const str = `${Math.abs(n)}`;
return str.split('').reverse().map(x => +x);
}
/** Rounds a number `n` to `precision` decimal places. */
export function round(n: number, precision = 0) {
const factor = Math.pow(10, precision);
return Math.round(n * factor) / factor;
}
/** Round a number `n` to the nearest multiple of `increment`. */
export function roundTo(n: number, increment = 1) {
return Math.round(n / increment) * increment;
}
// -----------------------------------------------------------------------------
// Simple Operations
/** Bounds a number between a lower and an upper limit. */
export function clamp(x: number, min = -Infinity, max = Infinity) {
return Math.min(max, Math.max(min, x));
}
/** Linear interpolation */
export function lerp(a: number, b: number, t = 0.5) {
return a + (b - a) * t;
}
/** Squares a number. */
export function square(x: number) {
return x * x;
}
/** Cubes a number. */
export function cube(x: number) {
return x * x * x;
}
/**
* Calculates `a mod m`. The JS implementation of the % operator returns the
* symmetric modulo. Both are identical if a >= 0 and m >= 0 but the results
* differ if a or m < 0.
*/
export function mod(a: number, m: number) {
return ((a % m) + m) % m;
}
/** Calculates the logarithm of `x` with base `b`. */
export function log(x: number, b?: number) {
return (b === undefined) ? Math.log(x) : Math.log(x) / Math.log(b);
}
/** Solves the quadratic equation a x^2 + b x + c = 0 */
export function quadratic(a: number, b: number, c: number): number[] {
if (nearlyEquals(a, 0) && nearlyEquals(b, 0)) return [];
if (nearlyEquals(a, 0)) return [-c / b];
const p = -b / 2 / a;
const q = Math.sqrt(b * b - 4 * a * c) / 2 / a;
return [p + q, p - q];
}
export function polynomial(x: number, coefficients: number[]) {
let total = 0;
let xi = 1;
for (const c of coefficients) {
total += xi * c;
xi *= x;
}
return total;
}