Skip to main content

use_currency_code/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::fmt;
5
6/// Common currency code examples used in docs and tests.
7pub const COMMON_CURRENCY_CODES: &[&str] = &["USD", "EUR", "GBP", "JPY", "CAD", "AUD", "CHF"];
8
9/// A normalized 3-letter currency code identifier.
10#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
11pub struct CurrencyCode {
12    value: String,
13}
14
15impl CurrencyCode {
16    /// Parses and normalizes a currency code identifier.
17    #[must_use]
18    pub fn new(input: &str) -> Option<Self> {
19        parse_currency_code(input)
20    }
21
22    /// Returns the normalized currency code identifier.
23    #[must_use]
24    pub fn as_str(&self) -> &str {
25        &self.value
26    }
27
28    /// Consumes the currency code and returns the normalized string.
29    #[must_use]
30    pub fn into_string(self) -> String {
31        self.value
32    }
33}
34
35impl AsRef<str> for CurrencyCode {
36    fn as_ref(&self) -> &str {
37        self.as_str()
38    }
39}
40
41impl fmt::Display for CurrencyCode {
42    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
43        formatter.write_str(self.as_str())
44    }
45}
46
47/// Parses a currency code identifier and normalizes it to uppercase.
48#[must_use]
49pub fn parse_currency_code(input: &str) -> Option<CurrencyCode> {
50    normalize_currency_code(input).map(|value| CurrencyCode { value })
51}
52
53/// Returns `true` when the input is a 3-letter currency code identifier.
54#[must_use]
55pub fn is_currency_code(input: &str) -> bool {
56    normalize_currency_code(input).is_some()
57}
58
59/// Normalizes a 3-letter currency code identifier to uppercase.
60#[must_use]
61pub fn normalize_currency_code(input: &str) -> Option<String> {
62    let trimmed = input.trim();
63    if trimmed.len() != 3 || !trimmed.bytes().all(|byte| byte.is_ascii_alphabetic()) {
64        return None;
65    }
66
67    Some(trimmed.to_ascii_uppercase())
68}
69
70#[cfg(test)]
71mod tests {
72    use super::{
73        COMMON_CURRENCY_CODES, CurrencyCode, is_currency_code, normalize_currency_code,
74        parse_currency_code,
75    };
76
77    #[test]
78    fn accepts_common_currency_examples() {
79        for currency in COMMON_CURRENCY_CODES {
80            assert!(is_currency_code(currency));
81            assert_eq!(parse_currency_code(currency).unwrap().as_str(), *currency);
82        }
83    }
84
85    #[test]
86    fn normalizes_to_uppercase() {
87        assert_eq!(normalize_currency_code("usd"), Some("USD".to_string()));
88        assert_eq!(normalize_currency_code(" eur "), Some("EUR".to_string()));
89        assert_eq!(CurrencyCode::new("gbp").unwrap().as_str(), "GBP");
90    }
91
92    #[test]
93    fn rejects_invalid_currency_shapes() {
94        for currency in ["", "US", "USDA", "U1D", "USD-", "€€€"] {
95            assert!(!is_currency_code(currency));
96            assert!(parse_currency_code(currency).is_none());
97        }
98    }
99}