use crate::error::{FinMoneyError, Result};
use std::fmt;
use tinystr::{TinyAsciiStr, tinystr};
#[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> = tinystr!(16, "UNDEFINED");
const INVALID_CODE: TinyAsciiStr<16> = tinystr!(16, "INVALID");
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: tinystr!(16, "USD"),
precision: 2,
};
pub const EUR: FinMoneyCurrency = FinMoneyCurrency {
id: 2,
name: None,
code: tinystr!(16, "EUR"),
precision: 2,
};
pub const BTC: FinMoneyCurrency = FinMoneyCurrency {
id: 3,
name: None,
code: tinystr!(16, "BTC"),
precision: 8,
};
pub const ETH: FinMoneyCurrency = FinMoneyCurrency {
id: 4,
name: None,
code: tinystr!(16, "ETH"),
precision: 18,
};
pub const GBP: FinMoneyCurrency = FinMoneyCurrency {
id: 5,
name: None,
code: tinystr!(16, "GBP"),
precision: 2,
};
pub const JPY: FinMoneyCurrency = FinMoneyCurrency {
id: 6,
name: None,
code: tinystr!(16, "JPY"),
precision: 0,
};
pub const CHF: FinMoneyCurrency = FinMoneyCurrency {
id: 7,
name: None,
code: tinystr!(16, "CHF"),
precision: 2,
};
pub const CNY: FinMoneyCurrency = FinMoneyCurrency {
id: 8,
name: None,
code: tinystr!(16, "CNY"),
precision: 2,
};
pub const RUB: FinMoneyCurrency = FinMoneyCurrency {
id: 9,
name: None,
code: tinystr!(16, "RUB"),
precision: 2,
};
pub const USDT: FinMoneyCurrency = FinMoneyCurrency {
id: 10,
name: None,
code: tinystr!(16, "USDT"),
precision: 6,
};
pub const SOL: FinMoneyCurrency = FinMoneyCurrency {
id: 11,
name: None,
code: tinystr!(16, "SOL"),
precision: 9,
};
pub fn all_predefined() -> &'static [FinMoneyCurrency] {
static ALL: [FinMoneyCurrency; 11] = [
FinMoneyCurrency::USD,
FinMoneyCurrency::EUR,
FinMoneyCurrency::BTC,
FinMoneyCurrency::ETH,
FinMoneyCurrency::GBP,
FinMoneyCurrency::JPY,
FinMoneyCurrency::CHF,
FinMoneyCurrency::CNY,
FinMoneyCurrency::RUB,
FinMoneyCurrency::USDT,
FinMoneyCurrency::SOL,
];
&ALL
}
}
impl fmt::Display for FinMoneyCurrency {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.get_name() {
Some(name) => write!(f, "{} ({})", self.get_code(), name),
None => write!(f, "{}", self.get_code()),
}
}
}