iso_currency/
lib.rs

1//! ISO 4217 currency codes
2//!
3//! This crate provides an enum that represents all ISO 4217 currencies and
4//! has simple methods to convert between numeric and character code, list of
5//! territories where each currency is used, the symbol,
6//! and the English name of the currency.
7//!
8//! The data for this is taken from
9//! [https://en.wikipedia.org/wiki/ISO_4217](https://en.wikipedia.org/wiki/ISO_4217)
10//!
11//! The `Country` enum is re-exported from the only dependency - the [iso_country](https://crates.io/crates/iso_country) crate.
12//!
13//! # Examples
14//!
15//! ```
16//! use iso_currency::{Currency, Country};
17//!
18//! assert_eq!(Currency::EUR.name(), "Euro");
19//! assert_eq!(Currency::EUR.numeric(), 978);
20//! assert_eq!(Currency::from_numeric(978), Some(Currency::EUR));
21//! assert_eq!(Currency::from_code("EUR"), Some(Currency::EUR));
22//! assert_eq!(Currency::from_country(Country::IO), vec![Currency::GBP, Currency::USD]);
23//! assert_eq!(Currency::from(Country::AF), Currency::AFN);
24//! assert_eq!(Currency::CHF.used_by(), vec![Country::LI, Country::CH]);
25//! assert_eq!(format!("{}", Currency::EUR.symbol()), "€");
26//! assert_eq!(Currency::EUR.subunit_fraction(), Some(100));
27//! assert_eq!(Currency::JPY.exponent(), Some(0));
28//! assert_eq!(Currency::BOV.is_fund(), true);
29//! assert_eq!(Currency::XDR.is_special(), true);
30//! assert_eq!(Currency::VES.is_superseded(), Some(Currency::VED));
31//! assert_eq!(Currency::VED.is_superseded(), None);
32//! assert_eq!(Currency::VES.latest(), Currency::VED);
33//! assert_eq!(Currency::BOV.flags(), vec![iso_currency::Flag::Fund]);
34//! ```
35
36#![cfg_attr(docsrs, feature(doc_cfg))]
37
38pub use iso_country::Country;
39
40#[cfg(feature = "with-serde")]
41#[cfg_attr(docsrs, doc(cfg(feature = "with-serde")))]
42use serde::{Deserialize, Serialize};
43
44#[cfg(feature = "with-schemars")]
45use schemars::JsonSchema;
46#[cfg(feature = "iterator")]
47#[cfg_attr(docsrs, doc(cfg(feature = "iterator")))]
48use strum::EnumIter;
49#[cfg(feature = "iterator")]
50#[cfg_attr(docsrs, doc(cfg(feature = "iterator")))]
51pub use strum::IntoEnumIterator;
52
53include!(concat!(env!("OUT_DIR"), "/isodata.rs"));
54
55#[derive(PartialEq, Eq)]
56pub struct CurrencySymbol {
57    pub symbol: String,
58    pub subunit_symbol: Option<String>,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct ParseCurrencyError;
63
64impl std::fmt::Display for ParseCurrencyError {
65    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
66        write!(f, "not a valid ISO 4217 currency code")
67    }
68}
69
70impl std::error::Error for ParseCurrencyError {}
71
72impl std::fmt::Debug for CurrencySymbol {
73    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
74        write!(f, "{}", self.symbol)
75    }
76}
77
78impl std::fmt::Display for CurrencySymbol {
79    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
80        write!(f, "{}", self.symbol)
81    }
82}
83
84impl CurrencySymbol {
85    /// Represents the commonly used symbol for a currency
86    ///
87    /// Data for the symbols was collected from
88    /// [https://en.wikipedia.org/wiki/Currency_symbol#List_of_presently-circulating_currency_symbols]()
89    ///
90    pub fn new(symbol: &str, subunit_symbol: Option<&str>) -> CurrencySymbol {
91        CurrencySymbol {
92            symbol: symbol.to_owned(),
93            subunit_symbol: subunit_symbol.map(|v| v.to_owned()),
94        }
95    }
96}
97
98impl std::fmt::Debug for Currency {
99    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
100        write!(f, "{}", self.code())
101    }
102}
103
104impl std::fmt::Display for Currency {
105    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
106        write!(f, "{}", self.name())
107    }
108}
109
110impl std::str::FromStr for Currency {
111    type Err = ParseCurrencyError;
112
113    fn from_str(s: &str) -> Result<Self, Self::Err> {
114        match Self::from_code(s) {
115            Some(c) => Ok(c),
116            None => Err(ParseCurrencyError),
117        }
118    }
119}
120
121/// Extra information for a currency
122#[derive(Debug, Clone, PartialEq, Eq)]
123pub enum Flag {
124    /// The currency is a fund
125    Fund,
126    /// The currency is a special currency
127    Special,
128    /// The currency is superseded by another currency
129    Superseded(Currency),
130}
131
132impl From<Country> for Currency {
133    /// Returns the regular currency used in a country
134    ///
135    /// If a country uses multiple currencies, the first one is returned.
136    /// All currencies who are superseded by another currency are filtered out.
137    /// Same goes for funds and special currencies.
138    fn from(country: Country) -> Self {
139        Self::from_country(country)
140            .into_iter()
141            .find(|c| c.flags().is_empty())
142            .unwrap()
143    }
144}
145
146#[cfg(feature = "with-sqlx-sqlite")]
147impl sqlx::Decode<'_, sqlx::Sqlite> for Currency {
148    fn decode(value: sqlx::sqlite::SqliteValueRef<'_>) -> Result<Self, sqlx::error::BoxDynError> {
149        let code: String = sqlx::Decode::<'_, sqlx::Sqlite>::decode(value)?;
150        Currency::from_code(&code)
151            .ok_or_else(|| sqlx::error::BoxDynError::from("Invalid currency code"))
152    }
153}
154
155#[cfg(feature = "with-sqlx-sqlite")]
156impl sqlx::Type<sqlx::Sqlite> for Currency {
157    fn type_info() -> sqlx::sqlite::SqliteTypeInfo {
158        <String as sqlx::Type<sqlx::Sqlite>>::type_info()
159    }
160}
161
162#[cfg(feature = "with-sqlx-postgres")]
163impl sqlx::Decode<'_, sqlx::Postgres> for Currency {
164    fn decode(value: sqlx::postgres::PgValueRef<'_>) -> Result<Self, sqlx::error::BoxDynError> {
165        let code: String = sqlx::Decode::<'_, sqlx::Postgres>::decode(value)?;
166        Currency::from_code(&code)
167            .ok_or_else(|| sqlx::error::BoxDynError::from("Invalid currency code"))
168    }
169}
170
171#[cfg(feature = "with-sqlx-postgres")]
172impl sqlx::Type<sqlx::Postgres> for Currency {
173    fn type_info() -> sqlx::postgres::PgTypeInfo {
174        <String as sqlx::Type<sqlx::Postgres>>::type_info()
175    }
176}
177
178#[cfg(feature = "with-sqlx-mysql")]
179impl sqlx::Decode<'_, sqlx::MySql> for Currency {
180    fn decode(value: sqlx::mysql::MySqlValueRef<'_>) -> Result<Self, sqlx::error::BoxDynError> {
181        let code: String = sqlx::Decode::<'_, sqlx::MySql>::decode(value)?;
182        Currency::from_code(&code)
183            .ok_or_else(|| sqlx::error::BoxDynError::from("Invalid currency code"))
184    }
185}
186
187#[cfg(feature = "with-sqlx-mysql")]
188impl sqlx::Type<sqlx::MySql> for Currency {
189    fn type_info() -> sqlx::mysql::MySqlTypeInfo {
190        <String as sqlx::Type<sqlx::MySql>>::type_info()
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use crate::{Country, Currency, Flag, ParseCurrencyError};
197
198    #[cfg(feature = "with-serde")]
199    use std::collections::HashMap;
200
201    #[test]
202    fn return_numeric_code() {
203        assert_eq!(Currency::EUR.numeric(), 978);
204        assert_eq!(Currency::BBD.numeric(), 52);
205        assert_eq!(Currency::XXX.numeric(), 999);
206    }
207
208    #[test]
209    fn return_name() {
210        assert_eq!(Currency::EUR.name(), "Euro");
211        assert_eq!(Currency::BGN.name(), "Bulgarian lev");
212        assert_eq!(Currency::USD.name(), "United States dollar");
213    }
214
215    #[test]
216    fn return_code() {
217        assert_eq!(Currency::EUR.code(), "EUR");
218    }
219
220    #[test]
221    fn from_code() {
222        assert_eq!(Currency::from_code("EUR"), Some(Currency::EUR));
223        assert_eq!(Currency::from_code("SEK"), Some(Currency::SEK));
224        assert_eq!(Currency::from_code("BGN"), Some(Currency::BGN));
225        assert_eq!(Currency::from_code("AAA"), None);
226    }
227
228    #[test]
229    #[allow(clippy::zero_prefixed_literal)]
230    fn from_numeric() {
231        assert_eq!(Currency::from_numeric(999), Some(Currency::XXX));
232        assert_eq!(Currency::from_numeric(052), Some(Currency::BBD));
233        assert_eq!(Currency::from_numeric(978), Some(Currency::EUR));
234        assert_eq!(Currency::from_numeric(012), Some(Currency::DZD));
235        assert_eq!(Currency::from_numeric(123), None);
236    }
237
238    #[test]
239    fn used_by() {
240        assert_eq!(Currency::BGN.used_by(), vec![Country::BG]);
241        assert_eq!(Currency::CHF.used_by(), vec![Country::LI, Country::CH]);
242    }
243
244    #[test]
245    fn symbol() {
246        assert_eq!(format!("{}", Currency::EUR.symbol()), "€");
247        assert_eq!(format!("{}", Currency::XXX.symbol()), "¤");
248        assert_eq!(format!("{}", Currency::GEL.symbol()), "ლ");
249        assert_eq!(format!("{}", Currency::AED.symbol()), "د.إ");
250    }
251
252    #[test]
253    fn subunit_fraction() {
254        assert_eq!(Currency::EUR.subunit_fraction(), Some(100));
255        assert_eq!(Currency::DZD.subunit_fraction(), Some(100));
256        /* [Malagasy ariary](https://en.wikipedia.org/wiki/Malagasy_ariary) (`MRU`)
257        and the [Mauritanian ouguiya](https://en.wikipedia.org/wiki/Mauritanian_ouguiya) (`MGA`)
258        are technically divided into 5 subunits (iraimbilanja and khoum).
259        However, while they have a face value of "1/5" and are referred to as a "fifth" (Khoum/cinquième),
260        these are not used in practice. When written out, a single significant digit is used (example: 1.2 UM so that 10 UM = 1 MRU).
261        -- Source [Wikipedia](https://en.wikipedia.org/wiki/ISO_4217#cite_note-divby5-15). */
262        assert_eq!(Currency::MRU.subunit_fraction(), Some(100));
263        assert_eq!(Currency::XAU.subunit_fraction(), None);
264    }
265
266    #[test]
267    fn subunit_exponent() {
268        assert_eq!(Currency::EUR.exponent(), Some(2));
269        assert_eq!(Currency::JPY.exponent(), Some(0));
270        assert_eq!(Currency::MRU.exponent(), Some(2));
271    }
272
273    #[test]
274    #[cfg(feature = "with-serde")]
275    fn deserialize() {
276        let hashmap: HashMap<&str, Currency> = serde_json::from_str("{\"foo\": \"EUR\"}").unwrap();
277        assert_eq!(hashmap["foo"], Currency::EUR);
278    }
279
280    #[test]
281    #[cfg(feature = "with-serde")]
282    fn serialize() {
283        let mut hashmap: HashMap<&str, Currency> = HashMap::new();
284        hashmap.insert("foo", Currency::EUR);
285
286        assert_eq!(
287            serde_json::to_string(&hashmap).unwrap(),
288            "{\"foo\":\"EUR\"}"
289        );
290    }
291
292    #[test]
293    fn can_be_sorted() {
294        let mut v = vec![Currency::SEK, Currency::DKK, Currency::EUR];
295        v.sort();
296        assert_eq!(v, vec![Currency::DKK, Currency::EUR, Currency::SEK]);
297    }
298
299    #[test]
300    fn implements_from_str() {
301        use std::str::FromStr;
302        assert_eq!(Currency::from_str("EUR"), Ok(Currency::EUR));
303        assert_eq!(Currency::from_str("SEK"), Ok(Currency::SEK));
304        assert_eq!(Currency::from_str("BGN"), Ok(Currency::BGN));
305        assert_eq!(Currency::from_str("AAA"), Err(ParseCurrencyError));
306    }
307
308    #[test]
309    #[cfg(feature = "iterator")]
310    fn test_iterator() {
311        use crate::IntoEnumIterator;
312        let mut iter = Currency::iter();
313        assert_eq!(iter.next(), Some(Currency::AED));
314        assert_eq!(iter.next(), Some(Currency::AFN));
315    }
316
317    #[test]
318    fn test_is_fund() {
319        assert!(Currency::BOV.is_fund());
320        assert!(!Currency::EUR.is_fund());
321    }
322
323    #[test]
324    fn test_is_special() {
325        assert!(Currency::XBA.is_special());
326        assert!(!Currency::EUR.is_special());
327    }
328
329    #[test]
330    fn test_is_superseded() {
331        assert_eq!(Currency::VES.is_superseded(), Some(Currency::VED));
332        assert_eq!(Currency::VED.is_superseded(), None);
333    }
334
335    #[test]
336    fn test_latest() {
337        assert_eq!(Currency::VED.latest(), Currency::VED);
338        assert_eq!(Currency::VES.latest(), Currency::VED);
339    }
340
341    #[test]
342    fn test_flags() {
343        assert_eq!(Currency::BOV.flags(), vec![Flag::Fund]);
344        assert_eq!(Currency::XBA.flags(), vec![Flag::Special]);
345        assert_eq!(Currency::VES.flags(), vec![Flag::Superseded(Currency::VED)]);
346        assert_eq!(Currency::VED.flags(), vec![]);
347    }
348
349    #[test]
350    fn test_has_flag() {
351        assert!(Currency::BOV.has_flag(Flag::Fund));
352        assert!(!Currency::XBA.has_flag(Flag::Fund));
353    }
354
355    #[test]
356    fn test_from_country() {
357        assert_eq!(Currency::from_country(Country::AF), vec![Currency::AFN]);
358        assert_eq!(
359            Currency::from_country(Country::IO),
360            vec![Currency::GBP, Currency::USD]
361        );
362    }
363
364    #[test]
365    fn test_from_country_trait() {
366        assert_eq!(Currency::from(Country::AF), Currency::AFN);
367        assert_eq!(Currency::from(Country::IO), Currency::GBP);
368    }
369}