2025年1月4日 星期六

fixed 印出小數點部份

the 1st edition: 20210415
the 2nd edition: 20260113
對「Re: [討論] 請大家聊聊 JavaScript的缺陷」這問題很好奇, c++ 也有類似的情形。

這邊可以測試 js:
https://www.w3schools.com/jsref/tryit.asp?filename=tryjsref_tofixed

fn1.html
 1 <!DOCTYPE html>
 2 <html>
 3 <body>
 4   <p id="demo"></p>
 5   <script>
 6     // 修正點:加上 = 號
 7     var doc = document.getElementById("demo");
 8
 9     function t(v) {
10       // 修正點:加上 = 號與字串連接
11       doc.innerHTML = doc.innerHTML + '<br />' + v + " = " + v.toFixed(2);
12     }
13
14     t(0.005); // 0.01
15     t(0.015); // 0.01 (不進位)
16     t(0.025); // 0.03
17     t(0.035); // 0.04
18     t(0.045); // 0.04 (不進位)
19     t(0.055); // 0.06
20     t(0.065); // 0.07
21     t(0.075); // 0.07 (不進位)
22     t(0.085); // 0.09
23     t(0.095); // 0.10
24   </script>
25 </body>
26 </html>


fn1.html 執行結果
0.005 = 0.01
0.015 = 0.01
0.025 = 0.03
0.035 = 0.04
0.045 = 0.04
0.055 = 0.06
0.065 = 0.07
0.075 = 0.07
0.085 = 0.09
0.095 = 0.10


n.cpp 感謝 cschat lan* 提供
 1 #include <iostream>
 2 #include <string>
 3 #include <cmath>
 4 using namespace std;
 5
 6 int main(int argc, char *argv[])
 7 {
 8   std::cout.precision(2);
 9   cout << fixed << 0.005 << endl;
10   cout << 0.015 << endl;
11   cout << 0.025 << endl;
12   cout << 0.035 << endl;
13   cout << 0.045 << endl;
14   cout << 0.055 << endl;
15   cout << 0.065 << endl;
16   cout << 0.075 << endl;
17   cout << 0.085 << endl;
18   cout << 0.095 << endl;
19   return 0;
20 }


n.cpp 執行結果
0.01
0.01
0.03
0.04
0.04
0.06
0.07
0.07
0.09
0.10

藉由 ai, ref 4, 終於知道是怎麼回事, 當決定要印出小數點 2 位數時候用的演算法是「偶數捨入法」(Banker's Rounding) 這是很多繪圖、統計或會計系統的規則: 若剛好在 .5 的位置, 則捨入到最接近的「偶數」。

0.005 → 靠近 0.00 還是 0.01? 這裡 0 是偶數, 所以會趨向 0.00, 但在電腦裡 0.005 是 0.00500000000000000010 所以它判斷靠近 0.01。

0.015 → 靠近 0.01 還是 0.02? 這裡 2 是偶數, 本應往 0.02 走, 但在電腦裡 0.015 是 0.0149..., 所以它決定留在 0.01。

另外也請 ai 給出一個簡易版本的演算法, 實作 Banker's Rounding。

fn.cpp
 1 #include <iostream>
 2 #include <iomanip> // 必須包含此庫以使用 setprecision
 3 
 4 #include <cmath>
 5 #include <string>
 6 #include <cstdio>
 7 
 8 using namespace std;
 9 
10 /**
11  * 模擬 setprecision(2) + fixed 的行為
12  * @param value 要輸出的數值
13  * @param precision 小數點後位數
14  */
15 void my_print_fixed(double value, int precision) {
16     cout << fixed << setprecision(20) << value << endl;
17 
18     // 1. 取得放大倍數 (例如 precision 2 則為 100)
19     long double multiplier = std::pow(10, precision);
20     
21     // 2. 模擬底層捨入規則
22     // 注意:std::round 在這裡會反映出 0.015 儲存成 0.01499... 的事實
23     double rounded_value = std::round(value * multiplier) / multiplier;
24 
25     // 3. 格式化輸出字串
26     // 我們用 printf 的格式化字串來模擬輸出流的最後一步
27     char format[10];
28     sprintf(format, "%%.%df", precision);
29     printf("Input: %.20f | Result: ", value);
30     printf(format, rounded_value);
31     printf("\n");
32 }
33 
34 int main() {
35     double n1 = 0.005;
36     double n2 = 0.015;
37 
38     std::cout << "--- 模擬底層數值轉換 ---" << std::endl;
39     
40     // 看看 0.005 實際上在想什麼
41     my_print_fixed(n1, 2); 
42     
43     // 看看 0.015 實際上在想什麼
44     my_print_fixed(n2, 2);
45 
46     n2 = 0.025;
47     my_print_fixed(n2, 2);
48     n2 = 0.035;
49     my_print_fixed(n2, 2);
50     n2 = 0.045;
51     my_print_fixed(n2, 2);
52     n2 = 0.055;
53     my_print_fixed(n2, 2);
54     n2 = 0.065;
55     my_print_fixed(n2, 2);
56     n2 = 0.075;
57     my_print_fixed(n2, 2);
58     n2 = 0.085;
59     my_print_fixed(n2, 2);
60     n2 = 0.095;
61     my_print_fixed(n2, 2);
62     return 0;
63 }
list 5 fn.cpp 執行結果
 1 --- 模擬底層數值轉換 ---
 2 0.00500000000000000010
 3 Input: 0.00500000000000000010 | Result: 0.01
 4 0.01499999999999999944
 5 Input: 0.01499999999999999944 | Result: 0.01
 6 0.02500000000000000139
 7 Input: 0.02500000000000000139 | Result: 0.03
 8 0.03500000000000000333
 9 Input: 0.03500000000000000333 | Result: 0.04
10 0.04499999999999999833
11 Input: 0.04499999999999999833 | Result: 0.04
12 0.05500000000000000028
13 Input: 0.05500000000000000028 | Result: 0.06
14 0.06500000000000000222
15 Input: 0.06500000000000000222 | Result: 0.07
16 0.07499999999999999722
17 Input: 0.07499999999999999722 | Result: 0.07
18 0.08500000000000000611
19 Input: 0.08500000000000000611 | Result: 0.09
20 0.09500000000000000111
21 Input: 0.09500000000000000111 | Result: 0.10

另外注意 fn.cpp L19, 需要用 long double, 如果用 double 結果會是 0.015 印出來會是 0.02, 因為演算法 L23 value * multiplier 0.014999999999999999 X 100 變成是 1.50000000000000000000, 而不是 1.4999, 需要使用精度更高的 long double 才會是 1.4999。

ref:
  1. C++中cout用fixed以及setprecision 設置輸出精度時候,為什么不是四舍五入
  2. What is the rule behind toFixed() function
  3. Algorithm Number.prototype.toFixed (fractionDigits)
  4. 問 gemini 過程

沒有留言:

張貼留言

使用 google 的 reCAPTCHA 驗證碼, 總算可以輕鬆留言了。

我實在受不了 spam 了, 又不想讓大家的眼睛花掉, 只好放棄匿名留言。這是沒辦法中的辦法了。留言的朋友需要有 google 帳號。