use std::{
fmt::{self, Debug, Display, Formatter},
num::NonZeroU8, str::FromStr, mem, ptr, hash::Hash,
};
const CURRENCY_LEN_MIN: usize = 2;
const CURRENCY_LEN_MAX: usize = 5;
#[derive(Debug, Clone, Copy)]
#[repr(C, align(8))]
pub struct CurrencyCode {
code_head: [NonZeroU8; CURRENCY_LEN_MIN],
code_tail: [u8; CURRENCY_LEN_MAX - CURRENCY_LEN_MIN],
padding: [u8; 8 - CURRENCY_LEN_MAX],
}
impl CurrencyCode {
const fn as_u64(self) -> u64 {
unsafe {
*(&self as *const Self as *const u8 as *const u64)
}
}
}
impl Default for CurrencyCode { #[inline] fn default() -> Self { list::USD } }
impl PartialEq for CurrencyCode {
#[inline] fn eq(&self, other: &Self) -> bool { self.as_u64() == other.as_u64() }
} impl Eq for CurrencyCode {}
impl Hash for CurrencyCode {
#[inline] fn hash<H: std::hash::Hasher>(&self, state: &mut H) { self.as_u64().hash(state) }
}
impl PartialOrd for CurrencyCode {
#[inline] fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.as_u64().partial_cmp(&other.as_u64())
}
}
impl Ord for CurrencyCode {
#[inline] fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.as_u64().cmp(&other.as_u64())
}
}
impl CurrencyCode {
const unsafe fn from_array_unchecked<const N: usize>(code: [u8; N]) -> Self {
let mut buf = [0u8; mem::size_of::<CurrencyCode>()];
let mut n = 0;
while n < N {
buf[n] = code[n];
n += 1;
}
std::mem::transmute(buf)
}
pub unsafe fn new_unchecked(code: &[u8]) -> Self {
let mut buf = [0u8; CURRENCY_LEN_MAX];
ptr::copy_nonoverlapping::<u8>(
code.as_ptr(),
buf.as_mut_ptr(),
code.len()
);
Self::from_array_unchecked(buf)
}
}
impl TryFrom<&[u8]> for CurrencyCode {
type Error = Error;
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
let len = value.len();
if len < CURRENCY_LEN_MIN { return Err(Error::TooShort); }
if len > CURRENCY_LEN_MAX { return Err(Error::TooLong); }
let bad_char = value[..CURRENCY_LEN_MIN].iter().find(|&&c| !c.is_ascii_uppercase())
.and(value[CURRENCY_LEN_MIN..].iter().find(|&&c| !c.is_ascii_uppercase() && c != 0))
.copied();
if let Some(bad_char) = bad_char { return Err(Error::InvalidCharacter(bad_char)); }
unsafe { Ok(Self::new_unchecked(value)) }
}
}
impl FromStr for CurrencyCode {
type Err = Error;
#[inline] fn from_str(s: &str) -> Result<Self, Self::Err> { <Self as TryFrom<&[u8]>>::try_from(s.as_ref()) }
}
impl AsRef<[u8]> for CurrencyCode {
#[inline] fn as_ref(&self) -> &[u8] {
let tail_len = self.code_tail.into_iter().take_while(|&c| c != 0).count();
unsafe {
std::slice::from_raw_parts(
self as *const Self as *const u8,
CURRENCY_LEN_MIN + tail_len
)
}
}
}
impl AsRef<str> for CurrencyCode {
#[inline] fn as_ref(&self) -> &str {
unsafe {
std::str::from_utf8_unchecked(self.as_ref())
}
}
}
impl Display for CurrencyCode {
#[inline] fn fmt(&self, f: &mut Formatter) -> fmt::Result { Display::fmt(AsRef::<str>::as_ref(&self), f) }
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("the currency code is too short")]
TooShort,
#[error("the currency code is too long")]
TooLong,
#[error("invalid currency code character ({0:?})")]
InvalidCharacter(u8),
}
pub mod list {
macro_rules! unsafe_define_currencies {
($from_fn:expr, $($currency:ident),*) => {
$(
#[doc=concat!("The [", stringify!($currency), "](https://www.google.com/search?q=USD+to+", stringify!($currency), ") currency code.")]
pub const $currency: crate::CurrencyCode = unsafe { crate::CurrencyCode::from_array_unchecked(*bstringify::bstringify!($currency)) };
)*
const LEN: usize = 0 $(+ { stringify!($currency); 1} )*;
pub const ARRAY: [crate::CurrencyCode; LEN] = [ $( $currency ),* ];
};
}
unsafe_define_currencies!(
ADA, AED, AFN, ALL, AMD, ANG, AOA, ARB, ARS, AUD, AVAX, AWG, AZN, BAM, BBD, BDT, BGN, BHD, BIF, BMD, BNB, BND, BOB, BRL, BSD, BTC, BTN, BUSD, BWP, BYN, BYR, BZD, CAD, CDF, CHF, CLF, CLP, CNY, COP, CRC, CUC, CUP, CVE, CZK, DAI, DJF, DKK, DOP, DOT, DZD, EGP, ERN, ETB, ETH, EUR, FJD, FKP, GBP, GEL, GGP, GHS, GIP, GMD, GNF, GTQ, GYD, HKD, HNL, HRK, HTG, HUF, IDR, ILS, IMP, INR, IQD, IRR, ISK, JEP, JMD, JOD, JPY, KES, KGS, KHR, KMF, KPW, KRW, KWD, KYD, KZT, LAK, LBP, LKR, LRD, LSL, LTC, LTL, LVL, LYD, MAD, MATIC, MDL, MGA, MKD, MMK, MNT, MOP, MRO, MUR, MVR, MWK, MXN, MYR, MZN, NAD, NGN, NIO, NOK, NPR, NZD, OMR, OP, PAB, PEN, PGK, PHP, PKR, PLN, PYG, QAR, RON, RSD, RUB, RWF, SAR, SBD, SCR, SDG, SEK, SGD, SHP, SLL, SOL, SOS, SRD, STD, SVC, SYP, SZL, THB, TJS, TMT, TND, TOP, TRY, TTD, TWD, TZS, UAH, UGX, USD, USDC, USDT, UYU, UZS, VEF, VND, VUV, WST, XAF, XAG, XAU, XCD, XDR, XOF, XPD, XPF, XPT, XRP, YER, ZAR, ZMK, ZMW, ZWL
);
}
#[cfg(test)]
mod tests {
use super::*;
const AVAX_MANUAL: CurrencyCode = CurrencyCode {
code_head: unsafe { [
NonZeroU8::new_unchecked(b'A'),
NonZeroU8::new_unchecked(b'V'),
] },
code_tail: [b'A', b'X', 0],
padding: [0; 8 - CURRENCY_LEN_MAX],
};
#[test]
fn test_repr() {
assert_eq!(
mem::size_of::<CurrencyCode>(),
mem::size_of::<u64>(),
"sizeof(CurrencyCode) = sizeof(u64)"
);
let avax = AVAX_MANUAL;
assert_eq!(
&avax as *const _ as usize,
avax.code_head.as_ptr() as usize,
"¤cy = ¤cy.code_head"
);
assert_eq!(
avax.code_head.as_ptr() as usize + CURRENCY_LEN_MIN,
avax.code_tail.as_ptr() as usize
);
}
#[test]
fn test_as_ref_bytes_4() {
assert_eq!(
<CurrencyCode as AsRef<[u8]>>::as_ref(&AVAX_MANUAL),
b"AVAX",
);
}
#[test]
fn test_parse_1() {
match "A".parse::<CurrencyCode>() {
Err(Error::TooShort) => {},
_ => panic!(),
}
}
#[test]
fn test_parse_2() {
assert_eq!(
"OP".parse::<CurrencyCode>().unwrap(),
unsafe { CurrencyCode::from_array_unchecked(*b"OP") },
);
}
#[test]
fn test_parse_3() {
assert_eq!(
"USD".parse::<CurrencyCode>().unwrap(),
unsafe { CurrencyCode::from_array_unchecked(*b"USD") },
);
}
#[test]
fn test_parse_4() {
assert_eq!(
"AVAX".parse::<CurrencyCode>().unwrap(),
unsafe { CurrencyCode::from_array_unchecked(*b"AVAX") },
);
}
#[test]
fn test_parse_5() {
assert_eq!(
"MATIC".parse::<CurrencyCode>().unwrap(),
unsafe { CurrencyCode::from_array_unchecked(*b"MATIC") },
);
}
#[test]
fn test_parse_6() {
match "ABCDEF".parse::<CurrencyCode>() {
Err(Error::TooLong) => {},
_ => panic!(),
}
}
}