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 FinMoneyCurrency {
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_code_tiny(&self) -> TinyAsciiStr<16> {
self.code
}
#[inline]
pub fn get_name_tiny(&self) -> Option<TinyAsciiStr<52>> {
self.name
}
#[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 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()),
}
}
}