lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

currency.rs (4142B)


      1 //! Canonical three-letter currency codes and currency-specific minor units.
      2 
      3 use core::fmt;
      4 use core::str::FromStr;
      5 
      6 #[cfg(all(feature = "serde", not(feature = "std")))]
      7 use alloc::string::String;
      8 #[cfg(feature = "serde")]
      9 #[cfg(feature = "std")]
     10 use std::string::String;
     11 
     12 #[cfg(feature = "serde")]
     13 use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as DeError};
     14 
     15 #[cfg_attr(all(test, feature = "std"), derive(dto_bindgen::Dto))]
     16 #[cfg_attr(all(test, feature = "std"), dto(as = "string"))]
     17 #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
     18 pub struct Currency([u8; 3]);
     19 
     20 impl Currency {
     21     /// Builds a currency from its canonical three-byte ASCII code.
     22     #[inline]
     23     pub const fn from_const(bytes: [u8; 3]) -> Result<Self, ParseError> {
     24         if Self::is_ascii_upper(bytes[0])
     25             && Self::is_ascii_upper(bytes[1])
     26             && Self::is_ascii_upper(bytes[2])
     27         {
     28             Ok(Self(bytes))
     29         } else {
     30             Err(ParseError::InvalidFormat)
     31         }
     32     }
     33 
     34     const fn is_ascii_upper(byte: u8) -> bool {
     35         byte >= b'A' && byte <= b'Z'
     36     }
     37 
     38     #[inline]
     39     pub fn from_str_upper(s: &str) -> Result<Self, ParseError> {
     40         let b = s.as_bytes();
     41         if b.len() != 3 || b.iter().any(|c| !c.is_ascii_uppercase()) {
     42             return Err(ParseError::InvalidFormat);
     43         }
     44         Ok(Self([b[0], b[1], b[2]]))
     45     }
     46 
     47     #[inline]
     48     pub fn as_str(&self) -> &str {
     49         core::str::from_utf8(&self.0).unwrap_or("???")
     50     }
     51 
     52     #[inline]
     53     pub const fn as_bytes(&self) -> &[u8; 3] {
     54         &self.0
     55     }
     56 
     57     pub const USD: Currency = Currency(*b"USD");
     58     pub const EUR: Currency = Currency(*b"EUR");
     59     pub const GBP: Currency = Currency(*b"GBP");
     60     pub const JPY: Currency = Currency(*b"JPY");
     61     pub const CAD: Currency = Currency(*b"CAD");
     62     pub const AUD: Currency = Currency(*b"AUD");
     63 
     64     #[inline]
     65     pub const fn minor_unit_exponent(&self) -> u32 {
     66         match self.0 {
     67             [b'J', b'P', b'Y'] | [b'K', b'R', b'W'] | [b'V', b'N', b'D'] => 0,
     68             [b'B', b'H', b'D']
     69             | [b'I', b'Q', b'D']
     70             | [b'J', b'O', b'D']
     71             | [b'K', b'W', b'D']
     72             | [b'L', b'Y', b'D']
     73             | [b'O', b'M', b'R']
     74             | [b'T', b'N', b'D'] => 3,
     75             _ => 2,
     76         }
     77     }
     78 }
     79 
     80 impl fmt::Debug for Currency {
     81     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
     82         f.debug_tuple("Currency").field(&self.as_str()).finish()
     83     }
     84 }
     85 
     86 impl fmt::Display for Currency {
     87     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
     88         f.write_str(self.as_str())
     89     }
     90 }
     91 
     92 impl TryFrom<&str> for Currency {
     93     type Error = ParseError;
     94     fn try_from(s: &str) -> Result<Self, Self::Error> {
     95         s.parse()
     96     }
     97 }
     98 
     99 impl FromStr for Currency {
    100     type Err = ParseError;
    101 
    102     fn from_str(s: &str) -> Result<Self, Self::Err> {
    103         // Currency input is canonicalized to uppercase ASCII. Serialization
    104         // and Display always emit that canonical representation.
    105         let s = s.trim();
    106         if s.len() != 3 || !s.chars().all(|c| c.is_ascii_alphabetic()) {
    107             return Err(ParseError::InvalidFormat);
    108         }
    109         let upper = s.to_ascii_uppercase();
    110         Self::from_str_upper(&upper)
    111     }
    112 }
    113 
    114 #[non_exhaustive]
    115 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    116 pub enum ParseError {
    117     InvalidFormat,
    118 }
    119 
    120 impl fmt::Display for ParseError {
    121     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    122         match self {
    123             ParseError::InvalidFormat => {
    124                 write!(f, "currency must be a 3-letter code")
    125             }
    126         }
    127     }
    128 }
    129 
    130 #[cfg(feature = "std")]
    131 impl std::error::Error for ParseError {}
    132 
    133 #[cfg(feature = "serde")]
    134 impl Serialize for Currency {
    135     fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
    136         ser.serialize_str(self.as_str())
    137     }
    138 }
    139 
    140 #[cfg(feature = "serde")]
    141 impl<'de> Deserialize<'de> for Currency {
    142     fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
    143         let s = String::deserialize(de)?;
    144         s.parse().map_err(D::Error::custom)
    145     }
    146 }