use crate::error::{FinMoneyError, Result};
use tinystr::TinyAsciiStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct FinMoneyCurrency {
id: i32,
name: Option<TinyAsciiStr<52>>,
code: TinyAsciiStr<16>,
precision: u8,
}
impl Default for FinMoneyCurrency {
fn default() -> Self {
Self {
id: 0,
name: None,
code: FinMoneyCurrency::UNDEFINED_CODE,
precision: 8,
}
}
}
impl FinMoneyCurrency {
const UNDEFINED_CODE: TinyAsciiStr<16> =
unsafe { TinyAsciiStr::from_utf8_unchecked(*b"UNDEFINED\0\0\0\0\0\0\0") };
const INVALID_CODE: TinyAsciiStr<16> =
unsafe { TinyAsciiStr::from_utf8_unchecked(*b"INVALID\0\0\0\0\0\0\0\0\0") };
pub fn new(
id: i32,
code: impl Into<String>,
name: Option<impl Into<String>>,
precision: u8,
) -> Result<FinMoneyCurrency> {
if precision > 28 {
return Err(FinMoneyError::InvalidPrecision(precision as u32));
}
let code = code.into();
let parsed_name = match name {
Some(n) => {
let n = n.into();
match Self::sanitize_and_parse_name(&n) {
Ok(ascii_name) => Some(ascii_name),
Err(_) => return Err(FinMoneyError::InvalidCurrencyName(n)),
}
}
None => None,
};
let parsed_code = Self::sanitize_and_parse_code(code.as_str())
.map_err(|_| FinMoneyError::InvalidCurrencyCode(code))?;
Ok(Self {
id,
name: parsed_name,
code: parsed_code,
precision,
})
}
pub fn new_from_tiny(
id: i32,
code: TinyAsciiStr<16>,
name: Option<TinyAsciiStr<52>>,
precision: u8,
) -> Result<FinMoneyCurrency> {
if precision > 28 {
return Err(FinMoneyError::InvalidPrecision(precision as u32));
}
Ok(Self {
id,
name,
code,
precision,
})
}
pub fn new_sanitized(
id: i32,
code: String,
name: Option<String>,
precision: u8,
) -> FinMoneyCurrency {
let clamped_precision = precision.min(28);
let sanitized_name = name.and_then(|n| Self::sanitize_and_parse_name(&n).ok());
let sanitized_code =
Self::sanitize_and_parse_code(&code).unwrap_or(FinMoneyCurrency::INVALID_CODE);
Self {
id,
name: sanitized_name,
code: sanitized_code,
precision: clamped_precision,
}
}
#[inline]
pub fn get_id(&self) -> i32 {
self.id
}
#[inline]
pub fn get_name(&self) -> Option<&str> {
self.name.as_ref().map(|s| s.as_str())
}
#[inline]
pub fn get_code(&self) -> &str {
self.code.as_str()
}
#[inline]
pub fn get_precision(&self) -> u8 {
self.precision
}
pub fn with_precision(&self, precision: u8) -> Result<FinMoneyCurrency> {
if precision > 28 {
return Err(FinMoneyError::InvalidPrecision(precision as u32));
}
Ok(FinMoneyCurrency {
id: self.id,
name: self.name,
code: self.code,
precision,
})
}
#[inline]
pub fn is_same_currency(&self, other: &FinMoneyCurrency) -> bool {
self.id == other.id
}
#[inline]
fn sanitize_ascii_truncate(input: &str, max_len: usize) -> String {
let mut out = String::with_capacity(std::cmp::min(input.len(), max_len));
for (count, ch) in input.chars().enumerate() {
if count == max_len {
break;
}
out.push(if ch.is_ascii() { ch } else { '_' });
}
out
}
fn sanitize_and_parse_name(
name: &str,
) -> std::result::Result<TinyAsciiStr<52>, tinystr::ParseError> {
if let Ok(ascii_name) = name.parse() {
return Ok(ascii_name);
}
let sanitized = Self::sanitize_ascii_truncate(name, 52);
sanitized.parse()
}
fn sanitize_and_parse_code(
code: &str,
) -> std::result::Result<TinyAsciiStr<16>, tinystr::ParseError> {
if let Ok(ascii_code) = code.parse() {
return Ok(ascii_code);
}
let sanitized = Self::sanitize_ascii_truncate(code, 16);
sanitized.parse()
}
}
impl FinMoneyCurrency {
pub const USD: FinMoneyCurrency = FinMoneyCurrency {
id: 1,
name: None, code: unsafe { TinyAsciiStr::from_utf8_unchecked(*b"USD\0\0\0\0\0\0\0\0\0\0\0\0\0") },
precision: 2,
};
pub const EUR: FinMoneyCurrency = FinMoneyCurrency {
id: 2,
name: None,
code: unsafe { TinyAsciiStr::from_utf8_unchecked(*b"EUR\0\0\0\0\0\0\0\0\0\0\0\0\0") },
precision: 2,
};
pub const BTC: FinMoneyCurrency = FinMoneyCurrency {
id: 3,
name: None,
code: unsafe { TinyAsciiStr::from_utf8_unchecked(*b"BTC\0\0\0\0\0\0\0\0\0\0\0\0\0") },
precision: 8,
};
pub const ETH: FinMoneyCurrency = FinMoneyCurrency {
id: 4,
name: None,
code: unsafe { TinyAsciiStr::from_utf8_unchecked(*b"ETH\0\0\0\0\0\0\0\0\0\0\0\0\0") },
precision: 18,
};
}