//! [Currency codes](CurrencyCode).
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;
/// [Currency code](https://en.wikipedia.org/wiki/ISO_4217).
///
/// It's recommended to use the constants in the [`currencies`](list) module.
#[derive(Debug, Clone, Copy)]
#[repr(C, align(8))]
pub struct CurrencyCode {
// Notes about the representation of the code:
// - Variable-length (CURRENCY_LEN_MIN to CURRENCY_LEN_MAX).
// - Stored in 8 bytes.
// - Its value is the code in uppercase ASCII, followed by zeroes.
// - The first CURRENCY_LEN_MIN is split as NonZeroU8 to enable niche optimization.
/// The first `CURRENCY_LEN_MIN` letters of the code.
code_head: [NonZeroU8; CURRENCY_LEN_MIN],
/// The tail of the code.
code_tail: [u8; CURRENCY_LEN_MAX - CURRENCY_LEN_MIN],
/// Padding, must be zeroed out.
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)
}
}
}
/// The default currency code is [`USD`](list::USD).
///
/// It is chosen for being the most traded currency.
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 {
/// Creates a new [`CurrencyCode`] value.
///
/// # Safety
/// - See comments for [`CurrencyCode`] struct.
/// - The const parameter `N` must be in the range [CURRENCY_LEN_MIN..CURRENCY_LEN_MAX].
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)
}
/// Creates a new [`CurrencyCode`] value.
///
/// # Safety
/// Ensure that the code's length is within range [2..5].
/// The code must consist only of uppercase ASCII characters, and be terminated by zeroes until
/// the end of the slice.
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 {
// SAFETY:
// (1) `tail` adjacently tails `head` (per repr(C), tested).
// (2) NonZeroU8 is repr(transparent) on u8: https://doc.rust-lang.org/std/num/struct.NonZeroU8.html#:~:text=%23%5Brepr(transparent)%5D.
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 {
// safety: the code is always ASCII per the invariant documented in CurrencyCode::code therefore
// valid UTF-8 .
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) }
}
/// Invalid currency code error.
///
/// Valid currency codes are three uppercase alpha ASCII characters.
#[derive(Debug, thiserror::Error)]
pub enum Error {
/// The currency code is too short.
#[error("the currency code is too short")]
TooShort,
/// The currency code is too long.
#[error("the currency code is too long")]
TooLong,
/// The currency code has an invalid character.
#[error("invalid currency code character ({0:?})")]
InvalidCharacter(u8),
}
pub mod list {
//! [Currencies](super::CurrencyCode) constants.
//!
//! This module defines all known currencies as constants, as well as [`ARRAY`]
//! which contains all of them in a constant array.
/// Defines const [`super::CurrencyCode`]s.
///
/// # Safety
/// Ensure all arguments consist of only uppercase alpha characters.
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)) };
)*
/// The length of all currencies defined in this module.
const LEN: usize = 0 $(+ { stringify!($currency); 1} )*;
/// An array of all the currencies defined in this module.
pub const ARRAY: [crate::CurrencyCode; LEN] = [ $( $currency ),* ];
};
}
// Currencies are documented here: https://currencyapi.com/docs/currency-list
// DEPRECATED NOTE:
// To update this list, open dev-tools on the page, evaluate
// ```js
// [...document.querySelectorAll("td:first-child")].map(td => td.textContent).join()
// ```
// right click on the result, select "Copy string contents", and paste below between the parentheses.
// The docs aren't synced tightly enough, it's better to update by making a request and pulling
// the currencies from there. This can be easily done in the [currencyapi
// dashboard](https://app.currencyapi.com/dashboard).
// Paste into browser developer console and:
// ```js
// Object.keys(payload.data).join(", ")
// ```
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!(),
}
}
}