Pertanyaan ini rumit.
Misalkan kita memiliki fungsi,, roundTo2DP(num)
yang menggunakan float sebagai argumen dan mengembalikan nilai yang dibulatkan menjadi 2 tempat desimal. Apa yang harus dievaluasi dari masing-masing ekspresi ini?
roundTo2DP(0.014999999999999999)
roundTo2DP(0.0150000000000000001)
roundTo2DP(0.015)
Jawaban 'jelas' adalah bahwa contoh pertama harus dibulatkan menjadi 0,01 (karena lebih dekat ke 0,01 daripada 0,02) sedangkan dua lainnya harus bulat menjadi 0,02 (karena 0,015000000000000000001 lebih dekat ke 0,02 daripada 0,01, dan karena 0,015 persis setengah jalan antara mereka dan ada konvensi matematika bahwa angka-angka seperti itu dapat ditangkap).
Tangkapannya, yang mungkin sudah Anda duga, adalah yang roundTo2DP
tidak mungkin diterapkan untuk memberikan jawaban yang jelas, karena ketiga angka yang diteruskan itu adalah angka yang sama . Angka-angka floating point biner IEEE 754 (jenis yang digunakan oleh JavaScript) tidak bisa persis mewakili sebagian besar angka-angka non-integer, sehingga ketiga literal numerik di atas dibulatkan ke angka floating point terdekat yang valid. Jumlah ini, seperti yang terjadi, tepat
0.01499999999999999944488848768742172978818416595458984375
yang lebih dekat ke 0,01 daripada ke 0,02.
Anda dapat melihat bahwa ketiga angka itu sama di konsol browser Anda, Node shell, atau penerjemah JavaScript lainnya. Bandingkan saja mereka:
> 0.014999999999999999 === 0.0150000000000000001
true
Jadi ketika saya menulis m = 0.0150000000000000001
, nilai pastim
yang saya dapatkan lebih dekat 0.01
daripada itu 0.02
. Namun, jika saya mengonversi m
ke String ...
> var m = 0.0150000000000000001;
> console.log(String(m));
0.015
> var m = 0.014999999999999999;
> console.log(String(m));
0.015
... Saya mendapatkan 0,015, yang seharusnya membulatkan ke 0,02, dan yang jelas bukan angka 56-desimal yang sebelumnya saya katakan bahwa semua angka-angka ini persis sama dengan. Jadi sihir gelap apa ini?
Jawabannya dapat ditemukan dalam spesifikasi skrip ECMAS , di bagian 7.1.12.1: ToString diterapkan pada tipe Number . Di sini aturan untuk mengubah beberapa Nomor m menjadi String ditetapkan. Bagian kuncinya adalah titik 5, di mana bilangan bulat s dihasilkan yang digitnya akan digunakan dalam representasi String m :
misalkan n , k , dan s adalah bilangan bulat sehingga k ≥ 1, 10 k -1 ≤ s <10 k , nilai angka untuk s × 10 n - k adalah m , dan k adalah sekecil mungkin. Perhatikan bahwa k adalah jumlah digit dalam representasi desimal s , bahwa s tidak dapat dibagi dengan 10, dan bahwa digit paling signifikan dari s tidak selalu ditentukan secara unik oleh kriteria ini.
Bagian kuncinya di sini adalah persyaratan bahwa " k adalah sekecil mungkin". Apa yang dimaksud dengan jumlah persyaratan adalah persyaratan yang, diberikan Nomor m
, nilai String(m)
harus memiliki jumlah digit yang paling mungkin sambil tetap memenuhi persyaratan itu Number(String(m)) === m
. Karena kita sudah tahu itu 0.015 === 0.0150000000000000001
, sekarang jelas mengapa String(0.0150000000000000001) === '0.015'
harus benar.
Tentu saja, tidak satu pun dari diskusi ini yang langsung menjawab apa yang roundTo2DP(m)
harus dikembalikan. Jika m
nilai tepatnya adalah 0,01499999999999999949488848768742172978818416595458984375, tetapi representasi String-nya adalah '0,015', lalu apa jawaban yang benar - secara matematis, praktis, filosofis, atau apa pun - ketika kita membulatkannya ke dua tempat desimal?
Tidak ada jawaban yang benar untuk ini. Itu tergantung pada kasus penggunaan Anda. Anda mungkin ingin menghormati representasi String dan membulatkan ke atas ketika:
- Nilai yang diwakili secara inheren diskrit, misalnya jumlah mata uang dalam mata uang 3-desimal seperti dinar. Dalam hal ini, nilai sebenarnya dari Angka seperti 0,015 adalah 0,015, dan representasi 0,0149999999 ... yang didapat di titik mengambang biner adalah kesalahan pembulatan. (Tentu saja, banyak yang akan berargumen, secara wajar, bahwa Anda harus menggunakan pustaka desimal untuk menangani nilai-nilai tersebut dan tidak pernah mewakili mereka sebagai Angka titik mengambang biner.)
- Nilai diketik oleh pengguna. Dalam kasus ini, sekali lagi, angka desimal yang dimasukkan lebih 'benar' daripada representasi titik mengambang biner terdekat.
Di sisi lain, Anda mungkin ingin menghormati nilai titik apung biner dan membulatkan ke bawah ketika nilai Anda berasal dari skala inheren terus menerus - misalnya, jika itu pembacaan dari sensor.
Kedua pendekatan ini memerlukan kode yang berbeda. Untuk menghormati representasi String dari Angka, kita dapat (dengan sedikit kode yang cukup halus) menerapkan pembulatan kita sendiri yang bertindak langsung pada representasi String, digit demi digit, menggunakan algoritma yang sama yang akan Anda gunakan di sekolah ketika Anda diajarkan bagaimana membulatkan angka. Di bawah ini adalah contoh yang menghormati persyaratan OP untuk mewakili angka ke 2 tempat desimal "hanya jika perlu" dengan menelusur membuntuti nol setelah titik desimal; Anda mungkin, tentu saja, perlu menyesuaikannya dengan kebutuhan Anda yang sebenarnya.
/**
* 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+$/, '')
}
Contoh penggunaan:
> 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'
Fungsi di atas mungkin adalah apa yang ingin Anda gunakan untuk menghindari pengguna yang pernah menyaksikan angka yang mereka masukkan dibulatkan secara salah.
(Sebagai alternatif, Anda juga bisa mencoba perpustakaan round10 yang menyediakan fungsi berperilaku serupa dengan implementasi yang sangat berbeda.)
Tetapi bagaimana jika Anda memiliki Bilangan jenis kedua - nilai yang diambil dari skala berkelanjutan, di mana tidak ada alasan untuk berpikir bahwa perkiraan representasi desimal dengan lebih sedikit tempat desimal lebih akurat daripada yang memiliki lebih banyak? Dalam hal ini, kami tidak ingin menghormati representasi String, karena representasi itu (seperti yang dijelaskan dalam spesifikasi) sudah semacam sorting; kami tidak ingin membuat kesalahan dengan mengatakan "0,014999999 ... 375 putaran hingga 0,015, yang bulat hingga 0,02, jadi 0,014999999 ... 375 putaran hingga 0,02".
Di sini kita cukup menggunakan toFixed
metode bawaan. Perhatikan bahwa dengan memanggil Number()
String yang dikembalikan oleh toFixed
, kita mendapatkan Nomor yang representasi String-nya tidak memiliki angka nol (berkat cara JavaScript menghitung representasi String dari Angka, yang dibahas sebelumnya dalam jawaban ini).
/**
* 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);
}