1use std::fmt;
2
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4use sofie_core::{Cast, ColumnReader, ColumnSpec, Error, Result, Show, Shown, SqlValue};
5
6use sofie_core::deserialize_parsed;
7
8#[rustfmt::skip]
12const CODES: [([u8; 3], u8); 165] = [
13 (*b"AED", 2), (*b"AFN", 2), (*b"ALL", 2), (*b"AMD", 2), (*b"AOA", 2), (*b"ARS", 2),
14 (*b"AUD", 2), (*b"AWG", 2), (*b"AZN", 2), (*b"BAM", 2), (*b"BBD", 2), (*b"BDT", 2),
15 (*b"BHD", 3), (*b"BIF", 0), (*b"BMD", 2), (*b"BND", 2), (*b"BOB", 2), (*b"BOV", 2),
16 (*b"BRL", 2), (*b"BSD", 2), (*b"BTN", 2), (*b"BWP", 2), (*b"BYN", 2), (*b"BZD", 2),
17 (*b"CAD", 2), (*b"CDF", 2), (*b"CHE", 2), (*b"CHF", 2), (*b"CHW", 2), (*b"CLF", 4),
18 (*b"CLP", 0), (*b"CNY", 2), (*b"COP", 2), (*b"COU", 2), (*b"CRC", 2), (*b"CUP", 2),
19 (*b"CVE", 2), (*b"CZK", 2), (*b"DJF", 0), (*b"DKK", 2), (*b"DOP", 2), (*b"DZD", 2),
20 (*b"EGP", 2), (*b"ERN", 2), (*b"ETB", 2), (*b"EUR", 2), (*b"FJD", 2), (*b"FKP", 2),
21 (*b"GBP", 2), (*b"GEL", 2), (*b"GHS", 2), (*b"GIP", 2), (*b"GMD", 2), (*b"GNF", 0),
22 (*b"GTQ", 2), (*b"GYD", 2), (*b"HKD", 2), (*b"HNL", 2), (*b"HTG", 2), (*b"HUF", 2),
23 (*b"IDR", 2), (*b"ILS", 2), (*b"INR", 2), (*b"IQD", 3), (*b"IRR", 2), (*b"ISK", 0),
24 (*b"JMD", 2), (*b"JOD", 3), (*b"JPY", 0), (*b"KES", 2), (*b"KGS", 2), (*b"KHR", 2),
25 (*b"KMF", 0), (*b"KPW", 2), (*b"KRW", 0), (*b"KWD", 3), (*b"KYD", 2), (*b"KZT", 2),
26 (*b"LAK", 2), (*b"LBP", 2), (*b"LKR", 2), (*b"LRD", 2), (*b"LSL", 2), (*b"LYD", 3),
27 (*b"MAD", 2), (*b"MDL", 2), (*b"MGA", 2), (*b"MKD", 2), (*b"MMK", 2), (*b"MNT", 2),
28 (*b"MOP", 2), (*b"MRU", 2), (*b"MUR", 2), (*b"MVR", 2), (*b"MWK", 2), (*b"MXN", 2),
29 (*b"MXV", 2), (*b"MYR", 2), (*b"MZN", 2), (*b"NAD", 2), (*b"NGN", 2), (*b"NIO", 2),
30 (*b"NOK", 2), (*b"NPR", 2), (*b"NZD", 2), (*b"OMR", 3), (*b"PAB", 2), (*b"PEN", 2),
31 (*b"PGK", 2), (*b"PHP", 2), (*b"PKR", 2), (*b"PLN", 2), (*b"PYG", 0), (*b"QAR", 2),
32 (*b"RON", 2), (*b"RSD", 2), (*b"RUB", 2), (*b"RWF", 0), (*b"SAR", 2), (*b"SBD", 2),
33 (*b"SCR", 2), (*b"SDG", 2), (*b"SEK", 2), (*b"SGD", 2), (*b"SHP", 2), (*b"SLE", 2),
34 (*b"SOS", 2), (*b"SRD", 2), (*b"SSP", 2), (*b"STN", 2), (*b"SVC", 2), (*b"SYP", 2),
35 (*b"SZL", 2), (*b"THB", 2), (*b"TJS", 2), (*b"TMT", 2), (*b"TND", 3), (*b"TOP", 2),
36 (*b"TRY", 2), (*b"TTD", 2), (*b"TWD", 2), (*b"TZS", 2), (*b"UAH", 2), (*b"UGX", 0),
37 (*b"USD", 2), (*b"USN", 2), (*b"UYI", 0), (*b"UYU", 2), (*b"UYW", 4), (*b"UZS", 2),
38 (*b"VED", 2), (*b"VES", 2), (*b"VND", 0), (*b"VUV", 0), (*b"WST", 2), (*b"XAD", 2),
39 (*b"XAF", 0), (*b"XCD", 2), (*b"XCG", 2), (*b"XOF", 0), (*b"XPF", 0), (*b"YER", 2),
40 (*b"ZAR", 2), (*b"ZMW", 2), (*b"ZWG", 2),
41];
42
43#[derive(Clone, Copy, PartialEq, Eq, Hash)]
44pub struct Currency {
45 code: [u8; 3],
46 minor_digits: u8,
47}
48
49impl Currency {
50 pub fn parse(input: &str) -> Result<Self> {
51 let index = CODES
52 .binary_search_by(|(code, _)| code.as_slice().cmp(input.as_bytes()))
53 .map_err(|_| invalid_currency())?;
54 let (code, minor_digits) = CODES[index];
55 Ok(Self { code, minor_digits })
56 }
57
58 #[must_use]
59 pub fn as_str(&self) -> &str {
60 std::str::from_utf8(&self.code).expect("a currency code is ASCII letters")
61 }
62
63 #[must_use]
64 pub fn minor_digits(&self) -> u8 {
65 self.minor_digits
66 }
67
68 pub fn all() -> impl Iterator<Item = Self> {
70 CODES
71 .iter()
72 .map(|&(code, minor_digits)| Self { code, minor_digits })
73 }
74}
75
76fn invalid_currency() -> Error {
77 Error::invalid(
78 "currency",
79 "must be an ISO 4217 code in use, in uppercase, such as EUR",
80 )
81}
82
83impl fmt::Display for Currency {
84 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
85 formatter.write_str(self.as_str())
86 }
87}
88
89impl fmt::Debug for Currency {
90 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
91 write!(formatter, "Currency({})", self.as_str())
92 }
93}
94
95impl Show for Currency {
96 fn show(&self) -> Shown<'_> {
97 Shown::Text(self.as_str())
98 }
99}
100
101impl Cast for Currency {
102 const COLUMNS: &'static [ColumnSpec] = String::COLUMNS;
103
104 fn encode(&self, out: &mut Vec<SqlValue>) {
105 out.push(SqlValue::Text(self.as_str().to_owned()));
106 }
107
108 fn decode(row: &mut dyn ColumnReader) -> Result<Self> {
109 Self::parse(row.text()?)
110 .map_err(|error| Error::internal("stored currency is not an ISO 4217 code", error))
111 }
112}
113
114impl Serialize for Currency {
115 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
116 serializer.serialize_str(self.as_str())
117 }
118}
119
120impl<'de> Deserialize<'de> for Currency {
121 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
122 deserialize_parsed(deserializer, "an ISO 4217 currency code", Self::parse)
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use serde::de::{self, Visitor};
129 use sofie_core::RowReader;
130
131 use super::*;
132 use crate::testing::{assert_refused, round_trip};
133
134 #[test]
135 fn accepts_codes_in_use() {
136 for code in ["EUR", "USD", "TRY", "JPY"] {
137 assert_eq!(Currency::parse(code).unwrap().as_str(), code);
138 }
139 }
140
141 #[test]
142 fn rejects_anything_else() {
143 assert_refused(
144 Currency::parse,
145 &[
146 "", "EU", "EURO", "eur", "Eur", "E1R", " EUR", "ÉUR", "€", "QQQ", "XXX", "XAU",
147 "DEM", "BGN",
148 ],
149 );
150 }
151
152 #[test]
153 fn knows_the_digits_of_its_minor_unit() {
154 for (code, digits) in [
155 ("EUR", 2),
156 ("USD", 2),
157 ("JPY", 0),
158 ("XPF", 0),
159 ("KWD", 3),
160 ("CLF", 4),
161 ("UYW", 4),
162 ("UYI", 0),
163 ] {
164 assert_eq!(
165 Currency::parse(code).unwrap().minor_digits(),
166 digits,
167 "{code}"
168 );
169 }
170 }
171
172 #[test]
173 fn displays_as_its_code() {
174 assert_eq!(Currency::parse("GBP").unwrap().to_string(), "GBP");
175 }
176
177 #[test]
178 fn round_trips_through_one_text_column() {
179 let currency = Currency::parse("EUR").unwrap();
180 assert_eq!(round_trip(¤cy), vec![SqlValue::Text("EUR".into())]);
181 }
182
183 #[test]
184 fn an_invalid_stored_code_is_an_internal_error() {
185 let values = [SqlValue::Text("euro".into())];
186 let mut row = RowReader::new(&values);
187
188 assert!(matches!(
189 Currency::decode(&mut row),
190 Err(Error::Internal { .. })
191 ));
192 }
193
194 #[test]
195 fn round_trips_through_json_as_a_string() {
196 let currency = Currency::parse("USD").unwrap();
197 let json = serde_json::to_string(¤cy).unwrap();
198
199 assert_eq!(json, "\"USD\"");
200 assert_eq!(serde_json::from_str::<Currency>(&json).unwrap(), currency);
201 }
202
203 #[test]
204 fn json_deserialization_validates_the_code() {
205 assert!(serde_json::from_str::<Currency>("\"usd\"").is_err());
206 }
207
208 struct BorrowedOnly<'de>(&'de str);
210
211 impl<'de> Deserializer<'de> for BorrowedOnly<'de> {
212 type Error = serde::de::value::Error;
213
214 fn deserialize_any<V: Visitor<'de>>(self, _: V) -> Result<V::Value, Self::Error> {
215 Err(de::Error::custom("only a borrowed str is offered"))
216 }
217
218 fn deserialize_str<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
219 visitor.visit_borrowed_str(self.0)
220 }
221
222 serde::forward_to_deserialize_any! {
223 bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char string bytes byte_buf
224 option unit unit_struct newtype_struct seq tuple tuple_struct map struct enum
225 identifier ignored_any
226 }
227 }
228
229 #[test]
230 fn deserializes_from_a_borrowed_str() {
231 let currency = Currency::deserialize(BorrowedOnly("JPY")).unwrap();
232
233 assert_eq!(currency, Currency::parse("JPY").unwrap());
234 assert!(Currency::deserialize(BorrowedOnly("jpy")).is_err());
235 }
236
237 #[test]
238 fn every_listed_code_reads_back_as_written() {
239 for (code, digits) in CODES {
240 let code = std::str::from_utf8(&code).unwrap();
241 let currency = Currency::parse(code).unwrap();
242
243 assert_eq!(currency.as_str(), code);
244 assert_eq!(currency.minor_digits(), digits, "{code}");
245 }
246 }
247
248 #[test]
249 fn the_listed_codes_are_sorted_uppercase_letters() {
250 assert!(CODES.is_sorted_by(|(first, _), (second, _)| first < second));
251 assert!(
252 CODES
253 .iter()
254 .all(|(code, _)| code.iter().all(u8::is_ascii_uppercase))
255 );
256 }
257}