1use std::{fmt, ops::Neg, str::FromStr};
5
6use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
7
8use crate::{Date, Object, ParseError};
9
10#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
16pub struct Currency(String);
17
18impl Currency {
19 pub fn as_str(&self) -> &str {
21 &self.0
22 }
23
24 pub fn is_iso(&self) -> bool {
26 !self.0.starts_with("x-")
27 }
28}
29
30impl FromStr for Currency {
31 type Err = ParseError;
32
33 fn from_str(s: &str) -> Result<Self, Self::Err> {
34 let iso = s.len() == 3 && s.bytes().all(|b| b.is_ascii_uppercase());
35 let custom = s.strip_prefix("x-").is_some_and(|name| {
36 (1..=62).contains(&name.len())
37 && name.bytes().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
38 });
39 if iso || custom { Ok(Currency(s.to_owned())) } else { Err(ParseError::Currency(s.to_owned())) }
40 }
41}
42
43impl fmt::Display for Currency {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 f.write_str(&self.0)
46 }
47}
48
49impl Serialize for Currency {
50 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
51 serializer.serialize_str(&self.0)
52 }
53}
54
55impl<'de> Deserialize<'de> for Currency {
56 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
57 String::deserialize(deserializer)?.parse().map_err(de::Error::custom)
58 }
59}
60
61#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
68pub struct Money {
69 currency: Currency,
70 #[serde(with = "base_units")]
71 base_units: i128,
72}
73
74impl Money {
75 pub const MAX_BASE_UNITS: i128 = 10i128.pow(38) - 1;
77
78 pub fn new(base_units: i128, currency: Currency) -> Option<Self> {
80 (base_units.unsigned_abs() <= Self::MAX_BASE_UNITS.unsigned_abs()).then_some(Money { currency, base_units })
81 }
82
83 pub fn currency(&self) -> &Currency {
85 &self.currency
86 }
87
88 pub fn base_units(&self) -> i128 {
90 self.base_units
91 }
92}
93
94impl Object for Money {
95 const TYPE: &'static str = "money";
96}
97
98impl Neg for Money {
99 type Output = Money;
100
101 fn neg(self) -> Money {
103 Money { currency: self.currency, base_units: -self.base_units }
104 }
105}
106
107impl fmt::Display for Money {
108 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109 write!(f, "{} base units of {}", self.base_units, self.currency)
110 }
111}
112
113mod base_units {
115 use serde::{Deserialize, Deserializer, Serializer, de};
116
117 use super::Money;
118
119 pub fn serialize<S: Serializer>(base_units: &i128, serializer: S) -> Result<S::Ok, S::Error> {
120 serializer.collect_str(base_units)
121 }
122
123 pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<i128, D::Error> {
124 let s = String::deserialize(deserializer)?;
125 parse(&s).ok_or_else(|| de::Error::custom(format!("not an amount in base units: {s:?}")))
126 }
127
128 pub(super) fn parse(s: &str) -> Option<i128> {
130 let digits = s.strip_prefix('-').unwrap_or(s);
131 let canonical = !digits.is_empty()
132 && digits.bytes().all(|b| b.is_ascii_digit())
133 && (digits == "0" || !digits.starts_with('0'))
134 && s != "-0";
135 let base_units: i128 = if canonical { s.parse().ok()? } else { return None };
136 (base_units.unsigned_abs() <= Money::MAX_BASE_UNITS.unsigned_abs()).then_some(base_units)
137 }
138}
139
140#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
148#[serde(try_from = "RawDefinition", into = "RawDefinition")]
149pub struct CurrencyDefinition {
150 code: Currency,
151 subdivisions: Vec<u64>,
152}
153
154impl Object for CurrencyDefinition {
155 const TYPE: &'static str = "currency";
156}
157
158#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
160pub enum DefinitionError {
161 #[error("every subdivision is between 2 and 2^53 - 1")]
163 Stage,
164 #[error("more than 10^38 base units per unit")]
166 TooLarge,
167 #[error("an ISO 4217 currency is divided once by a power of ten, or not at all")]
169 Iso,
170}
171
172impl CurrencyDefinition {
173 pub fn new(code: Currency, subdivisions: Vec<u64>) -> Result<Self, DefinitionError> {
176 let definition = CurrencyDefinition { code, subdivisions };
177 if definition.subdivisions.iter().any(|&s| !(2..=crate::MAX_SAFE_INTEGER).contains(&s)) {
178 return Err(DefinitionError::Stage);
179 }
180 definition.base_units_per_unit().ok_or(DefinitionError::TooLarge)?;
181 if definition.code.is_iso() && (definition.subdivisions.len() > 1 || definition.decimals().is_none()) {
182 return Err(DefinitionError::Iso);
183 }
184 Ok(definition)
185 }
186
187 pub fn code(&self) -> &Currency {
189 &self.code
190 }
191
192 pub fn subdivisions(&self) -> &[u64] {
194 &self.subdivisions
195 }
196
197 pub fn base_units_per_unit(&self) -> Option<u128> {
199 let limit = Money::MAX_BASE_UNITS.unsigned_abs() + 1;
200 self.subdivisions
201 .iter()
202 .try_fold(1u128, |product, &s| product.checked_mul(u128::from(s)).filter(|&p| p <= limit))
203 }
204
205 pub fn decimals(&self) -> Option<u32> {
208 let mut per_unit = self.base_units_per_unit()?;
209 let mut decimals = 0;
210 while per_unit % 10 == 0 {
211 per_unit /= 10;
212 decimals += 1;
213 }
214 (per_unit == 1).then_some(decimals)
215 }
216}
217
218#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
220pub struct CurrencyTable {
221 pub version: String,
223 pub iso_4217: Date,
225 pub currencies: Vec<CurrencyDefinition>,
227}
228
229impl Object for CurrencyTable {
230 const TYPE: &'static str = "currency_table";
231}
232
233impl CurrencyTable {
234 pub fn v0_1() -> Self {
237 crate::from_root(include_bytes!("../../../spec/data/currencies.json")).expect("the published table is valid")
238 }
239
240 pub fn get(&self, code: &Currency) -> Option<&CurrencyDefinition> {
242 self.currencies.iter().find(|definition| definition.code() == code)
243 }
244
245 pub fn is_valid(&self) -> bool {
247 let mut codes: Vec<&Currency> = self.currencies.iter().map(CurrencyDefinition::code).collect();
248 codes.sort();
249 codes.windows(2).all(|pair| pair[0] != pair[1])
250 }
251}
252
253#[derive(Serialize, Deserialize)]
255#[serde(deny_unknown_fields)]
256struct RawDefinition {
257 code: Currency,
258 subdivisions: Vec<u64>,
259}
260
261impl TryFrom<RawDefinition> for CurrencyDefinition {
262 type Error = DefinitionError;
263
264 fn try_from(raw: RawDefinition) -> Result<Self, Self::Error> {
265 CurrencyDefinition::new(raw.code, raw.subdivisions)
266 }
267}
268
269impl From<CurrencyDefinition> for RawDefinition {
270 fn from(definition: CurrencyDefinition) -> Self {
271 RawDefinition { code: definition.code, subdivisions: definition.subdivisions }
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278
279 fn eur() -> Currency {
280 "EUR".parse().unwrap()
281 }
282
283 #[test]
284 fn currency_codes_are_iso_or_begin_with_x() {
285 for s in ["EUR", "XAU", "x-btc", "x-galleon", "x-bonus-points-2026"] {
286 assert_eq!(s.parse::<Currency>().unwrap().to_string(), s);
287 }
288 assert!(eur().is_iso());
289 assert!(!"x-btc".parse::<Currency>().unwrap().is_iso());
290 for s in ["eur", "EU", "EURO", "E1R", "", "x-", "X-btc", "x-BTC", "x-b c", "btc"] {
291 assert!(s.parse::<Currency>().is_err(), "{s:?}");
292 }
293 }
294
295 #[test]
296 fn money_is_base_units_as_a_string_in_json() {
297 let money = Money::new(-1240, eur()).unwrap();
298 let json = serde_json::to_string(&money).unwrap();
299 assert_eq!(json, r#"{"currency":"EUR","base_units":"-1240"}"#);
300 assert_eq!(serde_json::from_str::<Money>(&json).unwrap(), money);
301 }
302
303 #[test]
304 fn base_units_have_one_written_form() {
305 for s in ["0", "7", "-1240", "99999999999999999999999999999999999999"] {
306 assert_eq!(base_units::parse(s).unwrap().to_string(), s);
307 }
308 for s in
309 ["", "-", "-0", "+1", "01", "-01", "1.0", "12.40", "1e3", " 1", "100000000000000000000000000000000000000"]
310 {
311 assert!(base_units::parse(s).is_none(), "{s:?}");
312 }
313 assert!(serde_json::from_str::<Money>(r#"{"currency":"EUR","base_units":1240}"#).is_err());
314 }
315
316 #[test]
317 fn the_range_is_symmetric() {
318 assert!(Money::new(Money::MAX_BASE_UNITS + 1, eur()).is_none());
319 assert_eq!((-Money::new(-Money::MAX_BASE_UNITS, eur()).unwrap()).base_units(), Money::MAX_BASE_UNITS);
320 }
321
322 fn definition(json: &str) -> Result<CurrencyDefinition, serde_json::Error> {
323 serde_json::from_str(json)
324 }
325
326 #[test]
327 fn decimal_currencies() {
328 let json = r#"{"code":"EUR","subdivisions":[100]}"#;
329 let eur = definition(json).unwrap();
330 assert_eq!((eur.base_units_per_unit(), eur.decimals()), (Some(100), Some(2)));
331 assert_eq!(serde_json::to_string(&eur).unwrap(), json);
332 let jpy = definition(r#"{"code":"JPY","subdivisions":[]}"#).unwrap();
333 assert_eq!((jpy.base_units_per_unit(), jpy.decimals()), (Some(1), Some(0)));
334 let eth = definition(r#"{"code":"x-eth","subdivisions":[1000000000,1000000000]}"#).unwrap();
335 assert_eq!(eth.decimals(), Some(18));
336 }
337
338 #[test]
339 fn other_currencies() {
340 let galleon = definition(r#"{"code":"x-galleon","subdivisions":[17,29]}"#).unwrap();
341 assert_eq!((galleon.base_units_per_unit(), galleon.decimals()), (Some(493), None));
342 assert_eq!(galleon.subdivisions(), [17, 29]);
343 }
344
345 #[test]
346 fn the_table_of_version_0_1() {
347 let table = CurrencyTable::v0_1();
348 assert!(table.is_valid());
349 assert_eq!(table.currencies.len(), 167);
350 let decimals = |code: &str| table.get(&code.parse().unwrap()).and_then(CurrencyDefinition::decimals);
351 assert_eq!(
352 [decimals("EUR"), decimals("JPY"), decimals("KWD"), decimals("CLF"), decimals("MGA")],
353 [Some(2), Some(0), Some(3), Some(4), Some(2)]
354 );
355 assert_eq!((decimals("x-btc"), decimals("x-eth")), (Some(8), Some(18)));
356 assert!(table.get(&"XAU".parse().unwrap()).is_none(), "units without a minor unit are not in the table");
357 }
358
359 #[test]
360 fn invalid_definitions() {
361 let x = || "x-test".parse::<Currency>().unwrap();
362 for stages in [vec![1], vec![17, 0], vec![20, 1, 12], vec![1 << 53]] {
363 assert_eq!(CurrencyDefinition::new(x(), stages), Err(DefinitionError::Stage));
364 }
365 let too_large = vec![1_000_000_000_000_000; 3];
366 assert_eq!(CurrencyDefinition::new(x(), too_large), Err(DefinitionError::TooLarge));
367 for stages in [vec![20, 12], vec![10, 10], vec![5]] {
368 assert_eq!(CurrencyDefinition::new(eur(), stages), Err(DefinitionError::Iso));
369 }
370 for json in [
371 r#"{"code":"EUR"}"#,
372 r#"{"code":"EUR","decimals":2}"#,
373 r#"{"code":"EUR","subdivisions":[100],"number":"978"}"#,
374 r#"{"code":"EUR","subdivisions":[100.0]}"#,
375 ] {
376 assert!(definition(json).is_err(), "{json}");
377 }
378 }
379}