Overwrite toFixed() with appropriate replacement to fix floating point error javascript

627 Views Asked by At

This is my attempt to fix the JavaScript toFixed() function...

Any input, ideas, corrections for possible errors are much appreciated!

  • Fix floating point inacurracy (example (35.355).toFixed(2) = 35.36, not 35.35)
  • No big additional libraries
  • Comprehensive function (readable by humans)
  • Mimics toFixed / i.e. outputs exactly the same (albeit with correction for floating point inac. or course)

This is my attempt -> Demo below (see console log)

enter image description here

Number.prototype.toFixed = function(fractionDigits) {
    var digits = parseInt(fractionDigits) || 0;
    var num = Number(this);
    if( isNaN(num) ) {
        return 'NaN';
    }
    
    var sign = num < 0 ? -1 : 1;
    if (sign < 0) { num = -num; }
    digits = Math.pow(10, digits);
    num *= digits;
    num = Math.round( Math.round(num * Math.pow(10,12)) / Math.pow(10,12) );
    var finalNumber = sign * num / digits;

    // add 0 after last decimal number (not 0) for as many as requested (fractionDigits)
    // in else case, check if requested digits exceed actual, then add 0 (avoid 10.1 for toFixed(2))

    if(fractionDigits > 0 && finalNumber.toString().indexOf('.') == -1){
        // check that .00 is present
        finalNumber = finalNumber.toString() + '.' + '0'.repeat(fractionDigits);
    } else if(fractionDigits > finalNumber.toString().split('.')[1]?.length){
        finalNumber = finalNumber.toString() + '0'.repeat((fractionDigits - finalNumber.toString().split('.')[1]?.length));
    }
    
    return finalNumber.toString(); // tofixed returns as string always, do the same
}

console.log('(35.355).toFixed(2)', (35.355).toFixed(2));
console.log('(35.1).toFixed(2)', (35.1).toFixed(2));
console.log('(35).toFixed(2)', (35).toFixed(2));

Number.prototype.toFixed = function(fractionDigits) {
//function toFixed(numberInput, fractionDigits){
    var digits = parseInt(fractionDigits) || 0;
    var num = Number(this);
    if( isNaN(num) ) {
        return 'NaN';
    }
    
    var sign = num < 0 ? -1 : 1;
    if (sign < 0) { num = -num; }
    digits = Math.pow(10, digits);
    num *= digits;
    num = Math.round( Math.round(num * Math.pow(10,12)) / Math.pow(10,12) );
    var finalNumber = sign * num / digits;

    // add 0 after last decimal number (not 0) for as many as requested (fractionDigits)

    if(fractionDigits > 0 && finalNumber.toString().indexOf('.') == -1){
        // check that .00 is present
        finalNumber = finalNumber.toString() + '.' + '0'.repeat(fractionDigits);
    } else if(fractionDigits > finalNumber.toString().split('.')[1]?.length){
        finalNumber = finalNumber.toString() + '0'.repeat((fractionDigits - finalNumber.toString().split('.')[1]?.length));
    }
    
    return finalNumber.toString(); // tofixed returns as string always, do the same
}

console.log('post-fix | (35.355).toFixed(2)', (35.355).toFixed(2));
console.log('post-fix | (35.1).toFixed(2)', (35.1).toFixed(2));
console.log('post-fix | (35).toFixed(2)', (35).toFixed(2));

3

There are 3 best solutions below

0
On BEST ANSWER

If it were me, I might have this string manipulation approach:

Number.prototype.toFixed = function(fractionDigits) {
  var number = String(this);
  var digits = fractionDigits || 0, length;
  
  if(digits < 0 && digits > 100) 
    throw 'RangeError: toFixed() digits argument must be between 0 and 100';

  var decimal = number.match(/(?<=\.)(\d*)/g);
  var factor = Math.pow(10, digits);
  if (decimal && decimal[0].length >= digits) 
    return String(Math.round(Number(number + '1') * factor) / factor);
  else {
    var length = digits - (decimal ? decimal[0].length : 0);
    var delimiter = number.includes('.') || !length ? '' : '.';
    return String(number) + delimiter + '0'.repeat(length);
  }
}

function test() {
  console.log((-35.555).toFixed(2))
  console.log((-35.35).toFixed(2))
  console.log((-35.9).toFixed(2))
  console.log((-35).toFixed(2))
}

Note:

  • I think you're not going to encounter a string in your toFixed since it will not be triggered by it so you don't need isNaN check.
  • Catch beforehand when the parameter is less than 0 or greater than 100. This should throw an error like the original one.

Output:

output

0
On

Instead of rounding number num = Math.round( Math.round(num * Math.pow(10,12)) / Math.pow(10,12) ); here you try parsing it to integer.

Math.round will round the value depending on its factorial part greater or less than 0.5. parseInt will simply fetch integer part without rounding, as you are expecting here.

console.log('(35.355).toFixed(2)', (35.355).toFixed(2));
console.log('(35.1).toFixed(2)', (35.1).toFixed(2));
console.log('(35).toFixed(2)', (35).toFixed(2));

Number.prototype.toFixed = function(fractionDigits) {
//function toFixed(numberInput, fractionDigits){
    debugger;
    var digits = parseInt(fractionDigits) || 0;
    var num = Number(this);
    if( isNaN(num) ) {
        return 'NaN';
    }
    
    var sign = num < 0 ? -1 : 1;
    if (sign < 0) { num = -num; }
    digits = Math.pow(10, digits);
    num *= digits;
    num = parseInt( Math.round(num * Math.pow(10,12)) / Math.pow(10,12) );
    var finalNumber = sign * num / digits;

    // add 0 after last decimal number (not 0) for as many as requested (fractionDigits)

    if(fractionDigits > 0 && finalNumber.toString().indexOf('.') == -1){
        // check that .00 is present
        finalNumber = finalNumber.toString() + '.' + '0'.repeat(fractionDigits);
    } else if(fractionDigits > finalNumber.toString().split('.')[1]?.length){
        finalNumber = finalNumber.toString() + '0'.repeat((fractionDigits - finalNumber.toString().split('.')[1]?.length));
    }
    
    return finalNumber.toString(); // tofixed returns as string always, do the same
}

console.log('post-fix | (35.355).toFixed(2)', (35.355).toFixed(2));
console.log('post-fix | (35.1).toFixed(2)', (35.1).toFixed(2));
console.log('post-fix | (35).toFixed(2)', (35).toFixed(2));

0
On
function toFixed (varNumber, fixedDigits = 2) {
    let result = '', digit = '', remainder = 5; 
    let strNumber = (varNumber + '').replaceAll(/[^-.0-9]*/g, '');
    strNumber = strNumber.includes('.') ? strNumber : strNumber+ '.0'; 
    strNumber = strNumber + '0'.repeat(fixedDigits + 1);
    strNumber = strNumber.slice(0, strNumber.indexOf('.') + fixedDigits + 2);                
    for (let pos = strNumber.length - 1; pos >= 0; pos--) {
        digit = strNumber.charAt(pos);
        if (! '0123456789'.includes(digit)) {
            result = digit + result;
            continue;
        }
        result = (remainder + (+digit)) % 10 + result;
        remainder = remainder + (+digit) > 9 ? 1 : 0;            
    }
    return result.slice(0, (fixedDigits ? -1 : -2));
}