UNPKG

decimal128

Version:

Partial implementation of IEEE 754 Decimal128 decimal floating-point numbers

474 lines (398 loc) 12.7 kB
import JSBI from "jsbi"; import { countFractionalDigits, Digit, ROUNDING_MODE_CEILING, ROUNDING_MODE_FLOOR, ROUNDING_MODE_HALF_EVEN, ROUNDING_MODE_HALF_EXPAND, ROUNDING_MODE_TRUNCATE, RoundingMode, } from "./common.mjs"; const zero = JSBI.BigInt(0); const one = JSBI.BigInt(1); const minusOne = JSBI.BigInt(-1); const ten = JSBI.BigInt(10); function gcd(a: JSBI, b: JSBI): JSBI { while (JSBI.notEqual(b, zero)) { let t = b; b = JSBI.remainder(a, b); a = t; } return a; } function* nextDigitForDivision(x: JSBI, y: JSBI, n: number): Generator<Digit> { let result = ""; let emittedDecimalPoint = false; let done = false; while (!done && countFractionalDigits(result) < n) { if (JSBI.equal(x, zero)) { done = true; } else if (JSBI.LT(x, y)) { if (emittedDecimalPoint) { x = JSBI.multiply(x, ten); if (JSBI.LT(x, y)) { // look ahead: are we still a power of 10 behind? result = result + "0"; yield 0; } } else { emittedDecimalPoint = true; result = (result === "" ? "0" : result) + "."; x = JSBI.multiply(x, ten); yield -1; if (JSBI.LT(x, y)) { // look ahead: are we still a power of 10 behind? result = result + "0"; yield 0; } } } else { let q = JSBI.divide(x, y); x = JSBI.remainder(x, y); let qString = q.toString(); result = result + qString; for (let i = 0; i < qString.length; i++) { yield parseInt(qString.charAt(i)) as Digit; } } } return 0; } export class Rational { readonly numerator: JSBI; readonly denominator: JSBI; readonly isNegative: boolean; constructor(p: JSBI, q: JSBI) { if (JSBI.equal(q, zero)) { throw new RangeError( "Cannot construct rational whose denominator is zero" ); } let num = p; let den = q; let neg = false; if (JSBI.LT(p, zero)) { if (JSBI.LT(q, zero)) { num = JSBI.unaryMinus(p); den = JSBI.unaryMinus(q); } else { num = JSBI.unaryMinus(p); neg = true; } } else if (JSBI.LT(q, zero)) { den = JSBI.unaryMinus(q); neg = true; } let g = gcd(num, den); this.numerator = JSBI.divide(num, g); this.denominator = JSBI.divide(den, g); this.isNegative = neg; } public toString(): string { return `${this.isNegative ? "-" : ""}${this.numerator}/${ this.denominator }`; } public static fromString(s: string): Rational { if (s.match(/^-/)) { return Rational.fromString(s.substring(1)).negate(); } if (s.match(/^[0-9]+$/)) { return new Rational(JSBI.BigInt(s), JSBI.BigInt(1)); } if (s.match(/^[0-9]+[eE][+-]?[0-9]+$/)) { let [num, exp] = s.split(/[eE]/); let originalRat = new Rational(JSBI.BigInt(num), JSBI.BigInt(1)); return originalRat.scale10(Number(exp)); } if (s.match(/[.]/)) { let [whole, decimal] = s.split("."); if (decimal.match(/[eE]/)) { let [dec, exp] = decimal.split(/[eE]/); let originalRat = Rational.fromString(`${whole}.${dec}`); return originalRat.scale10(Number(exp)); } let numerator = JSBI.BigInt(whole + decimal); let denominator = JSBI.exponentiate( ten, JSBI.BigInt(decimal.length) ); return new Rational(numerator, denominator); } throw new SyntaxError(`Invalid rational number string: ${s}`); } public scale10(n: number): Rational { if (this.isNegative) { return this.negate().scale10(n).negate(); } if (n === 0) { return this; } if (n < 0) { return new Rational( this.numerator, JSBI.multiply( this.denominator, JSBI.exponentiate(ten, JSBI.BigInt(0 - n)) ) ); } return new Rational( JSBI.multiply( this.numerator, JSBI.exponentiate(ten, JSBI.BigInt(n)) ), this.denominator ); } public negate(): Rational { if (this.isNegative) { return new Rational(this.numerator, this.denominator); } return new Rational( JSBI.multiply(this.numerator, minusOne), this.denominator ); } private static _add(x: Rational, y: Rational): Rational { if (x.isNegative) { return Rational._subtract(y, x.negate()); } if (y.isNegative) { return Rational._subtract(x, y.negate()); } return new Rational( JSBI.add( JSBI.multiply(x.numerator, y.denominator), JSBI.multiply(y.numerator, x.denominator) ), JSBI.multiply(x.denominator, y.denominator) ); } private static _subtract(x: Rational, y: Rational): Rational { if (x.isNegative) { return Rational._add(x.negate(), y).negate(); } return new Rational( JSBI.subtract( JSBI.multiply(x.numerator, y.denominator), JSBI.multiply(y.numerator, x.denominator) ), JSBI.multiply(x.denominator, y.denominator) ); } private static _multiply(x: Rational, y: Rational): Rational { return new Rational( JSBI.multiply(x.numerator, y.numerator), JSBI.multiply(x.denominator, y.denominator) ); } public static add(...theArgs: Rational[]): Rational { return theArgs.reduce( (acc, cur) => Rational._add(acc, cur), new Rational(zero, one) ); } public static subtract(x: Rational, ...theArgs: Rational[]): Rational { return theArgs.reduce((acc, cur) => Rational._subtract(acc, cur), x); } public static multiply(...theArgs: Rational[]): Rational { return theArgs.reduce( (acc, cur) => Rational._multiply(acc, cur), new Rational(one, one) ); } public toFixed(n: number): string { if (n !== Infinity && !Number.isInteger(n)) { throw new TypeError( "Cannot enumerate a non-integer number of decimal places" ); } if (n < 0) { throw new RangeError( "Cannot enumerate a negative number of decimal places" ); } if (this.isNegative) { return "-" + this.negate().toFixed(n); } if (JSBI.equal(this.numerator, zero)) { if (Infinity === n) { throw new RangeError( "Cannot enumerate infinite decimal places of zero" ); } return "0" + "." + "0".repeat(n); } let digitGenerator = nextDigitForDivision( this.numerator, this.denominator, n ); let digit = digitGenerator.next(); let result = ""; while (!digit.done) { let v = digit.value; if (-1 === v) { result = ("" === result ? "0" : result) + "."; } else { result = result + `${v}`; } digit = digitGenerator.next(); } if (Infinity === n) { return result; } let numFractionalDigits = countFractionalDigits(result); if (numFractionalDigits >= n) { return result; } let numZeroesNeeded = n - numFractionalDigits; let zeroesNeeded = "0".repeat(numZeroesNeeded); if (result.match(/[.]/)) { return result + zeroesNeeded; } return result + "." + zeroesNeeded; } private static roundHalfEven( initialPart: Rational, penultimateDigit: Digit, finalDigit: Digit, quantum: Rational ): Rational { if (finalDigit < 5) { return initialPart; } if (finalDigit > 5) { return Rational.add( initialPart, initialPart.isNegative ? quantum.negate() : quantum ); } if (penultimateDigit % 2 === 0) { return initialPart; } return Rational.add( initialPart, initialPart.isNegative ? quantum.negate() : quantum ); } private static roundHalfExpand( initialPart: Rational, penultimateDigit: Digit, finalDigit: Digit, quantum: Rational ): Rational { if (finalDigit < 5) { return initialPart; } return Rational.add( initialPart, initialPart.isNegative ? quantum.negate() : quantum ); } private static roundCeil( initialPart: Rational, penultimateDigit: Digit, finalDigit: Digit, quantum: Rational ): Rational { if (initialPart.isNegative) { return initialPart; } if (finalDigit === 0) { return initialPart; } return Rational.add(initialPart, quantum); } private static roundFloor( initialPart: Rational, penultimateDigit: Digit, finalDigit: Digit, quantum: Rational ): Rational { if (initialPart.isNegative) { return Rational.subtract(initialPart, quantum); } return initialPart; } round(numFractionalDigits: number, mode: RoundingMode): Rational { if (numFractionalDigits < 0) { throw new RangeError( "Cannot round to negative number of decimal places" ); } let s = this.toFixed(numFractionalDigits + 1); let [integerPart, fractionalPart] = s.split("."); let quantum = Rational.fromString( numFractionalDigits === 0 ? "1" : "0" + "." + "0".repeat(numFractionalDigits - 1) + "1" ); let truncated = Rational.fromString( integerPart + "." + fractionalPart.substring(0, numFractionalDigits) ); let penultimateDigit = parseInt( numFractionalDigits === 0 ? integerPart.charAt(integerPart.length - 1) : fractionalPart.charAt(numFractionalDigits - 1) ) as Digit; let finalDigit = parseInt( fractionalPart.charAt(numFractionalDigits) ) as Digit; if (mode === ROUNDING_MODE_TRUNCATE) { return truncated; } if (mode === ROUNDING_MODE_HALF_EVEN) { return Rational.roundHalfEven( truncated, penultimateDigit, finalDigit, quantum ); } if (mode === ROUNDING_MODE_CEILING) { return Rational.roundCeil( truncated, penultimateDigit, finalDigit, quantum ); } if (mode === ROUNDING_MODE_FLOOR) { return Rational.roundFloor( truncated, penultimateDigit, finalDigit, quantum ); } return Rational.roundHalfExpand( truncated, penultimateDigit, finalDigit, quantum ); } cmp(x: Rational): -1 | 0 | 1 { let a = JSBI.multiply( JSBI.multiply(this.isNegative ? minusOne : one, this.numerator), x.denominator ); let b = JSBI.multiply( JSBI.multiply(x.isNegative ? minusOne : one, x.numerator), this.denominator ); if (JSBI.LT(a, b)) { return -1; } if (JSBI.LT(b, a)) { return 1; } return 0; } isZero(): boolean { return JSBI.equal(this.numerator, zero); } }