Skip to main content

ocpi_tariffs/
currency.rs

1//! An ISO 4217 currency code.
2#[cfg(test)]
3pub(crate) mod test;
4
5use std::fmt;
6
7use num_derive::{FromPrimitive, ToPrimitive};
8use num_traits::{FromPrimitive as _, ToPrimitive as _};
9
10use crate::{
11    from_warning_all, into_caveat, json,
12    warning::{self, GatherWarnings as _},
13    IntoCaveat, Verdict,
14};
15
16/// The warnings that can happen when parsing or linting a currency code.
17#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
18pub enum Warning {
19    /// The currency field does not require char escape codes.
20    ContainsEscapeCodes,
21
22    /// The field at the path could not be decoded.
23    Decode(json::decode::Warning),
24
25    /// The `country` is not a valid ISO 3166-1 country code because it's not uppercase.
26    PreferUpperCase,
27
28    /// The `currency` is not a valid ISO 4217 currency code.
29    InvalidCode,
30
31    /// The JSON value given is not a string.
32    InvalidType { type_found: json::ValueKind },
33
34    /// The `currency` is not a valid ISO 4217 currency code: it should be 3 chars.
35    InvalidLength,
36
37    /// The `currency` is not a valid ISO 4217 currency code because it's a test code.
38    InvalidCodeXTS,
39
40    /// The `currency` is not a valid ISO 4217 currency code because it's a code for `no-currency`.
41    InvalidCodeXXX,
42}
43
44impl Warning {
45    fn invalid_type(elem: &json::Element<'_>) -> Self {
46        Self::InvalidType {
47            type_found: elem.value().kind(),
48        }
49    }
50}
51
52impl fmt::Display for Warning {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        match self {
55            Self::ContainsEscapeCodes => write!(
56                f,
57                "The currency-code contains escape-codes but it does not need them.",
58            ),
59            Self::Decode(warning) => fmt::Display::fmt(warning, f),
60            Self::PreferUpperCase => write!(
61                f,
62                "The currency-code follows the ISO 4217 standard whch states: the chars should be uppercase.",
63            ),
64            Self::InvalidCode => {
65                write!(f, "The currency-code is not a valid ISO 4217 code.")
66            }
67            Self::InvalidType { .. } => write!(f, "The currency-code should be a string."),
68            Self::InvalidLength => write!(f, "The currency-code follows the ISO 4217 standard whch states: the code should be three chars."),
69            Self::InvalidCodeXTS => write!(
70                f,
71                "The currency-code is `XTS`. This is a code for testing only",
72            ),
73            Self::InvalidCodeXXX => write!(
74                f,
75                "The currency-code is `XXX`. This means there is no currency",
76            ),
77        }
78    }
79}
80
81impl crate::Warning for Warning {
82    fn id(&self) -> warning::Id {
83        match self {
84            Self::ContainsEscapeCodes => warning::Id::from_static("contains_escape_codes"),
85            Self::Decode(kind) => kind.id(),
86            Self::PreferUpperCase => warning::Id::from_static("prefer_upper_case"),
87            Self::InvalidCode => warning::Id::from_static("invalid_code"),
88            Self::InvalidType { .. } => warning::Id::from_static("invalid_type"),
89            Self::InvalidLength => warning::Id::from_static("invalid_length"),
90            Self::InvalidCodeXTS => warning::Id::from_static("invalid_code_xts"),
91            Self::InvalidCodeXXX => warning::Id::from_static("invalid_code_xxx"),
92        }
93    }
94}
95
96from_warning_all!(json::decode::Warning => Warning::Decode);
97
98impl json::FromJson<'_, '_> for Code {
99    type Warning = Warning;
100
101    #[expect(
102        clippy::unwrap_used,
103        reason = "The CURRENCIES_ALPHA3_ARRAY is in sync with the Code enum."
104    )]
105    fn from_json(elem: &'_ json::Element<'_>) -> Verdict<Self, Self::Warning> {
106        let mut warnings = warning::Set::new();
107        let value = elem.as_value();
108
109        let Some(s) = value.as_raw_str() else {
110            return warnings.bail(Warning::invalid_type(elem), elem);
111        };
112
113        let pending_str = s.has_escapes(elem).gather_warnings_into(&mut warnings);
114
115        let s = match pending_str {
116            json::decode::PendingStr::NoEscapes(s) => s,
117            json::decode::PendingStr::HasEscapes(_) => {
118                return warnings.bail(Warning::ContainsEscapeCodes, elem);
119            }
120        };
121
122        let bytes = s.as_bytes();
123
124        // ISO 4217 is expected to be 3 chars enclosed in quotes.
125        let [a, b, c] = bytes else {
126            return warnings.bail(Warning::InvalidLength, elem);
127        };
128
129        let triplet: [u8; 3] = [
130            a.to_ascii_uppercase(),
131            b.to_ascii_uppercase(),
132            c.to_ascii_uppercase(),
133        ];
134
135        if triplet != bytes {
136            warnings.insert(Warning::PreferUpperCase, elem);
137        }
138
139        let Some(index) = CURRENCIES_ALPHA3_ARRAY
140            .iter()
141            .position(|code| code.as_bytes() == triplet)
142        else {
143            return warnings.bail(Warning::InvalidCode, elem);
144        };
145
146        let code = Code::from_usize(index).unwrap();
147
148        if matches!(code, Code::Xts) {
149            warnings.insert(Warning::InvalidCodeXTS, elem);
150        } else if matches!(code, Code::Xxx) {
151            warnings.insert(Warning::InvalidCodeXXX, elem);
152        }
153
154        Ok(code.into_caveat(warnings))
155    }
156}
157
158impl Code {
159    /// Return a str version of the [Code]
160    ///
161    /// # Panics
162    ///
163    /// Panics if the Code enum is out of sync with the `CURRENCIES_ALPHA3_ARRAY` array
164    #[expect(
165        clippy::indexing_slicing,
166        reason = "The CURRENCIES_ALPHA3_ARRAY is not in sync with the Code enum"
167    )]
168    pub fn into_str(self) -> &'static str {
169        let index = self
170            .to_usize()
171            .expect("The CURRENCIES_ALPHA3_ARRAY is in sync with the Code enum");
172        CURRENCIES_ALPHA3_ARRAY[index]
173    }
174
175    /// Return a str symbol of the [Code]
176    ///
177    /// # Panics
178    ///
179    /// Panics if the Code enum is out of sync with the `CURRENCIES_SYMBOLS_ARRAY` array
180    #[expect(
181        clippy::indexing_slicing,
182        reason = "The CURRENCIES_ALPHA3_ARRAY is not in sync with the Code enum"
183    )]
184    pub fn into_symbol(self) -> &'static str {
185        let index = self
186            .to_usize()
187            .expect("The CURRENCIES_SYMBOLS_ARRAY is in sync with the Code enum");
188        CURRENCIES_SYMBOLS_ARRAY[index]
189    }
190}
191
192into_caveat!(Code);
193
194impl fmt::Display for Code {
195    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196        f.write_str(self.into_str())
197    }
198}
199
200/// An ISO 4217 currency code.
201#[derive(Clone, Copy, Debug, Eq, FromPrimitive, Ord, PartialEq, PartialOrd, ToPrimitive)]
202pub enum Code {
203    Aed,
204    Afn,
205    All,
206    Amd,
207    Ang,
208    Aoa,
209    Ars,
210    Aud,
211    Awg,
212    Azn,
213    Bam,
214    Bbd,
215    Bdt,
216    Bgn,
217    Bhd,
218    Bif,
219    Bmd,
220    Bnd,
221    Bob,
222    Bov,
223    Brl,
224    Bsd,
225    Btn,
226    Bwp,
227    Byn,
228    Bzd,
229    Cad,
230    Cdf,
231    Che,
232    Chf,
233    Chw,
234    Clf,
235    Clp,
236    Cny,
237    Cop,
238    Cou,
239    Crc,
240    Cuc,
241    Cup,
242    Cve,
243    Czk,
244    Djf,
245    Dkk,
246    Dop,
247    Dzd,
248    Egp,
249    Ern,
250    Etb,
251    Eur,
252    Fjd,
253    Fkp,
254    Gbp,
255    Gel,
256    Ghs,
257    Gip,
258    Gmd,
259    Gnf,
260    Gtq,
261    Gyd,
262    Hkd,
263    Hnl,
264    Hrk,
265    Htg,
266    Huf,
267    Idr,
268    Ils,
269    Inr,
270    Iqd,
271    Irr,
272    Isk,
273    Jmd,
274    Jod,
275    Jpy,
276    Kes,
277    Kgs,
278    Khr,
279    Kmf,
280    Kpw,
281    Krw,
282    Kwd,
283    Kyd,
284    Kzt,
285    Lak,
286    Lbp,
287    Lkr,
288    Lrd,
289    Lsl,
290    Lyd,
291    Mad,
292    Mdl,
293    Mga,
294    Mkd,
295    Mmk,
296    Mnt,
297    Mop,
298    Mru,
299    Mur,
300    Mvr,
301    Mwk,
302    Mxn,
303    Mxv,
304    Myr,
305    Mzn,
306    Nad,
307    Ngn,
308    Nio,
309    Nok,
310    Npr,
311    Nzd,
312    Omr,
313    Pab,
314    Pen,
315    Pgk,
316    Php,
317    Pkr,
318    Pln,
319    Pyg,
320    Qar,
321    Ron,
322    Rsd,
323    Rub,
324    Rwf,
325    Sar,
326    Sbd,
327    Scr,
328    Sdg,
329    Sek,
330    Sgd,
331    Shp,
332    Sle,
333    Sll,
334    Sos,
335    Srd,
336    Ssp,
337    Stn,
338    Svc,
339    Syp,
340    Szl,
341    Thb,
342    Tjs,
343    Tmt,
344    Tnd,
345    Top,
346    Try,
347    Ttd,
348    Twd,
349    Tzs,
350    Uah,
351    Ugx,
352    Usd,
353    Usn,
354    Uyi,
355    Uyu,
356    Uyw,
357    Uzs,
358    Ved,
359    Ves,
360    Vnd,
361    Vuv,
362    Wst,
363    Xaf,
364    Xag,
365    Xau,
366    Xba,
367    Xbb,
368    Xbc,
369    Xbd,
370    Xcd,
371    Xdr,
372    Xof,
373    Xpd,
374    Xpf,
375    Xpt,
376    Xsu,
377    Xts,
378    Xua,
379    Xxx,
380    Yer,
381    Zar,
382    Zmw,
383    Zwl,
384}
385
386/// `&str` versions of an ISO 4217 currency code.
387pub(crate) const CURRENCIES_ALPHA3_ARRAY: [&str; 181] = [
388    "AED", "AFN", "ALL", "AMD", "ANG", "AOA", "ARS", "AUD", "AWG", "AZN", "BAM", "BBD", "BDT",
389    "BGN", "BHD", "BIF", "BMD", "BND", "BOB", "BOV", "BRL", "BSD", "BTN", "BWP", "BYN", "BZD",
390    "CAD", "CDF", "CHE", "CHF", "CHW", "CLF", "CLP", "CNY", "COP", "COU", "CRC", "CUC", "CUP",
391    "CVE", "CZK", "DJF", "DKK", "DOP", "DZD", "EGP", "ERN", "ETB", "EUR", "FJD", "FKP", "GBP",
392    "GEL", "GHS", "GIP", "GMD", "GNF", "GTQ", "GYD", "HKD", "HNL", "HRK", "HTG", "HUF", "IDR",
393    "ILS", "INR", "IQD", "IRR", "ISK", "JMD", "JOD", "JPY", "KES", "KGS", "KHR", "KMF", "KPW",
394    "KRW", "KWD", "KYD", "KZT", "LAK", "LBP", "LKR", "LRD", "LSL", "LYD", "MAD", "MDL", "MGA",
395    "MKD", "MMK", "MNT", "MOP", "MRU", "MUR", "MVR", "MWK", "MXN", "MXV", "MYR", "MZN", "NAD",
396    "NGN", "NIO", "NOK", "NPR", "NZD", "OMR", "PAB", "PEN", "PGK", "PHP", "PKR", "PLN", "PYG",
397    "QAR", "RON", "RSD", "RUB", "RWF", "SAR", "SBD", "SCR", "SDG", "SEK", "SGD", "SHP", "SLE",
398    "SLL", "SOS", "SRD", "SSP", "STN", "SVC", "SYP", "SZL", "THB", "TJS", "TMT", "TND", "TOP",
399    "TRY", "TTD", "TWD", "TZS", "UAH", "UGX", "USD", "USN", "UYI", "UYU", "UYW", "UZS", "VED",
400    "VES", "VND", "VUV", "WST", "XAF", "XAG", "XAU", "XBA", "XBB", "XBC", "XBD", "XCD", "XDR",
401    "XOF", "XPD", "XPF", "XPT", "XSU", "XTS", "XUA", "XXX", "YER", "ZAR", "ZMW", "ZWL",
402];
403
404/// A best effort mapping of ISO 4217 codes to currency symbols.
405pub(crate) const CURRENCIES_SYMBOLS_ARRAY: [&str; 181] = [
406    "D", "Afs", "L", "֏", "NAƒ", "Kz", "$", "$", "Aƒ", "₼", "KM", "$", "৳", "лв", "BD", "FBu", "$",
407    "$", "Bs", "BOV", "$", "$", "Nu.", "p", "BYN", "$", "$", "FC", "CHE", "CHF", "CHW", "UF", "$",
408    "¥", "$", "COU", "₡", "$", "$", "$", "Kč", "Fdj", "kr.", "$", "DA", "£", "Nfk", "Br", "€", "$",
409    "£", "£", "₾", "GH₵", "£", "D", "FG", "Q", "$", "$", "L", "lp", "G", "Ft", "Rp", "₪", "₹",
410    "IQD", "Rl", "kr", "$", "د.أ", "¥", "KES", "⃀", "៛", "KMF", "₩", "₩", "KD", "$", "₸", "₭",
411    "LL", "₨", "$", "L", "LD", "DH", "L", "Ar", "den", "Ks.", "₮", "$", "UM", "₨", "Rf", "K", "$",
412    "MXV", "RM", "MT", "$", "₦", "C$", "kr", "रु", "$", "OMR", "฿", "S/", "K", "₱", "Re", "zł", "₲",
413    "QR", "lei", "DIN", "₽", "R₣", "\u{20C1}", "$", "R", "PT", "kr", "$", "£", "Le", "Le",
414    "Sh.So.", "$", "£", "Db", "₡", "LS", "L", "฿", "SM", "m", "DT", "$", "₺", "$", "$", "TZS", "₴",
415    "UGX", "$", "USN", "UYI", "$", "UYW", "UZS", "Bs.D", "Bs.S", "₫", "Vt", "$", "F.CFA", "XAG",
416    "XAU", "XBA", "XBB", "XBC", "XBD", "$", "SDR", "F.CFA", "XPD", "F", "XPT", "sucre", "XTS",
417    "XUA", "XXX", "﷼", "R", "K", "$",
418];