1#![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 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#[derive(Debug, Clone, PartialEq, Eq)]
123pub enum Flag {
124 Fund,
126 Special,
128 Superseded(Currency),
130}
131
132impl From<Country> for Currency {
133 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 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}