1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::fmt;
5
6pub const COMMON_CURRENCY_CODES: &[&str] = &["USD", "EUR", "GBP", "JPY", "CAD", "AUD", "CHF"];
8
9#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
11pub struct CurrencyCode {
12 value: String,
13}
14
15impl CurrencyCode {
16 #[must_use]
18 pub fn new(input: &str) -> Option<Self> {
19 parse_currency_code(input)
20 }
21
22 #[must_use]
24 pub fn as_str(&self) -> &str {
25 &self.value
26 }
27
28 #[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#[must_use]
49pub fn parse_currency_code(input: &str) -> Option<CurrencyCode> {
50 normalize_currency_code(input).map(|value| CurrencyCode { value })
51}
52
53#[must_use]
55pub fn is_currency_code(input: &str) -> bool {
56 normalize_currency_code(input).is_some()
57}
58
59#[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}