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