如何處理JavaScript 中的貨幣值?
金錢無處不在。
無論在銀行應用程式、電子商務網站還是證券交易所平臺,我們每天都在與金錢互動。我們也越來越依賴技術來處理問題。
然而,關於如何以程式設計處理貨幣價值尚無共識。雖然金錢是現代社會中普遍存在的概念,但相較於日期和時間之類的東西,它並不是任何主流語言中的一流資料型別。結果,每一種軟體都有自己的處理方式,且伴隨著陷阱。
陷阱#1:金錢是數字?
當你需要代表錢時,你的第一直覺可能是使用一個數字。
金錢只不過是一個數值,對吧?
錯了。
貨幣價值的一部分與另一物件有關:貨幣。沒有10“錢”,應該是“10美元,10歐元,10比特幣”......如果你想用不同的貨幣新增兩個貨幣值,你需要先轉換它們。如果你想比較它們也是如此:如果你只有一個金額,你就無法進行準確的比較。金額和貨幣誰也離不開誰。
陷阱#2:讓人煩惱的小數點
大多數現代貨幣額都是以小數形式出現,或根本沒有子單位。這意味著當貨幣有子單位時,主單位中這些單位的數量是10的冪。例如,一美元有100美分——10的2次冪。
使用十進位制系統具有優勢,但在程式設計方面有一個問題。計算機使用二進位制系統,因此它們不能原生地表示十進位制數。有些語言提出了自己的解決方案,如Java中的BigDecimal型別或C#中的小數型別。JavaScript只有Number型別,可以用作整數或雙精度浮點數。因為它是基礎10系統的二進位制表示,所以當你嘗試進行數學運算時,最終會得到不準確的結果。
使用浮點來儲存貨幣價值是一個壞主意。
當你計算更多值時,難以察覺的精度誤差會導致更大的差異。這不可避免地導致最終的四捨五入問題。
陷阱#3:百分比與分期
有時你需要計算分期,但百分比總是或增加或減少付款數。
想象一下,你需要支付999.99美元,首付50%。這可以通過一些簡單的數學來完成。一半是499.995美元,但是你不能再分一分錢,所以你可能會把結果分成500美元。問題是,當你付尾款時,你最終會獲得相同的結果並且額外付一分錢。
分期有時候會遇到除不盡的情況。天然氣價格可能顯示超過兩位數,但它只是象徵性的:你最終總是支付一個四捨五入的價格。
程式設計人員應該怎麼辦?
幸運的是,軟體工程師Martin Fowler提出了一個解決方案。在企業應用程式架構模式中,他描述了貨幣價值的模式:
屬性
方法
數學:加,減,乘,除
比較:等於,大於,大於或等於,小於,小於或等於。
由此,你可以建立滿足大部分貨幣需求的價值物件。
“金額+貨幣”作為資料結構
金錢的行為與簡單的數字不同,因此應區別對待。第一個也是最重要的是:它應該始終由金額和貨幣組成。
你可以將貨幣金額一起新增,檢查它們的值是否相對應,並將它們格式化為你需要的任何內容。這可以通過物件的方法完成。在JavaScript中,任何返回物件的函式都可以解決問題。
以美分計額
有幾種方法可以解決JavaScript中的浮點問題。
你可以使用像Decimal.js這樣的庫來將浮點數作為字串。這不是一個糟糕的解決方案,當你必須處理大數字時,它會派上用場。然而,它以增重依賴性為代價,導致效能降低。
你可以在計算之前將浮點數乘以整數,然後將它們分開。
這是一個很好的解決方案,但需要在物件構造或每次操作時進行額外的計算。這不一定會影響效能,但仍然需要更多的流程工作。
第三種方法是直接以美分為單位儲存相對於單位的值。如果你需要儲存10美分,則不會儲存0.1,而是10.這允許你僅使用整數,這意味著安全計算(直到你遇到大數字)和出色的效能。
Dinero.js:一個用於建立、計算和格式化貨幣價值的不可變庫
從以上觀察中,我建立了一個JavaScript庫:Dinero.js。
Dinero.js遵循Fowler的模式更多一點兒。它允許你在JavaScript中建立、計算和格式化貨幣值。你可以進行數學運算、解析和格式化物件,甚至向他們提問,使你的開發過程更加輕鬆。
該庫設計為不可變和可連結的模式。它支援全域性設定,具有擴充套件格式選項,並提供本機國際化支援。
為什麼不可變?
不可變庫更安全,更好預測。可變操作和引用副本是許多錯誤的來源。選擇不變效能夠避免了這些錯誤。
使用Dinero.js,你可以執行計算而無需擔心更改原始用例。在以下Vue.js示例中,呼叫priceWithTax時不會更改Price。如果用例是可變的,它將會更改價格。
可連結性
優秀的開發人員努力使他們的程式碼更簡潔,更易於閱讀。當你想要在單個物件上連續執行多個操作時,連結提供了優雅的符號和簡潔的語法。
全球設定
當你處理大量貨幣價值時,你可能希望其中一些人分享一些屬性。如果你使用德語製作網站,你可能希望以德國貨幣格式顯示金額。
這是全球設定派上用場的地方。你可以宣告將應用於所有新物件的選項,而不是將它們傳遞給每個用例。
原生國際化支援
傳統意義上,庫使用區域設定檔案進行國際化。
區域設定檔案也很難維護。Internationalization API是原生的,並且得到了很不錯的支援。除非你必須使用過時的或不知名的瀏覽器,否則toFormat可以安全使用。
格式化
物件很適合儲存資料,但在顯示資料時卻沒那麼有用。Dinero.js提供了各種格式化方法,包括toFormat。它為Number.prototype.toLocaleString提供了直觀而簡潔的語法。將它與setLocale配對,你將能夠以任何語言將任何Dinero物件顯示為正確的格式。這對多語言電子商務網站特別有用。
接下來做什麼?
大家廣泛認同Fowler模式是一個不錯的解決方案。它激發了許多語言的同步實現。如果你正在DIY,我推薦它和本文的觀察結果作為起點。或者你可以選擇Dinero.js:一種現代,可靠,經過全面測試的解決方案,已經可以使用。