decimal128
Version:
Partial implementation of IEEE 754 Decimal128 decimal floating-point numbers
275 lines (274 loc) • 10.3 kB
JavaScript
import JSBI from "jsbi";
import { countFractionalDigits, ROUNDING_MODE_CEILING, ROUNDING_MODE_FLOOR, ROUNDING_MODE_HALF_EVEN, ROUNDING_MODE_TRUNCATE, } 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, b) {
while (JSBI.notEqual(b, zero)) {
let t = b;
b = JSBI.remainder(a, b);
a = t;
}
return a;
}
function* nextDigitForDivision(x, y, n) {
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));
}
}
}
return 0;
}
export class Rational {
constructor(p, q) {
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;
}
toString() {
return `${this.isNegative ? "-" : ""}${this.numerator}/${this.denominator}`;
}
static fromString(s) {
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}`);
}
scale10(n) {
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);
}
negate() {
if (this.isNegative) {
return new Rational(this.numerator, this.denominator);
}
return new Rational(JSBI.multiply(this.numerator, minusOne), this.denominator);
}
static _add(x, y) {
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));
}
static _subtract(x, y) {
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));
}
static _multiply(x, y) {
return new Rational(JSBI.multiply(x.numerator, y.numerator), JSBI.multiply(x.denominator, y.denominator));
}
static add(...theArgs) {
return theArgs.reduce((acc, cur) => Rational._add(acc, cur), new Rational(zero, one));
}
static subtract(x, ...theArgs) {
return theArgs.reduce((acc, cur) => Rational._subtract(acc, cur), x);
}
static multiply(...theArgs) {
return theArgs.reduce((acc, cur) => Rational._multiply(acc, cur), new Rational(one, one));
}
toFixed(n) {
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;
}
static roundHalfEven(initialPart, penultimateDigit, finalDigit, quantum) {
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);
}
static roundHalfExpand(initialPart, penultimateDigit, finalDigit, quantum) {
if (finalDigit < 5) {
return initialPart;
}
return Rational.add(initialPart, initialPart.isNegative ? quantum.negate() : quantum);
}
static roundCeil(initialPart, penultimateDigit, finalDigit, quantum) {
if (initialPart.isNegative) {
return initialPart;
}
if (finalDigit === 0) {
return initialPart;
}
return Rational.add(initialPart, quantum);
}
static roundFloor(initialPart, penultimateDigit, finalDigit, quantum) {
if (initialPart.isNegative) {
return Rational.subtract(initialPart, quantum);
}
return initialPart;
}
round(numFractionalDigits, mode) {
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));
let finalDigit = parseInt(fractionalPart.charAt(numFractionalDigits));
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) {
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() {
return JSBI.equal(this.numerator, zero);
}
}