dinero is a Clojure library designed for managing monetary amounts and currencies. It offers support for precise arithmetic operations, formatting, and parsing, all tailored to different locales and currencies.
This library supports two types of monetary amounts:
money
: represents monetary amounts with arbitrary precision and scale, ensuring accurate numerical computations during arithmetic operations.rounded money
: represents monetary amounts that are automatically rounded after each arithmetic operation, ensuring consistent precision.
It also offers functionality to format monetary amounts and parse strings representing monetary amounts. It takes care of different formats and currencies based on the user’s locale settings, ensuring accurate and locale-sensitive representations.
In terms of currencies, it supports all ISO 4217 currencies by default, allowing for seamless handling of all internationally recognized currencies like the Euro (EUR
) and British Pound (GBP
). You can also extend support to non-ISO currencies, such as cryptocurrencies like Bitcoin (BTC
), or even custom currencies specific to your domain, through an EDN configuration file.
Clojure CLI/deps.edn:
io.github.sernamar/dinero {:mvn/version "0.1.2"}
Leiningen/Boot:
[io.github.sernamar/dinero "0.1.2"]
Before using this library, you can configure the default currency and default rounding mode using a configuration file named config.edn
, which should be located in the root directory of the project.
For example, to set euros as the default currency and the half even method as the default rounding method, the config.edn
file should have the following content:
{:default-currency :eur
:default-rounding-mode :half-even}
This library support two types of monetary amounts:
money
: represents monetary amounts with arbitrary precision and scale, ensuring accurate numerical computations during arithmetic operations.rounded money
: represents monetary amounts that are automatically rounded after each arithmetic operation.
You can use money-of
to create monetary amounts with arbitrary precision and scale. It accepts two arguments:
amount
: can be either an integer, a floating-point number, aBigDecimal
or aString
.currency
: must be a keyword.
If the currency
is not specified, the money-of
function will use the default currency set in the configuration file. If a currency is provided, it will override the default and use the specified currency instead:
(require '[dinero.core :as dinero])
(dinero/money-of 1)
;; => {:amount 1M, :currency :eur}
(dinero/money-of 1 :gbp)
;; => {:amount 1M, :currency :gbp}
For convenience, you can use the with-currency
macro to bind the default currency in the macro’s body. This way, if the default currency isn’t set in the configuration file, you won’t need to specify the currency when creating monetary amounts:
(dinero/with-currency :gbp
(dinero/money-of 1))
;; => {:amount 1M, :currency :gbp}
If none the amount
nor the currency
is specified, it uses 0
as the amount, and the default currency as the currency:
(dinero/money-of)
;; => {:amount 0M, :currency :eur}
Although you can use either an integer, a floating-point number, a BigDecimal
or a String
as the amount
argument, they are all parsed and stored as BigDecimals
:
(dinero/money-of 1 :eur)
;; => {:amount 1M, :currency :eur}
(dinero/money-of 1.23 :eur)
;; => {:amount 1.23M, :currency :eur}
(dinero/money-of 1.23M :eur)
;; => {:amount 1.23M, :currency :eur}
(dinero/money-of "1.23" :eur)
;; => {:amount 1.23M, :currency :eur}
WARNING:
Be aware that using floating-point numbers can lead to rounding errors in certain calculations, as Clojure reads them as
Doubles
, which are 64-bit IEEE 754 floating point numbers. This means you get double precision, with about 15-17 significant decimal digits of precision.So for accurate numerical computations, especially in financial applications, don’t use floating-point numbers; use
BigDecimals
orStrings
instead, as they will be parsed accurately.;;; Not safe, as it uses a floating-point number, losing precision when parsing (dinero/money-of 0.123456789123456789123456789 :eur) ;; => {:amount 0.12345678912345678M, :currency :eur} ;;; Safe, as it uses BigDecimal or String (dinero/money-of 0.123456789123456789123456789M :eur) ;; => {:amount 0.123456789123456789123456789M, :currency :eur} (dinero/money-of "0.123456789123456789123456789" :eur) ;; => {:amount 0.123456789123456789123456789M, :currency :eur}
You can use rounded-money-of
to create monetary amounts that are automatically rounded after each arithmetic operation. This function accepts up to 4 arguments:
amount
: can be an integer, a floating-point number, aBigDecimal
or aString
.currency
: must be a keyword.scale
: the number of decimal places to which the amount will be rounded.rounding-mode
: the rounding mode to use when rounding the amount.
If currency
is not specified, the rounded-money-of
function will use the default currency from the configuration file. If scale
is not provided, the minor units of the given currency will be used. If rounding-mode
is not specified, the default rounding mode will be applied (or :half-even
if the default rounding mode is not set in the configuration file):
(dinero/rounded-money-of 1234.5678 :eur)
;; => {:amount 1234.57M, :currency :eur, :scale 2, :rounding-mode :half-even}
(dinero/rounded-money-of 1234.5678 :eur 0)
;; => {:amount 1235M, :currency :eur, :scale 0, :rounding-mode :half-even}
(dinero/rounded-money-of 1234.5678 :eur 0 :down)
;; => {:amount 1234M, :currency :eur, :scale 0, :rounding-mode :down}
Given a monetary amount, you can get its amount and currency using the get-amount
and get-currency
functions:
(let [money (dinero/money-of 1 :eur)]
(dinero/get-amount money))
;; => 1M
(let [money (dinero/money-of 1 :eur)]
(dinero/get-currency money))
;; => :eur
For rounded monetary amounts, you can also use the get-scale
and get-rounding-mode
functions to retrieve the scale and rounding mode applied during the rounding process:
(let [money (dinero/rounded-money-of 1 :eur)]
(dinero/get-scale money))
;; => 2
(let [money (dinero/rounded-money-of 1 :eur)]
(dinero/get-rounding-mode money))
;; => :half-even
This library supports all ISO 4217 currencies by default, providing seamless handling of all internationally recognized currencies like the Euro (EUR
) and British Pound (GBP
). Additionally, you can extend support to non-ISO currencies, such as cryptocurrencies like Bitcoin (BTC
), or even custom currencies specific to your domain, by editing the resources/currencies.edn
file. The format for defining currencies is as follows:
{:eur {:type :iso-4217, :currency-code "EUR", :minor-units 2},
:gbp {:type :iso-4217, :currency-code "GBP", :minor-units 2},
:btc {:type :crypto, :currency-code "BTC", :symbol "₿", :minor-units 8}}
For ISO 4217 currencies, the :symbol
key should not be used, as the library automatically relies on the symbol defined by the locale. For example, the British Pound (GBP
) is represented as £
in java.util.Locale/UK
, while in java.util.Locale/FRANCE
, it appears as £GB
. This ensures that the correct symbol is displayed based on the user’s locale settings.
For non-ISO currencies, such as Bitcoin, the :symbol
key is required because they are not supported by java.util.Locale
. Since their symbols are not locale-specific, we define a single symbol in the resources/currencies.edn
file, which is used consistently across all locales.
This approach provides flexibility in handling both standardized and custom currencies, allowing your application to adapt to a wide range of monetary systems.
As already mentioned, monetary amounts could be stored internally with more decimal places than the smallest unit of the currency. Although this may be important for accurate numerical computations, you might be interested in displaying amounts in a user-friendly format.
To display monetary amounts in a user-friendly format, you can use the format-money
function. This function will convert the internal representation of the monetary amount into a string with a more readable format.
The format-money
function accepts a map of configuration options as its second argument. The available options are:
- locale
- rounding-mode
- decimal-places
- symbol-style: accepts either
:symbol
(default) or:code
.
For example:
(require '[dinero.core :as dinero]
'[dinero.format :as format])
(let [m1 (dinero/money-of 1234.5678 :eur)
germany java.util.Locale/GERMANY]
(println (format/format-money m1 {:locale germany}))
(println (format/format-money m1 {:locale germany :symbol-style :code}))
(println (format/format-money m1 {:locale germany :rounding-mode :down :symbol-style :code}))
(println (format/format-money m1 {:locale germany :rounding-mode :down :decimal-places 0 :symbol-style :code})))
;; 1.234,57 €
;; 1.234,57 EUR
;; 1.234,56 EUR
;; 1.234 EUR
You can also use the format-money-with-pattern
function, which uses the given formatting pattern to format the monetary amount. This function also accepts a map of configuration options as its third argument, supporting these options:
- locale
- rounding-mode
For example:
(let [m1 (dinero/money-of 1234.5678 :eur)
germany java.util.Locale/GERMANY]
(println (format/format-money-with-pattern m1 "#,##0.00 ¤" {:locale germany}))
(println (format/format-money-with-pattern m1 "#,##0.00 ¤¤" {:locale germany}))
(println (format/format-money-with-pattern m1 "#,##0.00 euros" {:locale germany}))
(println (format/format-money-with-pattern m1 "#,##0.000 ¤" {:locale germany}))
(println (format/format-money-with-pattern m1 "#,##0 ¤" {:locale germany}))
(println (format/format-money-with-pattern m1 "#,##0 ¤" {:locale germany :rounding-mode :down})))
;; 1.234,57 €
;; 1.234,57 EUR
;; 1.234,57 euros
;; 1.234,568 €
;; 1.235 €
;; 1.234 €
This library supports parsing strings with both ISO 4217 currencies (e.g., Euro) and non-ISO 4217 currencies (e.g., Bitcoin), whether they use currency symbols (e.g., €
or ₿
) or currency codes (e.g., EUR
or BTC
).
To parse a string representing a monetary amount, use the parse-string
function, which accepts a map of configuration options as its second argument. The available options are:
:locale
: ajava.util.Locale
object used for parsing. IfNIL
, the default locale is used.:currencies
: a sequence of currencies to attempt during parsing. IfNIL
, it defaults to either the configured currency or the locale’s default currency.:try-all-currencies?
: a boolean flag. IfTRUE
, the function will attempt to parse the string using all currencies available inresources/currencies.edn
if the provided currencies fail. Defaults toFALSE
.
(require '[dinero.parse :as parse])
(parse/parse-string "1.234,56 €" {:locale java.util.Locale/GERMANY})
;; => {:amount 1234.56M, :currency :eur}
(parse/parse-string "1.234,56 EUR" {:locale java.util.Locale/GERMANY})
;; => {:amount 1234.56M, :currency :eur}
(parse/parse-string "1.234,56 £" {:locale java.util.Locale/GERMANY :currencies [:eur :gbp]})
;; => {:amount 1234.56M, :currency :gbp}
(parse/parse-string "1.234,56 GBP" {:locale java.util.Locale/GERMANY :currencies [:eur :gbp]})
;; => {:amount 1234.56M, :currency :gbp}
(parse/parse-string "1.234,56 £" {:locale java.util.Locale/GERMANY :try-all-currencies? true})
;; => {:amount 1234.56M, :currency :gbp}
(parse/parse-string "1.234,56 ₿" {:locale java.util.Locale/GERMANY :currencies [:btc]})
;; => {:amount 1234.56M, :currency :btc}
(parse/parse-string "1.234,56 BTC" {:locale java.util.Locale/GERMANY :currencies [:btc]})
;; => {:amount 1234.56M, :currency :btc}
(parse/parse-string "1.234,56 ₿" {:locale java.util.Locale/GERMANY :try-all-currencies? true})
;; => {:amount 1234.56M, :currency :btc}
(parse/parse-string "1.234,56 ₿" {:locale java.util.Locale/GERMANY :currencies [:eur :gbp] :try-all-currencies? true})
;; => {:amount 1234.56M, :currency :btc}
The parse-string
function is capable of differentiating between the same currency symbol used in different locales. For example, the dollar sign ($
) represents both US dollars (USD
) and Canadian dollars (CAD
), depending on the locale:
(parse/parse-string "$1,234.56" {:locale java.util.Locale/US})
;; => {:amount 1234.56M, :currency :usd}
(parse/parse-string "$1,234.56" {:locale java.util.Locale/CANADA})
;; => {:amount 1234.56M, :currency :cad}
If parse-string
cannot recognize the format or the currency in the string, it throws a java.text.ParseException
:
;; unrecognized format for java.util.Locale/GERMANY
(parse/parse-string "1,234.56 €" {:locale java.util.Locale/GERMANY})
;; Unhandled java.text.ParseException
;; Unparseable number: "1,234.56 €"
;; unrecognized currency for java.util.Locale/GERMANY
(parse/parse-string "1.234,56 £" {:locale java.util.Locale/GERMANY})
;; Unhandled java.text.ParseException
;; Unparseable number: "1.234,56 £"
;; unrecognized currency for any java.util.Locale
(parse/parse-string "1.234,56 ₿" {:locale java.util.Locale/GERMANY})
;; Unhandled java.text.ParseException
;; Unparseable number: "1.234,56 ₿"
You could use the following functions to do equality and comparison operations on monetary amounts: =
, not=
, money<
, money<=
, money>
, money>=
, money-zero?
, money-pos?
, and money-neg?
.
For example:
(require '[dinero.core :as dinero])
(let [m1 (dinero/money-of 1 :eur)
m2 (dinero/money-of 1 :eur)]
(= m1 m2))
;; => true
(let [m1 (dinero/money-of 1 :eur)
m2 (dinero/money-of 1 :gbp)]
(= m1 m2))
;; => false
(let [m1 (dinero/money-of 1 :eur)
m2 (dinero/money-of 2 :eur)]
(not= m1 m2))
;; => true
(let [m1 (dinero/money-of 1 :eur)
m2 (dinero/money-of 2 :eur)]
(dinero/money< m1 m2))
;; => true
(let [m1 (dinero/money-of 1 :eur)
m2 (dinero/money-of 2 :eur)]
(dinero/money> m1 m2))
;; => false
(let [money (dinero/money-of 0 :eur)]
(dinero/money-zero? money))
;; => true
(let [money (dinero/money-of -1 :eur)]
(dinero/money-pos? money))
;; => false
(let [money (dinero/money-of -1 :eur)]
(dinero/money-neg? money))
;; => true
You can use add
, substract
, multiply
, and divide
to perform arithmetic operations on monetary amounts:
(require '[dinero.core :as dinero])
(let [m1 (dinero/money-of 1 :eur)
m2 (dinero/money-of 1 :eur)]
(dinero/add m1 m2))
;; => {:amount 2M, :currency :eur}
(let [m1 (dinero/money-of 1 :eur)
m2 (dinero/money-of 1 :eur)]
(dinero/subtract m1 m2))
;; => {:amount 0M, :currency :eur}
(let [money (dinero/money-of 1 :eur)
factor 2]
(dinero/multiply money factor))
;; => {:amount 2M, :currency :eur}
(let [money (dinero/money-of 2 :eur)
divisor 2]
(dinero/divide money divisor))
;; => {:amount 1M, :currency :eur}
Note that add
and substract
can be used to add and substract more than two monetary amounts:
(let [m1 (dinero/money-of 1 :eur)
m2 (dinero/money-of 2 :eur)
m3 (dinero/money-of 3 :eur)]
(dinero/add m1 m2 m3))
;; => {:amount 6M, :currency :eur}
(let [m1 (dinero/money-of 3 :eur)
m2 (dinero/money-of 2 :eur)
m3 (dinero/money-of 1 :eur)]
(dinero/subtract m1 m2 m3))
;; => {:amount 0M, :currency :eur}
Adding or substracting monetary amounts with different currencies throws an ExceptionInfo
exception:
(let [m1 (dinero/money-of 1 :eur)
m2 (dinero/money-of 1 :gbp)]
(dinero/add m1 m2))
;; clojure.lang.ExceptionInfo
;; Currencies do not match
;; {:currencies (:eur :gbp)}
As previously mentioned, money amounts could be stored internally with more decimal places than the smallest unit of the currency. But some applications might require operating with amounts rounded to the smallest unit of currency. In such cases, you can use a monetary amount of type rounded, but you can also use the round
function to adjust the monetary amount accordingly.
By default, the round
function rounds amounts to the smallest unit of the currency, using the default rounding mode specified in the configuration file (if no rounding mode is configured, it defaults to :half-even
):
(require '[dinero.core :as dinero]
'[dinero.rounding :as rounding])
(let [m1 (dinero/money-of 1.555 :eur)
m2 (dinero/money-of 1.555 :eur)]
(dinero/add m1 m2))
;; => {:amount 3.110M, :currency :eur}
(let [m1 (dinero/money-of 1.555 :eur)
m2 (dinero/money-of 1.555 :eur)
m1-rounded (rounding/round m1)
m2-rounded (rounding/round m2)]
(dinero/add m1-rounded m2-rounded))
;; => {:amount 3.12M, :currency :eur}
But you can also speficy the number of decimal places and the rounding mode you want to use when rounding. For example:
(let [m1 (dinero/money-of 1.555 :eur)
m2 (dinero/money-of 1.555 :eur)
m1-rounded (rounding/round m1 0 :half-even)
m2-rounded (rounding/round m2 0 :half-even)]
(dinero/add m1-rounded m2-rounded))
;; => {:amount 4M, :currency :eur}
If necessary, you can also call round
with two arguments, which are the monetary amount and a custom rounding funtion to use to round the monetary amount. This allows you to specify different rounding rules for certain cases.
For example, the Swiss Franc (CHF
) uses unique rounding rules because the smallest unit of currency in Switzerland is the 5-centime (0.05 CHF
) coin. To handle the specific rounding requirements for Swiss Francs, you can use the chf-rounding-fn
function, which containins a rounding function tailored to CHF
:
(let [money (dinero/money-of 1.024 :chf)]
(rounding/round money rounding/chf-rounding-fn))
;; => {:amount 1.00M, :currency :chf}
(let [money (dinero/money-of 1.025 :chf)]
(rounding/round money rounding/chf-rounding-fn))
;; => {:amount 1.05M, :currency :chf}
This approach is also useful when formatting currencies with special rounding requirements. For instance, when formatting Swiss Francs, you might want to round the amount before using the format
function to ensure the displayed value matches the currency’s rounding conventions:
(let [money (dinero/money-of 1.025 :chf)]
(format/format-money money {:locale (java.util.Locale. "de" "CH")}))
;; => "CHF 1.02"
(let [money (dinero/money-of 1.025 :chf)
rounded-money (rounding/round money rounding/chf-rounding-fn)]
(format/format-money rounded-money {:locale (java.util.Locale. "de" "CH")}))
;; => "CHF 1.05"
This library provides several functions to convert monetary amounts between currencies using various sources for exchange rates.
The simplest function is convert-using-exchange-rate
, where you provide the exchange rate for the conversion:
(require '[dinero.core :as dinero]
'[dinero.conversion.core :as conversion])
(let [money (dinero/money-of 1 :eur)]
(conversion/convert-using-exchange-rate money :gbp 0.8))
;; => {:amount 0.8M, :currency :gbp}
In addition to this, you can use other functions designed for specific use cases, whether you’re retrieving exchange rates from external providers or custom databases.
For example, to perform currency conversion using a database, use convert-using-db
. This function requires, besides the monetary amount and target currency for the conversion (and optionally, the date), a database connection along with schema details (such as the table name, fields for the base and target currencies, the exchange rate field, and the date field if needed):
(require '[next.jdbc :as jdbc])
(defonce db (jdbc/get-datasource {:dbtype "h2:mem" :dbname "readme-db"}))
(jdbc/execute-one! db ["CREATE TABLE exchange_rate (from_currency VARCHAR(3), to_currency VARCHAR(3), rate DOUBLE, date DATE)"])
(jdbc/execute-one! db ["INSERT INTO exchange_rate (from_currency, to_currency, rate, date) VALUES ('EUR', 'GBP', 0.80, '2024-09-08')"])
(let [money (dinero/money-of 1 :eur)]
(conversion/convert-using-db money :gbp db "exchange_rate" "from_currency" "to_currency" "rate"))
;; => {:amount 0.8M, :currency :gbp}
(let [money (dinero/money-of 1 :eur)
date (java.time.LocalDate/parse "2024-09-08")]
(conversion/convert-using-db money :gbp date db "exchange_rate" "from_currency" "to_currency" "rate" "date"))
;; => {:amount 0.8M, :currency :gbp}
Additionally, you can retrieve exchange rates from external providers. Currently, the library supports exchange rates from the European Central Bank (ECB) for both current and historical (up to 90 days) data, as well as from Coinbase for both current and historical Bitcoin exchange rates:
(let [money (dinero/money-of 1 :eur)]
(conversion/convert-using-ecb money :gbp))
;; => {:amount 0.84375M, :currency :gbp}
(let [money (dinero/money-of 1 :eur)
date (java.time.LocalDate/of 2024 9 11)]
(conversion/convert-using-ecb money :gbp date))
;; => {:amount 0.84375M, :currency :gbp}
(let [money (dinero/money-of 1 :btc)]
(conversion/convert-using-coinbase money :eur))
;; => {:amount 52394.05M, :currency :eur}
(let [money (dinero/money-of 1 :btc)
date (java.time.LocalDate/of 2024 9 11)]
(conversion/convert-using-coinbase money :eur date))
;; => {:amount 52314.756527447545254192M, :currency :eur}
Copyright © 2024 Sergio Navarro
Distributed under the Apache License, Version 2.0.