Esta questão é complicada.
Suponha que tenhamos uma função, roundTo2DP(num)
que aceita float como argumento e retorna um valor arredondado para 2 casas decimais. O que cada uma dessas expressões deve avaliar?
roundTo2DP(0.014999999999999999)
roundTo2DP(0.0150000000000000001)
roundTo2DP(0.015)
A resposta 'óbvia' é que o primeiro exemplo deve arredondar para 0,01 (porque é mais próximo de 0,01 do que 0,02), enquanto os outros dois devem arredondar para 0,02 (porque 0,01500000000000000000001 está mais próximo de 0,02 do que 0,01 e porque 0,015 está exatamente na metade do caminho eles e existe uma convenção matemática de que esses números são arredondados).
O problema, que você deve ter adivinhado, é que roundTo2DP
não pode ser implementado para dar essas respostas óbvias, porque todos os três números passados para ele são o mesmo número . Os números binários de ponto flutuante IEEE 754 (o tipo usado pelo JavaScript) não podem representar exatamente a maioria dos números não inteiros e, portanto, todos os três literais numéricos acima são arredondados para um número de ponto flutuante válido nas proximidades. Esse número, por acaso, é exatamente
0.01499999999999999944488848768742172978818416595458984375
que é mais próximo de 0,01 do que 0,02.
Você pode ver que todos os três números são iguais no console do navegador, no shell do nó ou em outro intérprete JavaScript. Basta compará-los:
> 0.014999999999999999 === 0.0150000000000000001
true
Então, quando escrevo m = 0.0150000000000000001
, o valor exato dom
qual acabo é mais próximo 0.01
do que é 0.02
. E, no entanto, se eu converter m
para uma String ...
> var m = 0.0150000000000000001;
> console.log(String(m));
0.015
> var m = 0.014999999999999999;
> console.log(String(m));
0.015
... recebo 0,015, que deve arredondar para 0,02, e que não é notavelmente o número de 56 casas decimais que eu disse anteriormente que todos esses números eram exatamente iguais a. Então, que magia negra é essa?
A resposta pode ser encontrada na especificação ECMAScript, na seção 7.1.12.1: ToString aplicada ao tipo Number . Aqui são estabelecidas as regras para converter algum número m em uma string. A parte da chave é o ponto 5, no qual um número inteiro s é gerado cujos dígitos serão usados na representação String de m :
Seja n , k e s números inteiros tais que k ≥ 1, 10 k -1 ≤ s <10 k , o valor numérico para s × 10 n - k é m , e k é o menor possível. Observe que k é o número de dígitos na representação decimal de s , que s não é divisível por 10 e que o dígito menos significativo de s não é necessariamente determinado exclusivamente por esses critérios.
A parte principal aqui é a exigência de que " k seja o menor possível". O que esse requisito equivale é um requisito que, dado um Número m
, o valor de String(m)
deve ter o menor número possível de dígitos enquanto ainda satisfaz o requisito que Number(String(m)) === m
. Como já sabemos disso 0.015 === 0.0150000000000000001
, agora está claro por que String(0.0150000000000000001) === '0.015'
deve ser verdade.
Obviamente, nenhuma dessas discussões respondeu diretamente o que roundTo2DP(m)
deveria retornar. Se m
o valor exato for 0,01499999999999999944488848768742172978818416595458984375, mas sua representação de String for '0,015', qual é a resposta correta - matematicamente, praticamente, filosoficamente ou o que for - quando o arredondamos para duas casas decimais?
Não existe uma resposta correta para isso. Depende do seu caso de uso. Você provavelmente deseja respeitar a representação String e arredondar para cima quando:
- O valor que está sendo representado é inerentemente discreto, por exemplo, uma quantidade de moeda em uma moeda de 3 casas decimais, como dinares. Nesse caso, o valor verdadeiro de um número como 0,015 é 0,015 e a representação 0,0149999999 ... que chega no ponto flutuante binário é um erro de arredondamento. (Obviamente, muitos argumentarão, razoavelmente, que você deve usar uma biblioteca decimal para manipular esses valores e nunca representá-los como números binários de ponto flutuante em primeiro lugar.)
- O valor foi digitado por um usuário. Nesse caso, novamente, o número decimal exato inserido é mais 'verdadeiro' do que a representação binária de ponto flutuante mais próxima.
Por outro lado, você provavelmente quer respeitar o valor do ponto flutuante binário e arredondar para baixo quando o valor for de uma escala inerentemente contínua - por exemplo, se for uma leitura de um sensor.
Essas duas abordagens requerem código diferente. Para respeitar a representação String do número, podemos (com um pouco de código razoavelmente sutil) implementar nosso próprio arredondamento que atua diretamente na representação String, dígito por dígito, usando o mesmo algoritmo que você usaria na escola quando foram ensinados a arredondar números. Abaixo está um exemplo que respeita o requisito do OP de representar o número com 2 casas decimais "somente quando necessário" eliminando os zeros à direita após o ponto decimal; você pode, é claro, precisar ajustá-lo às suas necessidades precisas.
/**
* Converts num to a decimal string (if it isn't one already) and then rounds it
* to at most dp decimal places.
*
* For explanation of why you'd want to perform rounding operations on a String
* rather than a Number, see http://stackoverflow.com/a/38676273/1709587
*
* @param {(number|string)} num
* @param {number} dp
* @return {string}
*/
function roundStringNumberWithoutTrailingZeroes (num, dp) {
if (arguments.length != 2) throw new Error("2 arguments required");
num = String(num);
if (num.indexOf('e+') != -1) {
// Can't round numbers this large because their string representation
// contains an exponent, like 9.99e+37
throw new Error("num too large");
}
if (num.indexOf('.') == -1) {
// Nothing to do
return num;
}
var parts = num.split('.'),
beforePoint = parts[0],
afterPoint = parts[1],
shouldRoundUp = afterPoint[dp] >= 5,
finalNumber;
afterPoint = afterPoint.slice(0, dp);
if (!shouldRoundUp) {
finalNumber = beforePoint + '.' + afterPoint;
} else if (/^9+$/.test(afterPoint)) {
// If we need to round up a number like 1.9999, increment the integer
// before the decimal point and discard the fractional part.
finalNumber = Number(beforePoint)+1;
} else {
// Starting from the last digit, increment digits until we find one
// that is not 9, then stop
var i = dp-1;
while (true) {
if (afterPoint[i] == '9') {
afterPoint = afterPoint.substr(0, i) +
'0' +
afterPoint.substr(i+1);
i--;
} else {
afterPoint = afterPoint.substr(0, i) +
(Number(afterPoint[i]) + 1) +
afterPoint.substr(i+1);
break;
}
}
finalNumber = beforePoint + '.' + afterPoint;
}
// Remove trailing zeroes from fractional part before returning
return finalNumber.replace(/0+$/, '')
}
Exemplo de uso:
> roundStringNumberWithoutTrailingZeroes(1.6, 2)
'1.6'
> roundStringNumberWithoutTrailingZeroes(10000, 2)
'10000'
> roundStringNumberWithoutTrailingZeroes(0.015, 2)
'0.02'
> roundStringNumberWithoutTrailingZeroes('0.015000', 2)
'0.02'
> roundStringNumberWithoutTrailingZeroes(1, 1)
'1'
> roundStringNumberWithoutTrailingZeroes('0.015', 2)
'0.02'
> roundStringNumberWithoutTrailingZeroes(0.01499999999999999944488848768742172978818416595458984375, 2)
'0.02'
> roundStringNumberWithoutTrailingZeroes('0.01499999999999999944488848768742172978818416595458984375', 2)
'0.01'
A função acima é provavelmente o que você deseja usar para evitar que os usuários assistam aos números digitados serem arredondados incorretamente.
(Como alternativa, você também pode tentar o biblioteca round10 , que fornece uma função de comportamento semelhante com uma implementação totalmente diferente.)
Mas e se você tiver o segundo tipo de Número - um valor extraído de uma escala contínua, onde não há razão para pensar que representações decimais aproximadas com menos casas decimais são mais precisas do que aquelas com mais? Nesse caso, nós não queremos respeitar a representação String, porque essa representação (conforme explicado na especificação) já é do tipo arredondada; não queremos cometer o erro de dizer "0,014999999 ... 375 arredonda para 0,015, que arredonda para 0,02, então 0,014999999 ... 375 arredonda para 0,02".
Aqui podemos simplesmente usar o toFixed
método embutido . Observe que, chamando Number()
a String retornada por toFixed
, obtemos um Número cuja representação de String não possui zeros à direita (graças à maneira como o JavaScript calcula a representação de String de um Número, discutida anteriormente nesta resposta).
/**
* Takes a float and rounds it to at most dp decimal places. For example
*
* roundFloatNumberWithoutTrailingZeroes(1.2345, 3)
*
* returns 1.234
*
* Note that since this treats the value passed to it as a floating point
* number, it will have counterintuitive results in some cases. For instance,
*
* roundFloatNumberWithoutTrailingZeroes(0.015, 2)
*
* gives 0.01 where 0.02 might be expected. For an explanation of why, see
* http://stackoverflow.com/a/38676273/1709587. You may want to consider using the
* roundStringNumberWithoutTrailingZeroes function there instead.
*
* @param {number} num
* @param {number} dp
* @return {number}
*/
function roundFloatNumberWithoutTrailingZeroes (num, dp) {
var numToFixedDp = Number(num).toFixed(dp);
return Number(numToFixedDp);
}