1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
//! [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,
			"&currency = &currency.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!(),
		}
	}
}