|
// Copyright (c) Artem Avramenko. All rights reserved. |
|
// Licensed under the MIT license. See license.txt for details. |
|
|
|
/** |
|
* Returns an integer power of ten with maximum accuracy and performance |
|
*/ |
|
const pow10 = (() => { |
|
const precalc = [] as number[]; |
|
for (let i = -323; i < 309; i++) { |
|
precalc.push(+('1e' + i)); |
|
} |
|
return (intPow: number) => { |
|
intPow = +intPow; |
|
if (Number.isNaN(intPow)) { |
|
return NaN; |
|
} |
|
if (intPow <= -324) { |
|
return 0; |
|
} |
|
if (intPow >= 309) { |
|
return Infinity; |
|
} |
|
return precalc[(intPow|0) + 323]; |
|
} |
|
})(); |
|
|
|
/** |
|
* A more accurate version of decimal rounding. |
|
* In certain cases can be hundreds of times slower than normal rounding. |
|
*/ |
|
function roundDecimal(value: number, digits = 0, fromDigits = -1) { |
|
let newValue = +value; |
|
if (newValue === 0) { |
|
return newValue; |
|
} |
|
const isNegative = newValue < 0; |
|
if (isNegative) { |
|
newValue = -newValue; |
|
} |
|
|
|
digits |= 0; |
|
if (digits === 0 && fromDigits < 0) { |
|
newValue = Math.round(newValue); |
|
} else if (digits >= 0 && digits <= fromDigits && fromDigits < 309) { |
|
// knowing that the value already contains decimal, faster corrections can be used |
|
const fromPow = pow10(fromDigits); |
|
const toPow = pow10(fromDigits - digits); |
|
newValue = Math.round(newValue * fromPow); |
|
const rem = newValue % toPow; |
|
if (rem >= toPow / 2) { |
|
newValue += toPow - rem; |
|
} else { |
|
newValue -= rem; |
|
} |
|
newValue /= fromPow; |
|
} else if (newValue >= 1e-6 && newValue < 1e21) { |
|
// slow mode for numbers without exponent |
|
const s = newValue.toString() + 'e' + digits; |
|
newValue = Math.round(+s); |
|
newValue = digits >= 0 ? newValue / pow10(digits) : newValue * pow10(-digits); |
|
} else { |
|
// slowest mode (https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Math/round) |
|
let [valS, powS] = newValue.toString().split('e'); |
|
let pow = +(powS || 0) + digits; |
|
newValue = Math.round(+(valS + 'e' + pow)); |
|
[valS, powS] = newValue.toString().split('e'); |
|
pow = +(powS || 0) - digits; |
|
newValue = +(valS + 'e' + pow); |
|
} |
|
|
|
if (!Number.isFinite(newValue)) { |
|
return +value; |
|
} |
|
|
|
return isNegative ? -newValue : newValue; |
|
} |
|
|
|
/** |
|
* Corrects decimal amounts. |
|
* Suitable for addition/subtraction operations only. |
|
*/ |
|
function normalizeDecimal(value: number, digits: number) { |
|
return roundDecimal(value, digits, digits); |
|
} |
|
|
|
/** |
|
* Corrects money amounts. |
|
* Suitable for addition/subtraction operations only. |
|
*/ |
|
function normalizeMoney(value: number) { |
|
return roundDecimal(value, 4, 4); |
|
} |
|
|
|
/** |
|
* Rounds decimal amount. |
|
* Suitable only for values already containing normalized money. |
|
*/ |
|
function roundMoney(money: number, digits = 2) { |
|
return roundDecimal(money, digits, 4); |
|
} |
|
|
|
const expect = (actual: any) => ({ |
|
toBe: (expected: any) => { |
|
if (actual !== expected) { |
|
console.error(actual + ' should be ' + expected) |
|
} |
|
} |
|
}); |
|
|
|
expect(normalizeMoney(0.1 + 0.2)).toBe(0.3); |
|
expect(roundMoney(1.0250, 2)).toBe(1.03); |
|
expect(normalizeMoney(0.101 + 0.704)).toBe(0.805); |
|
expect(normalizeDecimal(0.101 + 0.704, 3)).toBe(0.805); |
|
expect(normalizeMoney(0.101 + 0.904)).toBe(1.005); |
|
expect(roundDecimal(0.101 + 0.904, 4)).toBe(1.005); |
|
expect(normalizeMoney(262143.521 + 0.704)).toBe(262144.225); |
|
expect(roundDecimal(1025, -1)).toBe(1030); |
|
expect(roundDecimal(Infinity)).toBe(Infinity); |
|
expect(roundDecimal(-Infinity, 2, 4)).toBe(-Infinity); |
|
expect(roundDecimal(NaN).toString()).toBe('NaN'); |
|
expect(roundDecimal(NaN, 2, 4).toString()).toBe('NaN'); |
|
expect(1 / roundDecimal(-0, 2)).toBe(-Infinity); |
|
expect(roundDecimal(1.2345678901234565, 15, 15)).toBe(1.234567890123457); |
|
expect(roundDecimal(1.2345678901234562, 15, 15)).toBe(1.234567890123456); |
|
expect(roundDecimal(0.1122334455667752, 15, 15)).toBe(0.112233445566775); |
|
expect(roundDecimal(0.11223344556677449, 15, 15)).toBe(0.112233445566774); |
|
expect(roundDecimal(1.495, 0, 3)).toBe(1); |
|
expect(roundDecimal(1.495, 0, 2)).toBe(2); |
|
expect(roundDecimal(1.495, 0, 1)).toBe(2); |
|
expect(roundDecimal(1.005, 2)).toBe(1.01); |
|
expect(roundDecimal(123450000, -5)).toBe(123500000); |
|
expect(roundDecimal(1.2344e-305, 307, 308)).toBe(1.23e-305); |
|
expect(roundDecimal(1.2345e-305, 307, 308)).toBe(1.24e-305); |
|
expect(roundDecimal(1.234e-320, 322)).toBe(1.23e-320); |
|
expect(roundDecimal(0.00123, 320)).toBe(0.00123); |
|
expect(roundDecimal(0.00123, 307, 308)).toBe(0.00123); |
|
expect(roundDecimal(1.23e308, 307, 308)).toBe(1.23e308); |
|
expect(roundDecimal(1.23e308, -307)).toBe(1.2e308); |
|
expect(roundDecimal(1.23e308, -308)).toBe(1e308); |
|
expect(roundDecimal(1.23e308, -309)).toBe(0); |
|
|
|
console.log('Tests finished'); |