一、回填之後,12.30 再也回不去 12.30
上週把一張對帳表統一成 2 位小數,匯出之前順手取消了補零。第二天同事來問:3 月那行 12.30 怎麼只剩 12.3,兩位的帳面精度對不上了。遺失發生在格式化的那一刻,回寫之後找不到原始字串。
那一頁很薄:輸入框、一根保留小數位數的滑桿、一個捨入方式下拉、補零與千分位兩個勾選框,外加「處理數值數」和「跳過無效項」兩個計數。滑桿只能停在 0 到 10,一共十一檔,預設 2 位;除此之外頁面上沒有任何關於取整方向、精度損失、科學計數法的提示文案。
二、四個捨入選項在負數上只剩兩個方向
下拉裡排著四件事:四捨五入、向下取整(地板)、向上取整(天花板)、截斷(直接捨去)。把 -2.5 取 0 位,四條分別給 -3、-2、-3、-2——四個名字、兩個結果:floor 與 trunc 逐條同值,round 與 ceil 在 .5 上同值,「四種模式給出四種結果」在這裡不成立。
原因在唯一的算式裡:四個分支都先對絕對值取整,再按符號把負號補回來。取整發生在絕對值軸上,負數上的「向下」其實是朝零,落到 -2。跟原生函數對一遍更清楚:Math.floor(-2.5) 是 -3,頁面的向下取整給 -2;Math.round(-2.5) 是 -2,頁面的四捨五入給 -3。-2.4 更徹底:-2、-2、-3、-2,三缺一。
圖 1:-2.5 取 0 位只剩兩個落點
三、1.005 保留 2 位為什麼得到 1.00
這一段沒有方向可言,只有二進制近似值落在哪一側。第三位小數正好是 5 的 8 個樣本,取 2 位、四捨五入,5 個被捨下、3 個被進上:
| 輸入 | 乘 100 的中間值 | 輸出 |
|---|---|---|
| 1.005 | 100.49999999999999 | 1.00 |
| 8.995 | 899.4999999999999 | 8.99 |
| 1.015 | 101.49999999999999 | 1.01 |
| 1.045 | 104.5 | 1.05 |
| 1.055 | 105.5 | 1.06 |
| 2.675 | 267.5 | 2.68 |
分界不是「見 5 就進」,而是乘完 100 之後那個二進制近似值有沒有嚴格壓過 .5:100.49999999999999 在 100.5 之下,捨;104.5 正好落在 .5 上,進。整條鏈就是乘一次 10 的位數次方、取整、再除回去,沒有字串移位、也沒有十進位庫參與——代碼裡那句「修正負數四捨五入」的註釋只修了符號,沒修精度。錢數只要第三位可能是 5,方向就由不得我。
四、補零一關,尾隨零永久遺失
回到開頭那次故障。補零預設是勾著的,標籤寫著「補零(如 3.5 → 3.50)」。勾著時 12.3 取 2 位得 12.30,取消勾選就只出來 12.3;同一批裡 100 從 100.00 退回 100,1.005 更直接——連小數都不剩,輸出 1。尾隨零在數值層面本來就不存在,輸出的是字串,回寫後沒有任何地方還留著「原本按 2 位記」。
位數 0 時補零完全不介入:5 仍是 5、5.5 截斷仍是 5,勾得再堅決也看不到 .00。位數檔位本身也不負責補零,12.3 取 4 位、關著補零,輸出還是 12.3。
五、批次貼上:三條版本號全部「成功」,其中一條已變成整數
批次入口是真的:輸入按換行、半形逗號、全形逗號三種分隔切成 token,一次跑完整批,全形與半形完全等價。我把一列版本號貼進去,取 1 位、選截斷。
圖 2:三行版本號 count=3、skipped=0,其中一行已被截成整數
沒有一行報錯,也沒有一行被標掉,跳過無效項老老實實是 0,第二條其實只剩一個 10。計數只回答「能不能讀成數」,不回答「讀成了什麼」。
混排一批更能說明問題:5 行文字得到 tokens=6、count=4、skipped=2——帶千分位的那一行被逗號切成 2 個 token,於是編號 #1 到 #6 比行數多,它是切分之後的下標、不等於行號。同一批裡 2024.01.15 被寫成 2024.01、1.2.3 被寫成 1.20,而「價格 3.5 元」和 v1.2.3 整行判無效:它只認每個 token 開頭的那一個數,行中間的 3.5 不會被提取。
「複製全部」是第二條管線,無效項按原文回填:那批貼出來是 1.00|234.56|价格 3.5 元|v1.2.3|2024.01|1.20,螢幕上標紅的 2 行變成 2 行原文混在同一份資料裡。反過來說,整批全無效時結果面板也不回落空態:三行 abc 給 tokens=3、skipped=3,列表照樣顯示,只有完全空的輸入才退回「請輸入數值後自動格式化」。
六、越界的那一頭:1e21 與長整數
1e21 取 2 位輸出 999999999999999900000.00——只是乘 100 再除 100,原值少了 100000000000;取 0 位時不乘不除,科學計數法原樣透出 1e+21。1e-7 取 10 位再補零得到畸形串 1e-7.0000000000,千分位在這種指數串上一個逗號也加不上,同一個數取 2 位則直接是 0.00。
長整數更安靜:9007199254740993 進了 parseFloat 就成了 9007199254740992,末位變了而介面看不出區別;123456789012345.67 取 2 位被寫成 123456789012345.69,整數部分已經吃掉 15 位有效數字。訂單號、卡號這類值不該進這條路,想按別的進位制讀它們得去進位轉換那篇——同一串字元,這一頁連 0x10 都不認。
這幾個開關在小數位數格式化那一頁都能復現:同一份輸入只動一個勾選框,把兩個計數一起讀。跑完一批,我會把輸入與輸出各貼一份進兩段文字對比,確認差額只落在該落的那幾行。
FAQ
Q:負數上的「向下取整」到底往哪邊?
往零。四個分支都對絕對值取整再補負號,-2.5 取 0 位給 -2,與截斷同值;真正的向下是 -3,這一檔反倒由「向上取整」給出。
Q:能拿它統一金額保留 2 位嗎?
第三位可能是 5 的時候不行:1.005 → 1.00、8.995 → 8.99,而 1.045 → 1.05、2.675 → 2.68。要它當顯示層可以,當結算層不行。
Q:整批都是無效數值,為什麼列表還在?
回到空態要求兩個計數同時為 0,三行 abc 是 count=0、skipped=3。列表非空不等於跑成功,那兩個數才是要讀的東西。