UNPKG

@mathigon/fermat

Version:

Powerful mathematics and statistics library for JavaScript.

246 lines (197 loc) 8.71 kB
// ============================================================================= // Fermat.js | Extended Number Class // (c) Mathigon // ============================================================================= import {isInteger, nearlyEquals, numberFormat, sign} from './arithmetic'; import {gcd, lcm} from './number-theory'; const FORMAT = /^([0-9\-.]*)([%πkmbtq]?)(\/([0-9\-.]+))?([%π]?)$/; const tooBig = (x: number) => x >= Number.MAX_SAFE_INTEGER; type Suffix = '%'|'π'|undefined; /** Extended Number class. */ export class XNumber { num: number; /** Used for all number types (decimals, fractions, units, ...). */ den?: number; /** Only used for fractions and always ≥ 0. */ constructor(num: number, den?: number, public unit?: Suffix) { // Ensure that den is always positive this.num = (den !== undefined && den < 0) ? -num : num; if (den !== undefined && Math.abs(den) !== 1 && num !== 0) this.den = Math.abs(den); } valueOf() { return this.value; } toMixed() { if (!this.den || this.unit) return this.toString(); const part = Math.abs(this.num) % this.den; const whole = Math.abs(Math.trunc(this.value)); if (!whole) return this.toString(); return `${this.sign < 0 ? '–' : ''}${whole} ${part}/${this.den}`; } toExpr(type?: 'decimal'|'fraction'|'mixed'|'scientific', precision = 4) { const v = this.value; // TODO Decide if we really want to return infinity here... if (Math.abs(v) >= Number.MAX_VALUE) return '∞'; if (tooBig(this.num) || this.den && tooBig(this.den)) type = 'decimal'; // In scientific notation, we try to return a number in the form a × 10^b if (type === 'scientific' || Math.abs(v) >= Number.MAX_SAFE_INTEGER) { const [base, power] = this.value.toExponential(precision - 1).split('e'); if (Math.abs(+power) >= precision) { const isNeg = power.startsWith('-'); const exp = `${isNeg ? '(' : ''}${isNeg ? power : power.slice(1)}${isNeg ? ')' : ''}`; return `${base.replace(/\.?0+$/, '')} × 10^${exp}${this.unit || ''}`; } } if ((!this.unit && !this.den) || type === 'decimal' || type === 'scientific') { const formatted = numberFormat(this.value, precision); // For non-standard number formatting, we add quotes for expr parsing. return (formatted.match(/^[\d.]+$/g) ? formatted : `"${formatted}"`); } else { return type === 'mixed' ? this.toMixed() : this.toString(); } } toString(precision = 4) { const separators = !this.den && !this.unit; let num = numberFormat(this.num, this.den ? 0 : precision, separators); let unit = this.unit || ''; const den = this.den ? `/${numberFormat(this.den, 0, separators)}` : ''; if (num === '0') unit = ''; if (unit === 'π' && !this.den && (num === '1' || num === '–1')) num = num.replace('1', ''); return `${num}${den}${unit}`; } toMathML() { let str = `<mn>${this.num}</mn>`; if (this.den !== undefined) str = `<mfrac>${str}<mn>${this.den}</mn></mfrac>`; if (this.unit) str += (this.unit === 'π') ? `<mi>π</mi>` : `<mo>%</mo>`; return str; } // --------------------------------------------------------------------------- /** * Returns the value of this number as a decimal. For example, 2/5 and 40% * would both return 0.4. */ get value() { const unit = (this.unit === '%') ? 1/100 : (this.unit === 'π') ? Math.PI : 1; return this.num * unit / (this.den || 1); } get sign() { return Math.sign(this.num); } /** Simplifies fractions, e.g. 4/8 would become 1/2. */ get simplified(): XNumber { if (!this.den) return this; const factor = gcd(Math.abs(this.num), this.den); return new XNumber(this.num / factor, this.den / factor, this.unit); } /** Returns 1/x of this number. */ get inverse() { if (!this.den) return new XNumber(this.den!, this.num); return new XNumber(1 / this.num, undefined, this.unit); } /** Returns -x of this number. */ get negative() { return new XNumber(-this.num, this.den, this.unit); } get fraction() { if (this.unit || !isInteger(this.num)) return; return [this.num, this.den || 1]; } // --------------------------------------------------------------------------- /** Parses a number string, e.g. '1/2' or '20.7%'. */ static fromString(s: string) { s = s.toLowerCase().replace(/[\s,"]/g, '').replace('–', '-').replace('pi', 'π'); const match = s.match(FORMAT); if (!match) return; let suffix = (match[2] || match[5] || undefined) as Suffix; let num = match[1] ? +match[1] : undefined; const den = match[4] ? +match[4] : undefined; // Special handling for π and -π if (suffix === 'π' && (!match[1] || match[1] === '-')) num = match[1] ? -1 : 1; if (num === undefined || isNaN(num)) return; // Handle larger power suffixes const power = suffix ? 'kmbtq'.indexOf(suffix) : -1; if (power >= 0) { num *= 1000 ** (power + 1); suffix = undefined; } // Create XNumber instances if (den === undefined) return new XNumber(num, undefined, suffix); if (isNaN(den) || nearlyEquals(den, 0)) return; if (!isInteger(num) || !isInteger(den)) return new XNumber(num / den, undefined, suffix); return new XNumber(num, den, suffix); } /** Converts a decimal into the closest fraction with a given maximum denominator. */ static fractionFromDecimal(x: number, maxDen = 1000, precision = 1e-12) { let n = [1, 0]; let d = [0, 1]; const absX = Math.abs(x); let rem = absX; while (Math.abs(n[0] / d[0] - absX) > precision) { const a = Math.floor(rem); n = [a * n[0] + n[1], n[0]]; d = [a * d[0] + d[1], d[0]]; if (d[0] > maxDen) return new XNumber(x); rem = 1 / (rem - a); } if (!nearlyEquals(n[0] / d[0], absX, precision)) return new XNumber(x); return new XNumber(sign(x) * n[0], d[0] === 1 ? undefined : d[0]); } // --------------------------------------------------------------------------- clamp(min?: number, max?: number) { const v = this.value; if (min !== undefined && v < min) return new XNumber(min); if (max !== undefined && v > max) return new XNumber(max); return this; } add(a: XNumber|number) { return XNumber.sum(this, a); } subtract(a: XNumber|number) { return XNumber.difference(this, a); } multiply(a: XNumber|number) { return XNumber.product(this, a); } divide(a: XNumber|number) { return XNumber.quotient(this, a); } /** Calculates the sum of two fractions a and b. */ static sum(a: XNumber, b: XNumber|number): XNumber { if (typeof b === 'number') b = new XNumber(b); if (a.num === 0) return b; // If units are different, always convert to a decimal // TODO Maybe have special handling for fraction + percentage? if (a.unit !== b.unit) return new XNumber(a.value + b.value); // Neither a nor b are fractions if (!a.den && !b.den) return new XNumber(a.num + b.num, undefined, a.unit); // Ensure that a is always a fraction if (!a.den) [a, b] = [b, a]; // Trying to add a decimal to a fraction. // TODO Maybe try XNumber.fractionFromDecimal? if (!isInteger(b.num)) return new XNumber(a.value + b.value, undefined, a.unit); const common = lcm(a.den!, b.den ||1); const fa = common / a.den!; const fb = common / (b.den || 1); return new XNumber(a.num * fa + b.num * fb, common, a.unit); } /** Calculates the difference of two numbers a and b. */ static difference(a: XNumber, b: XNumber|number) { if (typeof b === 'number') b = new XNumber(b); return XNumber.sum(a, b.negative); } /** Calculates the product of two numbers a and b. */ static product(a: XNumber, b: XNumber|number) { if (typeof b === 'number') b = new XNumber(b); // Handle simple integer multiplication if (!a.unit && !a.den && isInteger(a.num)) return new XNumber(a.num * b.num, b.den, b.unit); if (!b.unit && !b.den && isInteger(b.num)) return new XNumber(a.num * b.num, a.den, a.unit); // Decimals or units that need to be converted if (a.unit === 'π' || b.unit === 'π' || !isInteger(a.num) || !isInteger(b.num)) return new XNumber(a.value * b.value); // Fraction multiplication const units = (a.unit === '%' ? 100 : 1) * (b.unit === '%' ? 100 : 1); return new XNumber(a.num * b.num, (a.den || 1) * (b.den || 1) * units); } /** Calculates the quotient of two fractions a and b. */ static quotient(a: XNumber, b: XNumber|number) { if (typeof b === 'number') b = new XNumber(b); return XNumber.product(a, b.inverse); } }