To pytanie jest skomplikowane.
Załóżmy, że mamy funkcję, roundTo2DP(num)
która przyjmuje zmienną jako argument i zwraca wartość zaokrągloną do 2 miejsc po przecinku. Co powinno oceniać każde z tych wyrażeń?
roundTo2DP(0.014999999999999999)
roundTo2DP(0.0150000000000000001)
roundTo2DP(0.015)
„Oczywistą” odpowiedzią jest to, że pierwszy przykład powinien zaokrąglić do 0,01 (ponieważ jest bliżej 0,01 niż do 0,02), podczas gdy pozostałe dwa powinny zaokrąglić do 0,02 (ponieważ 0,0150000000000000001 jest bliższy 0,02 niż do 0,01, a ponieważ 0,015 jest dokładnie w połowie drogi między i istnieje konwencja matematyczna, że takie liczby są zaokrąglane w górę).
Haczyk, który być może zgadłeś, polega na tym, że roundTo2DP
nie można go wprowadzić w celu udzielenia tych oczywistych odpowiedzi, ponieważ wszystkie trzy przekazane mu liczby są tej samej liczby . Binarne liczby zmiennoprzecinkowe IEEE 754 (używane przez JavaScript) nie mogą dokładnie reprezentować większości liczb niecałkowitych, więc wszystkie trzy literały liczbowe powyżej są zaokrąglane do pobliskiej prawidłowej liczby zmiennoprzecinkowej. Ta liczba, jak to się dzieje, jest dokładnie
0,01499999999999999944488848768742172978818416595458984375
co jest bliższe 0,01 niż 0,02.
Możesz zobaczyć, że wszystkie trzy liczby są takie same w konsoli przeglądarki, powłoce węzła lub innym tłumaczu JavaScript. Po prostu porównaj je:
> 0.014999999999999999 === 0.0150000000000000001
true
Więc kiedy piszę m = 0.0150000000000000001
, dokładna wartość tegom
, z czym się skończyłem, jest bliższa 0.01
niż jest 0.02
. A jednak, jeśli przekonwertuję m
na ciąg znaków ...
> var m = 0.0150000000000000001;
> console.log(String(m));
0.015
> var m = 0.014999999999999999;
> console.log(String(m));
0.015
... Dostaję 0,015, co powinno zaokrąglić do 0,02, i który wyraźnie nie jest liczbą z 56 miejsc po przecinku, którą wcześniej powiedziałem, że wszystkie te liczby są dokładnie równe. Co to za mroczna magia?
Odpowiedź można znaleźć w specyfikacji ECMAScript, w sekcji 7.1.12.1: ToString zastosowane do typu Number . Ustanowiono tu zasady konwertowania pewnej liczby m na ciąg znaków. Kluczową częścią jest punkt 5, w którym generowana jest liczba całkowita s, której cyfry zostaną wykorzystane w reprezentacji ciągu m :
niech n , k i s będą liczbami całkowitymi takimi, że k ≥ 1, 10 k -1 ≤ s <10 k , wartość liczbowa dla s × 10 n - k wynosi m , a k jest tak małe, jak to możliwe. Zauważ, że k jest liczbą cyfr w reprezentacji dziesiętnej s , że s nie jest podzielna przez 10, i że najmniej znacząca cyfra s niekoniecznie jest jednoznacznie określona przez te kryteria.
Kluczową częścią jest tutaj wymóg, aby „ k było tak małe, jak to możliwe”. Co wymóg ten wynosi Jest to wymóg, że otrzymuje numer m
, wartość String(m)
musi mieć możliwie najmniejszą liczbę cyfr , a jednocześnie spełniający wymagania tego Number(String(m)) === m
. Ponieważ już to wiemy 0.015 === 0.0150000000000000001
, teraz jest jasne, dlaczego String(0.0150000000000000001) === '0.015'
musi to być prawda.
Oczywiście, żadna z tych dyskusji nie odpowiedziała bezpośrednio na to, co roundTo2DP(m)
powinno powrócić. Jeśli m
dokładna wartość wynosi 0,01499999999999999944488848768742172978818416595458984375, ale jej ciąg reprezentuje „0,015”, to jaka jest prawidłowa odpowiedź - matematycznie, praktycznie, filozoficznie lub cokolwiek innego - kiedy zaokrąglamy ją do dwóch miejsc po przecinku?
Nie ma jednej poprawnej odpowiedzi na to pytanie. To zależy od twojego przypadku użycia. Prawdopodobnie chcesz uszanować reprezentację ciągu i zaokrąglić w górę, gdy:
- Reprezentowana wartość jest z natury dyskretna, np. Ilość waluty w walucie z 3 miejscami po przecinku, takiej jak dinary. W tym przypadku prawdziwa wartość liczby takiej jak 0,015 wynosi 0,015, a reprezentacja 0,0149999999 ... w binarnym zmiennoprzecinkowym jest błędem zaokrąglenia. (Oczywiście wielu argumentuje rozsądnie, że do obsługi takich wartości należy używać biblioteki dziesiętnej i nigdy nie przedstawiać ich jako liczb binarnych zmiennoprzecinkowych.)
- Wartość została wpisana przez użytkownika. W tym przypadku ponownie dokładna wprowadzona liczba dziesiętna jest bardziej „prawdziwa” niż najbliższa binarna reprezentacja zmiennoprzecinkowa.
Z drugiej strony, prawdopodobnie chcesz przestrzegać binarnej wartości zmiennoprzecinkowej i zaokrąglać w dół, gdy twoja wartość pochodzi z naturalnie ciągłej skali - na przykład, jeśli jest to odczyt z czujnika.
Te dwa podejścia wymagają innego kodu. Aby uszanować reprezentację ciągu w postaci liczby, możemy (z dość rozsądnie subtelnym kodem) zaimplementować własne zaokrąglanie, które działa bezpośrednio na reprezentację ciągu, cyfra po cyfrze, przy użyciu tego samego algorytmu, którego używałbyś w szkole uczono, jak zaokrąglać liczby. Poniżej znajduje się przykład, który spełnia wymóg OP dotyczący reprezentowania liczby do 2 miejsc po przecinku „tylko w razie potrzeby” poprzez usunięcie końcowych zer po przecinku; możesz oczywiście dostosować go do swoich konkretnych potrzeb.
/**
* 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+$/, '')
}
Przykładowe użycie:
> 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'
Powyższa funkcja jest prawdopodobnie tym, czego chcesz użyć, aby uniknąć zauważania przez użytkowników nieprawidłowo zaokrąglonego numeru.
(Alternatywnie można również wypróbować bibliotekę round10, która zapewnia podobnie zachowującą się funkcję z zupełnie inną implementacją).
Ale co, jeśli masz drugi rodzaj liczby - wartość zaczerpnięta z ciągłej skali, gdzie nie ma powodu sądzić, że przybliżone reprezentacje dziesiętne z mniejszą liczbą miejsc dziesiętnych są dokładniejsze niż te z większą liczbą? W takim przypadku nie chcemy respektować reprezentacji String, ponieważ ta reprezentacja (jak wyjaśniono w specyfikacji) jest już nieco zaokrąglona; nie chcemy popełnić błędu mówiąc „0,014999999 ... 375 zaokrągla w górę do 0,015, co zaokrągla w górę do 0,02, a więc 0,014999999 ... 375 zaokrągla w górę do 0,02”.
Tutaj możemy po prostu użyć wbudowanej toFixed
metody. Zauważ, że wywołując Number()
zwrócony ciąg toFixed
, otrzymujemy liczbę, której reprezentacja ciągu nie ma zer zerowych (dzięki temu, jak JavaScript oblicza reprezentację ciągu liczby, omówioną wcześniej w tej odpowiedzi).
/**
* 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);
}