建置為什麼一直是壞的?

tsconfig 開了 verbatimModuleSyntax,型別必須用 import type 匯入; 又開了 noUnusedLocals,未使用的變數直接報錯。兩條規則加起來讓 tsc -b 失敗, 而 repo 沒有 CI 會把這件事攤出來。修完之後又撞到第二層:放在 src/ 裡的測試檔 沒有 Node 型別,同樣會讓 tsc -b 失敗。解法是拆出獨立的 tsconfig.test.json, 由主 tsconfig.json 以 references 引用,app 和測試各自用正確的型別環境。

為什麼 IRR 的答案剛好都是 -45.96%?

保險 IRR 用二分法解,月利率的搜尋下限寫死在 -5%。換算成年利率, 0.95 的 12 次方減 1 等於 -45.96%,所以任何虧損超過這個幅度的保單, 答案都被釘在區間邊界上,看起來像一個「算出來的數字」。

新的解法把月利率區間放寬到 -99.9% 到 +100%。區間一放寬就有溢位風險: 1200 個月的保單用大幅負利率折現,分母會趨近 0。所以 r ≥ 0 時算現值、 r < 0 時改算終值,兩者正負號相同,二分法的判斷不受影響,也不會溢位。 修正後同一張保單算出 -68.10%,再用一份獨立寫的 Python 求解器交叉驗算,結果一致。

匯率與日期:兩個「字串當數字」的錯

匯率頁顯示 Invalid Date。原本串的 API 回傳的 date 是「YYYY-MM-DD」字串, 程式卻把它乘以 1000 當成 Unix 秒數。改用不需要金鑰的 open.er-api.com v6, 讀它的 time_last_update_unix 欄位。

日期計算則有兩個方向相反的時區錯。用 new Date 解析「YYYY-MM-DD」得到的是 UTC 午夜, 在 UTC 以西的地區會變成前一天;toISOString 輸出的是 UTC 日期, 在台灣(UTC+8)早上 8 點前會拿到昨天。改成用年、月、日分開組 Date, 天數差用 Date.UTC 相減,順便避開夏令時間那一小時。測試在執行時設定 process.env.TZ, Node 會即時採用,所以可以在同一份測試裡模擬不同時區。

StrictMode 下歷史紀錄為什麼重複?

計算機在 setState 的 updater 裡呼叫 setHistory。React StrictMode 開發模式會把 updater 執行兩次,用來抓有副作用的 updater,結果每次計算都存了兩筆歷史。 改成純 reducer,歷史紀錄直接放在同一份 state 裡,updater 不再有副作用。

一整批從沒生效過的樣式

每個頁面都用 .layout :global(.displayArea) 覆寫版面,但 CalculatorLayout 只輸出 CSS Modules 雜湊過的 class 名稱,全域的 displayArea 根本不存在,這些規則一條都沒命中過。 修法是在元件上同時輸出穩定的全域 class 名稱。另外全站沒有設定 box-sizing: border-box, padding 會把寬度撐出容器,一併補上。

i18n 也是半成品:8 種語系有 6 種是空的。改成每個語系一個檔案, 型別宣告 Translation = typeof zhTw,任何語系少一個 key 或多一個 key 都會變成編譯錯誤。 語言偵測順序裡的 queryString 大小寫寫錯(正確是 querystring),等於這個來源從沒被讀過; 另外用 convertDetectedLanguage 把 en、zh-HK 這類代碼對應到實際有的語系。

驗證時差點被騙的兩次

第一次:在 preview 裡看 PWA build,畫面怎麼改都是舊的。原因是 Service Worker 還在供應上一版的快取,必須先 unregister、清掉 caches,截圖才反映真正的 build。

第二次:用瀏覽器自動化工具輸入文字,鍵盤快捷鍵看起來全部失效。 實際上那個 type 動作不會觸發 keydown,改用真正的 KeyboardEvent 派送,處理器都正常。 如果相信了第一個結果,就會去「修」一段沒壞的程式。

最後的驗證數字:lint 0 錯誤、137/137 測試通過、覆蓋率 97.97% statements、 95.81% branches,build 成功,GitHub CI 第一次跑就全綠, 13 個頁面在 375×812 的 build preview 裡逐頁檢查過。

關鍵教訓

答案剛好等於邊界值時,先懷疑搜尋區間:-45.96% 正是 0.95¹² − 1,數字整齊得可疑,代表求解器撞牆而不是收斂。

數值方法要有獨立的對照:用另一種語言、另一份實作重算一次,才能確認 -68.10% 是答案而不是新的 bug。

日期字串不要直接丟給 new Date:「YYYY-MM-DD」會被當成 UTC,toISOString 也是 UTC;在 UTC+8 早上 8 點前一定出錯。

讓型別系統替 i18n 把關:Translation = typeof 主語系,缺 key 就編譯失敗,比人工比對 8 份檔案可靠。

驗證工具本身也會說謊:Service Worker 快取與不送 keydown 的模擬輸入,都會讓正確的程式看起來壞掉,先確認觀察方法再修。

來源:個人開發日誌 2026-11-04 · 萬能計算機(React 19 + Vite 7 PWA)· 137/137 測試 · 覆蓋率 97.97% / 95.81% · 13 頁 375×812 實測