UNPKG

@mathigon/fermat

Version:

Powerful mathematics and statistics library for JavaScript.

267 lines (209 loc) 7.87 kB
// ============================================================================ // 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; }