Skip to main content

use_currency/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7/// United States dollar currency code.
8pub const USD: &str = "USD";
9/// Euro currency code.
10pub const EUR: &str = "EUR";
11/// British pound sterling currency code.
12pub const GBP: &str = "GBP";
13/// Canadian dollar currency code.
14pub const CAD: &str = "CAD";
15/// Australian dollar currency code.
16pub const AUD: &str = "AUD";
17/// Japanese yen currency code.
18pub const JPY: &str = "JPY";
19
20/// Common currency code primitives.
21pub mod prelude {
22    pub use crate::{AUD, CAD, CurrencyCode, CurrencyCodeError, EUR, GBP, JPY, USD};
23}
24
25/// A validated uppercase 3-letter alphabetic currency code.
26#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
27pub struct CurrencyCode(String);
28
29impl CurrencyCode {
30    /// Creates a currency code from an uppercase 3-letter ASCII alphabetic value.
31    ///
32    /// # Errors
33    ///
34    /// Returns [`CurrencyCodeError::InvalidLength`] when the input is not exactly three bytes,
35    /// [`CurrencyCodeError::NotAlphabetic`] when a byte is not alphabetic, and
36    /// [`CurrencyCodeError::NotUppercase`] when an alphabetic byte is lowercase.
37    pub fn new(value: impl AsRef<str>) -> Result<Self, CurrencyCodeError> {
38        let value = value.as_ref();
39        if value.len() != 3 {
40            return Err(CurrencyCodeError::InvalidLength);
41        }
42
43        if !value.bytes().all(|byte| byte.is_ascii_alphabetic()) {
44            return Err(CurrencyCodeError::NotAlphabetic);
45        }
46
47        if !value.bytes().all(|byte| byte.is_ascii_uppercase()) {
48            return Err(CurrencyCodeError::NotUppercase);
49        }
50
51        Ok(Self(value.to_string()))
52    }
53
54    /// Returns the validated currency code.
55    #[must_use]
56    pub fn as_str(&self) -> &str {
57        &self.0
58    }
59
60    /// Consumes the code and returns its owned string.
61    #[must_use]
62    pub fn into_string(self) -> String {
63        self.0
64    }
65}
66
67impl AsRef<str> for CurrencyCode {
68    fn as_ref(&self) -> &str {
69        self.as_str()
70    }
71}
72
73impl fmt::Display for CurrencyCode {
74    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
75        formatter.write_str(self.as_str())
76    }
77}
78
79impl FromStr for CurrencyCode {
80    type Err = CurrencyCodeError;
81
82    fn from_str(value: &str) -> Result<Self, Self::Err> {
83        Self::new(value)
84    }
85}
86
87impl TryFrom<&str> for CurrencyCode {
88    type Error = CurrencyCodeError;
89
90    fn try_from(value: &str) -> Result<Self, Self::Error> {
91        Self::new(value)
92    }
93}
94
95/// Errors returned while constructing currency codes.
96#[derive(Clone, Copy, Debug, Eq, PartialEq)]
97pub enum CurrencyCodeError {
98    /// Currency codes must be exactly three bytes long.
99    InvalidLength,
100    /// Currency codes must contain only ASCII alphabetic bytes.
101    NotAlphabetic,
102    /// Currency codes must be uppercase.
103    NotUppercase,
104}
105
106impl fmt::Display for CurrencyCodeError {
107    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
108        match self {
109            Self::InvalidLength => formatter.write_str("currency code must be exactly 3 letters"),
110            Self::NotAlphabetic => {
111                formatter.write_str("currency code must contain only ASCII letters")
112            },
113            Self::NotUppercase => formatter.write_str("currency code must be uppercase"),
114        }
115    }
116}
117
118impl Error for CurrencyCodeError {}
119
120#[cfg(test)]
121mod tests {
122    use super::{AUD, CAD, CurrencyCode, CurrencyCodeError, EUR, GBP, JPY, USD};
123
124    #[test]
125    fn accepts_common_uppercase_codes() -> Result<(), CurrencyCodeError> {
126        for code in [USD, EUR, GBP, CAD, AUD, JPY] {
127            let currency = CurrencyCode::new(code)?;
128            assert_eq!(currency.as_str(), code);
129            assert_eq!(currency.to_string(), code);
130        }
131        Ok(())
132    }
133
134    #[test]
135    fn rejects_lowercase_codes() {
136        assert_eq!(
137            CurrencyCode::new("usd"),
138            Err(CurrencyCodeError::NotUppercase)
139        );
140    }
141
142    #[test]
143    fn rejects_invalid_shapes() {
144        assert_eq!(
145            CurrencyCode::new("US"),
146            Err(CurrencyCodeError::InvalidLength)
147        );
148        assert_eq!(
149            CurrencyCode::new("USDA"),
150            Err(CurrencyCodeError::InvalidLength)
151        );
152        assert_eq!(
153            CurrencyCode::new("U1D"),
154            Err(CurrencyCodeError::NotAlphabetic)
155        );
156    }
157}