Skip to content

Commit

Permalink
Added string-based rounding.
Browse files Browse the repository at this point in the history
Fixes floating-point rounding issues in fallback format function.
Fixes #94.
  • Loading branch information
John Sharp Madhavan-Reese committed Jan 16, 2018
1 parent 02710fc commit 7bea3cf
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 12 deletions.
78 changes: 69 additions & 9 deletions lib/moment-duration-format.js
Expand Up @@ -110,9 +110,35 @@
return result;
}

function precisionRound(number, precision) {
var factor = Math.pow(10, precision);
return Math.round(number * factor) / factor;
function stringRound(digits) {
var digitsArray = digits.split("").reverse();
var i = 0;
var carry = true;

while (carry && i < digitsArray.length) {
if (i) {
if (digitsArray[i] === "9") {
digitsArray[i] = "0";
} else {
digitsArray[i] = (parseInt(digitsArray[i], 10) + 1).toString();
carry = false;
}
} else {
if (parseInt(digitsArray[i], 10) < 5) {
carry = false;
}

digitsArray[i] = "0";
}

i += 1;
}

if (carry) {
digitsArray.push("1");
}

return digitsArray.reverse().join("");
}

// formatNumber
Expand Down Expand Up @@ -140,8 +166,6 @@
var fractionDigits = options.fractionDigits || 0;
var groupingSeparator = options.groupingSeparator;
var decimalSeparator = options.decimalSeparator;
var numberString;
var formattedString = "";

if (useLocaleString && userLocale) {
var localeStringOptions = {
Expand All @@ -161,12 +185,13 @@
return number.toLocaleString(userLocale, localeStringOptions);
}

var numberString;

// Add 1 to digit output length for floating point errors workaround. See below.
if (maximumSignificantDigits) {
numberString = number.toPrecision(maximumSignificantDigits);
numberString = number.toPrecision(maximumSignificantDigits + 1);
} else {
// Workaround for floating point errors in `toFixed`.
// (3.55).toFixed(1); --> "3.5"
numberString = precisionRound(number, fractionDigits).toString();
numberString = number.toFixed(fractionDigits + 1);
}

var integerString;
Expand All @@ -182,6 +207,39 @@
fractionString = temp[1] || "";
integerString = temp[0] || "";

// Workaround for floating point errors in `toFixed` and `toPrecision`.
// (3.55).toFixed(1); --> "3.5"
// (123.55 - 120).toPrecision(2); --> "3.5"
// (123.55 - 120); --> 3.549999999999997
// (123.55 - 120).toFixed(2); --> "3.55"
// Round by examing the string output of the next digit.

// *************** Implement String Rounding here ***********************
// Check integerString + fractionString length of toPrecision before rounding.
// Check length of fractionString from toFixed output before rounding.
var integerLength = integerString.length;
var fractionLength = fractionString.length;
var digitCount = integerLength + fractionLength;
var digits = integerString + fractionString;

if (maximumSignificantDigits && digitCount === (maximumSignificantDigits + 1) || !maximumSignificantDigits && fractionLength === (fractionDigits + 1)) {
// Round digits.
digits = stringRound(digits);

if (digits.length === digitCount + 1) {
integerLength = integerLength + 1;
}

// Discard final fractionDigit.
if (fractionLength) {
digits = digits.slice(0, -1);
}

// Separate integer and fraction.
integerString = digits.slice(0, integerLength);
fractionString = digits.slice(integerLength);
}

// Trim trailing zeroes from fractionString because toPrecision outputs
// precision, not significant digits.
if (maximumSignificantDigits) {
Expand Down Expand Up @@ -222,6 +280,8 @@
}
}

var formattedString = "";

// Handle grouping.
if (useGrouping) {
temp = integerString;
Expand Down
8 changes: 5 additions & 3 deletions test/moment-duration-format-tests.js
Expand Up @@ -149,11 +149,13 @@ test("Floating point errors", function () {
equal(moment.duration(3.55, "hours").format("h", 1), "3.6");
});

QUnit.skip("Floating point errors in Moment.js", function () {
test("Floating point errors from Moment.js output", function () {
equal(moment.duration(123.55, "hours").format("d[d] h[h]", 1), "5d 3.6h");
equal(moment.duration(123.55, "hours").format("d[d] h[h]", 1, { forceFormatFallback: true }), "5d 3.6h");
equal(moment.duration(1234.55, "hours").format({
template: "d [days], h [hours]",
precision: 1
precision: 1,
forceFormatFallback: true
}), "51 days, 10.6 hours");
});

Expand Down Expand Up @@ -1622,7 +1624,7 @@ test("Mixed positive and negative durations", function () {
"-0m 1d 00:00",
"0m 0d 00:00",
"0m 0d 01:00",
"0m 1d 00:00",
"0m 1d 00:00"
]);
});

Expand Down

0 comments on commit 7bea3cf

Please sign in to comment.