@formatjs/ecma402-abstract
Version:
A collection of implementation for ECMAScript abstract operations
209 lines (208 loc) • 6.76 kB
JavaScript
import { ZERO } from "../constants.js";
import "../types/number.js";
import { invariant, repeat } from "../utils.js";
import { ApplyUnsignedRoundingMode } from "./ApplyUnsignedRoundingMode.js";
import { getPowerOf10 } from "./decimal-cache.js";
//IMPL: Helper function to find n1, e1, and r1 using direct calculation
function findN1E1R1(x, p) {
const maxN1 = getPowerOf10(p);
const minN1 = getPowerOf10(p - 1);
// Direct calculation: compute e1 from logarithm
// e1 is the exponent such that n1 * 10^(e1-p+1) <= x
// Taking log: log(n1) + (e1-p+1)*log(10) <= log(x)
// Since n1 is between 10^(p-1) and 10^p, we have:
// (p-1) + (e1-p+1) <= log10(x) < p + (e1-p+1)
// Simplifying: e1 <= log10(x) < e1 + 1
// Therefore: e1 = floor(log10(x))
const log10x = x.log(10);
let e1 = log10x.floor();
// Calculate n1 and r1 from e1
const divisor = getPowerOf10(e1.minus(p).plus(1));
let n1 = x.div(divisor).floor();
let r1 = n1.times(divisor);
// Verify and adjust if n1 is out of bounds
// This handles edge cases near powers of 10
if (n1.greaterThanOrEqualTo(maxN1)) {
e1 = e1.plus(1);
const newDivisor = getPowerOf10(e1.minus(p).plus(1));
n1 = x.div(newDivisor).floor();
r1 = n1.times(newDivisor);
} else if (n1.lessThan(minN1)) {
e1 = e1.minus(1);
const newDivisor = getPowerOf10(e1.minus(p).plus(1));
n1 = x.div(newDivisor).floor();
r1 = n1.times(newDivisor);
}
// Final verification with fallback to iterative search if needed
if (r1.lessThanOrEqualTo(x) && n1.lessThan(maxN1) && n1.greaterThanOrEqualTo(minN1)) {
return {
n1,
e1,
r1
};
}
// Fallback: iterative search (should rarely be needed)
const maxE1 = x.div(minN1).log(10).plus(p).minus(1).ceil();
let currentE1 = maxE1;
while (true) {
const currentDivisor = getPowerOf10(currentE1.minus(p).plus(1));
let currentN1 = x.div(currentDivisor).floor();
if (currentN1.lessThan(maxN1) && currentN1.greaterThanOrEqualTo(minN1)) {
const currentR1 = currentN1.times(currentDivisor);
if (currentR1.lessThanOrEqualTo(x)) {
return {
n1: currentN1,
e1: currentE1,
r1: currentR1
};
}
}
currentE1 = currentE1.minus(1);
}
}
//IMPL: Helper function to find n2, e2, and r2 using direct calculation
function findN2E2R2(x, p) {
const maxN2 = getPowerOf10(p);
const minN2 = getPowerOf10(p - 1);
// Direct calculation: similar to findN1E1R1 but with ceiling
const log10x = x.log(10);
let e2 = log10x.floor();
// Calculate n2 and r2 from e2
const divisor = getPowerOf10(e2.minus(p).plus(1));
let n2 = x.div(divisor).ceil();
let r2 = n2.times(divisor);
// Verify and adjust if n2 is out of bounds
if (n2.greaterThanOrEqualTo(maxN2)) {
e2 = e2.plus(1);
const newDivisor = getPowerOf10(e2.minus(p).plus(1));
n2 = x.div(newDivisor).ceil();
r2 = n2.times(newDivisor);
} else if (n2.lessThan(minN2)) {
e2 = e2.minus(1);
const newDivisor = getPowerOf10(e2.minus(p).plus(1));
n2 = x.div(newDivisor).ceil();
r2 = n2.times(newDivisor);
}
// Final verification with fallback to iterative search if needed
if (r2.greaterThanOrEqualTo(x) && n2.lessThan(maxN2) && n2.greaterThanOrEqualTo(minN2)) {
return {
n2,
e2,
r2
};
}
// Fallback: iterative search (should rarely be needed)
const minE2 = x.div(maxN2).log(10).plus(p).minus(1).floor();
let currentE2 = minE2;
while (true) {
const currentDivisor = getPowerOf10(currentE2.minus(p).plus(1));
let currentN2 = x.div(currentDivisor).ceil();
if (currentN2.lessThan(maxN2) && currentN2.greaterThanOrEqualTo(minN2)) {
const currentR2 = currentN2.times(currentDivisor);
if (currentR2.greaterThanOrEqualTo(x)) {
return {
n2: currentN2,
e2: currentE2,
r2: currentR2
};
}
}
currentE2 = currentE2.plus(1);
}
}
/**
* https://tc39.es/ecma402/#sec-torawprecision
* @param x a finite non-negative Number or BigInt
* @param minPrecision an integer between 1 and 21
* @param maxPrecision an integer between 1 and 21
*/
export function ToRawPrecision(x, minPrecision, maxPrecision, unsignedRoundingMode) {
// 1. Let p be maxPrecision.
const p = maxPrecision;
let m;
let e;
let xFinal;
// 2. If x = 0, then
if (x.isZero()) {
// a. Let m be the String value consisting of p occurrences of the character "0".
m = repeat("0", p);
// b. Let e be 0.
e = 0;
// c. Let xFinal be 0.
xFinal = ZERO;
} else {
// 3. Else,
// a. Let {n1, e1, r1} be the result of findN1E1R1(x, p).
const { n1, e1, r1 } = findN1E1R1(x, p);
// b. Let {n2, e2, r2} be the result of findN2E2R2(x, p).
const { n2, e2, r2 } = findN2E2R2(x, p);
// c. Let r be ApplyUnsignedRoundingMode(x, r1, r2, unsignedRoundingMode).
let r = ApplyUnsignedRoundingMode(x, r1, r2, unsignedRoundingMode);
let n;
// d. If r = r1, then
if (r.eq(r1)) {
// i. Let n be n1.
n = n1;
// ii. Let e be e1.
e = e1.toNumber();
// iii. Let xFinal be r1.
xFinal = r1;
} else {
// e. Else,
// i. Let n be n2.
n = n2;
// ii. Let e be e2.
e = e2.toNumber();
// iii. Let xFinal be r2.
xFinal = r2;
}
// f. Let m be the String representation of n.
m = n.toString();
}
let int;
// 4. If e ≥ p - 1, then
if (e >= p - 1) {
// a. Let m be the string-concatenation of m and p - 1 - e occurrences of the character "0".
m = m + repeat("0", e - p + 1);
// b. Let int be e + 1.
int = e + 1;
} else if (e >= 0) {
// 5. Else if e ≥ 0, then
// a. Let m be the string-concatenation of the first e + 1 characters of m, ".", and the remaining p - (e + 1) characters of m.
m = m.slice(0, e + 1) + "." + m.slice(m.length - (p - (e + 1)));
// b. Let int be e + 1.
int = e + 1;
} else {
// 6. Else,
// a. Assert: e < 0.
invariant(e < 0, "e should be less than 0");
// b. Let m be the string-concatenation of "0.", -e - 1 occurrences of the character "0", and m.
m = "0." + repeat("0", -e - 1) + m;
// c. Let int be 1.
int = 1;
}
// 7. If m contains ".", and maxPrecision > minPrecision, then
if (m.includes(".") && maxPrecision > minPrecision) {
// a. Let cut be maxPrecision - minPrecision.
let cut = maxPrecision - minPrecision;
// b. Repeat, while cut > 0 and the last character of m is "0",
while (cut > 0 && m[m.length - 1] === "0") {
// i. Remove the last character from m.
m = m.slice(0, m.length - 1);
// ii. Decrease cut by 1.
cut--;
}
// c. If the last character of m is ".", then
if (m[m.length - 1] === ".") {
// i. Remove the last character from m.
m = m.slice(0, m.length - 1);
}
}
// 8. Return the Record { [[FormattedString]]: m, [[RoundedNumber]]: xFinal, [[IntegerDigitsCount]]: int, [[RoundingMagnitude]]: e }.
return {
formattedString: m,
roundedNumber: xFinal,
integerDigitsCount: int,
roundingMagnitude: e
};
}