use crate::{CurrencyError, Result};
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Currency {
code: String,
}
impl Currency {
pub fn new<S: Into<String>>(code: S) -> Result<Self> {
let code = code.into().to_uppercase();
if !crate::api::is_valid_currency_code(&code) {
return Err(CurrencyError::invalid_currency(&code));
}
Ok(Currency { code })
}
pub fn code(&self) -> &str {
&self.code
}
pub fn is_same_as(&self, other: &Currency) -> bool {
self.code == other.code
}
}
impl fmt::Display for Currency {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.code)
}
}
impl TryFrom<&str> for Currency {
type Error = CurrencyError;
fn try_from(code: &str) -> Result<Self> {
Currency::new(code)
}
}
impl TryFrom<String> for Currency {
type Error = CurrencyError;
fn try_from(code: String) -> Result<Self> {
Currency::new(code)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Money {
amount: f64,
currency: Currency,
}
impl Money {
pub fn new(amount: f64, currency: Currency) -> Result<Self> {
Self::validate_amount(amount)?;
Ok(Money { amount, currency })
}
pub fn from_code<S: Into<String>>(amount: f64, currency_code: S) -> Result<Self> {
let currency = Currency::new(currency_code)?;
Self::new(amount, currency)
}
pub fn amount(&self) -> f64 {
self.amount
}
pub fn currency(&self) -> &Currency {
&self.currency
}
pub fn round(&self, decimal_places: u32) -> Self {
let multiplier = 10_f64.powi(decimal_places as i32);
let rounded_amount = (self.amount * multiplier).round() / multiplier;
Money {
amount: rounded_amount,
currency: self.currency.clone(),
}
}
fn validate_amount(amount: f64) -> Result<()> {
if amount < 0.0 {
return Err(CurrencyError::invalid_amount(amount));
}
if amount.is_nan() || amount.is_infinite() {
return Err(CurrencyError::invalid_amount(amount));
}
if amount > 1_000_000_000_000.0 {
return Err(CurrencyError::invalid_amount(amount));
}
Ok(())
}
}
impl fmt::Display for Money {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:.2} {}", self.amount, self.currency)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConversionRequest {
pub from: Money,
pub to: Currency,
}
impl ConversionRequest {
pub fn new(from: Money, to: Currency) -> Self {
ConversionRequest { from, to }
}
pub fn from_components(amount: f64, from_currency: &str, to_currency: &str) -> Result<Self> {
let from = Money::from_code(amount, from_currency)?;
let to = Currency::new(to_currency)?;
Ok(ConversionRequest::new(from, to))
}
pub fn is_same_currency(&self) -> bool {
self.from.currency.is_same_as(&self.to)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConversionResult {
pub request: ConversionRequest,
pub result: Money,
pub exchange_rate: f64,
pub timestamp: i64,
pub conversion_type: ConversionType,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ConversionType {
SameCurrency,
Direct,
Cross { via_currency: String },
}
impl ConversionResult {
pub fn new(
request: ConversionRequest,
result: Money,
exchange_rate: f64,
conversion_type: ConversionType,
) -> Self {
ConversionResult {
request,
result,
exchange_rate,
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64,
conversion_type,
}
}
pub fn summary(&self) -> String {
match self.conversion_type {
ConversionType::SameCurrency => {
format!("{} (same currency)", self.request.from)
}
ConversionType::Direct => {
format!(
"{} → {} (rate: {:.6})",
self.request.from, self.result, self.exchange_rate
)
}
ConversionType::Cross { ref via_currency } => {
format!(
"{} → {} via {} (rate: {:.6})",
self.request.from, self.result, via_currency, self.exchange_rate
)
}
}
}
}
impl fmt::Display for ConversionResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.summary())
}
}
#[cfg(test)]
mod tests;