locale_codes/
currency.rs

1/*!
2Codes for the representation of currencies.
3
4Currencies can be represented in the code in two ways: a three-letter alphabetic
5code and a three-digit numeric code. The most recent edition is ISO 4217:2015.
6The purpose of ISO 4217:2015 is to establish internationally recognised codes
7for the representation of currencies.
8
9## Source - ISO 4217:2015
10
11The data used here is taken from the tables in the html page
12[ISO.org](https://www.iso.org/iso-4217-currency-codes.html). Additional data was taken from
13[Forex](https://www.forexrealm.com/additional-info/foreign-currency-symbols.html),
14and [XE](https://www.xe.com/symbols.php).
15*/
16
17use std::collections::HashMap;
18
19use serde::{Deserialize, Serialize};
20
21// ------------------------------------------------------------------------------------------------
22// Public Types
23// ------------------------------------------------------------------------------------------------
24
25/// Represents a sub-division (minor currency unit) of a currency.
26/// For example, the US Dollar (USD) has a single sub-division in that
27/// each 100th of a dollar is named a cent. This would be represented
28/// as `Subdivision { exponent: 2, name: Somme("cent") }`. Some
29/// currencies have different names for different subdivisionsm, or simply
30/// different names for the same.
31#[derive(Serialize, Deserialize, Debug)]
32pub struct Subdivision {
33    /// The exponent, or scale, of the currency unit, determining it's value.
34    pub exponent: i8,
35    /// The optional name of the currency unit, localized.
36    pub name: Option<String>,
37}
38
39/// A representation of registered currency data that maintained by ISO.
40#[derive(Serialize, Deserialize, Debug)]
41pub struct CurrencyInfo {
42    /// The  ISO 4217 registered 3-character currency code.
43    pub alphabetic_code: String,
44    /// The registered name, in English, of the currency.
45    pub name: String,
46    /// The registered numeric curency code, if it has one.
47    pub numeric_code: Option<u16>,
48    /// The localized symbol used to represent the currency, if known.
49    pub symbol: Option<String>,
50    /// These correspond approximately to _countries using
51    ///this currency_.
52    pub standards_entities: Vec<String>,
53    /// The, possibly empty set of subdivisions for this currency.
54    pub subdivisions: Vec<Subdivision>,
55}
56
57// ------------------------------------------------------------------------------------------------
58// Public Functions
59// ------------------------------------------------------------------------------------------------
60
61lazy_static! {
62    static ref CURRENCIES: HashMap<String, CurrencyInfo> = load_currencies_from_json();
63    static ref NUMERIC_LOOKUP: HashMap<u16, String> = make_currency_lookup();
64}
65
66pub fn lookup_by_alpha(alphabetic_code: &str) -> Option<&'static CurrencyInfo> {
67    assert_eq!(
68        alphabetic_code.len(),
69        3,
70        "currency code must be 3 characters long"
71    );
72    CURRENCIES.get(alphabetic_code)
73}
74
75pub fn lookup_by_numeric(numeric_code: &u16) -> Option<&'static CurrencyInfo> {
76    match NUMERIC_LOOKUP.get(&numeric_code) {
77        Some(v) => lookup_by_alpha(v),
78        None => None,
79    }
80}
81
82pub fn currency_alpha_codes() -> Vec<String> {
83    CURRENCIES.keys().cloned().collect()
84}
85
86pub fn currency_numeric_codes() -> Vec<u16> {
87    NUMERIC_LOOKUP.keys().cloned().collect()
88}
89
90pub fn currencies_for_country_name(name: &str) -> Vec<&'static CurrencyInfo> {
91    CURRENCIES
92        .values()
93        .filter(|currency| currency.standards_entities.contains(&name.to_string()))
94        .collect()
95}
96
97pub fn all_alpha_codes() -> Vec<String> {
98    CURRENCIES.keys().cloned().collect()
99}
100
101pub fn all_numeric_codes() -> Vec<u16> {
102    NUMERIC_LOOKUP.keys().cloned().collect()
103}
104
105// ------------------------------------------------------------------------------------------------
106// Generated Data
107// ------------------------------------------------------------------------------------------------
108
109fn load_currencies_from_json() -> HashMap<String, CurrencyInfo> {
110    info!("currencies_from_json - loading JSON");
111    let raw_data = include_bytes!("data/currencies.json");
112    let currency_map: HashMap<String, CurrencyInfo> = serde_json::from_slice(raw_data).unwrap();
113    info!(
114        "currencies_from_json - loaded {} currencies",
115        currency_map.len()
116    );
117    currency_map
118}
119
120fn make_currency_lookup() -> HashMap<u16, String> {
121    info!("load_currency_lookup - create from CURRENCIES");
122    let mut lookup_map: HashMap<u16, String> = HashMap::new();
123    for currency in CURRENCIES.values() {
124        if let Some(numeric) = &currency.numeric_code {
125            lookup_map.insert(*numeric, currency.alphabetic_code.to_string());
126        }
127    }
128    info!(
129        "load_currency_lookup - mapped {} countries",
130        lookup_map.len()
131    );
132    lookup_map
133}
134
135// ------------------------------------------------------------------------------------------------
136// Unit Tests
137// ------------------------------------------------------------------------------------------------
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    use serde_json::ser::to_string_pretty;
144
145    // --------------------------------------------------------------------------------------------
146    #[test]
147    fn test_currency_loading() {
148        match lookup_by_alpha(&"GBP".to_string()) {
149            None => println!("lookup_by_alpha NO 'GBP'"),
150            Some(c) => println!("lookup_by_alpha {:#?}", to_string_pretty(c)),
151        }
152    }
153
154    // --------------------------------------------------------------------------------------------
155    #[test]
156    fn test_currency_codes() {
157        let codes = currency_alpha_codes();
158        assert!(codes.len() > 0);
159        let numerics = currency_numeric_codes();
160        assert!(numerics.len() > 0);
161    }
162
163    #[test]
164    fn test_good_currency_code() {
165        match lookup_by_alpha("GBP") {
166            None => panic!("was expecting a currency"),
167            Some(currency) => assert_eq!(currency.name.to_string(), "Pound Sterling".to_string()),
168        }
169    }
170
171    #[test]
172    fn test_bad_currency_code() {
173        match lookup_by_alpha(&"ZZZ") {
174            None => (),
175            Some(_) => panic!("was expecting a None in response"),
176        }
177    }
178
179    #[test]
180    fn test_for_country() {
181        let currencies = currencies_for_country_name("Mexico");
182        assert_eq!(currencies.len(), 2);
183    }
184}