decimal-money 0.2.0

Money types for Rust — CurrencyAmount with Decimal precision, multi-currency support, FX conversion, and formatting
Documentation
#[cfg(feature = "serde_impl")]
use serde::{Deserialize, Serialize};
use std::fmt;

/// Represents a supported currency.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde_impl", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub enum Currency {
    /// US Dollar.
    USD,
    /// Euro.
    EUR,
    /// British Pound.
    GBP,
    /// Japanese Yen.
    JPY,
    /// Swiss Franc.
    CHF,
    /// Canadian Dollar.
    CAD,
    /// Australian Dollar.
    AUD,
    /// Chinese Yuan.
    CNY,
    /// Indian Rupee.
    INR,
    /// Brazilian Real.
    BRL,
    /// South Korean Won.
    KRW,
    /// Mexican Peso.
    MXN,
    /// Swedish Krona.
    SEK,
    /// Norwegian Krone.
    NOK,
    /// Danish Krone.
    DKK,
    /// Polish Zloty.
    PLN,
    /// Czech Koruna.
    CZK,
    /// Hungarian Forint.
    HUF,
    /// Russian Ruble.
    RUB,
    /// South African Rand.
    ZAR,
    /// Singapore Dollar.
    SGD,
    /// Hong Kong Dollar.
    HKD,
    /// New Zealand Dollar.
    NZD,
    /// Thai Baht.
    THB,
    /// Turkish Lira.
    TRY,
    /// UAE Dirham.
    AED,
    /// Saudi Riyal.
    SAR,
    /// Nigerian Naira.
    NGN,
    /// Egyptian Pound.
    EGP,
    /// Philippine Peso.
    PHP,
    /// Indonesian Rupiah.
    IDR,
    /// Malaysian Ringgit.
    MYR,
    /// Vietnamese Dong.
    VND,
    /// Pakistani Rupee.
    PKR,
    /// Bangladeshi Taka.
    BDT,
    /// Bitcoin.
    BTC,
    /// Ethereum.
    ETH,
}

impl Currency {
    /// Returns the ISO 4217 currency code.
    pub fn code(&self) -> &'static str {
        match self {
            Currency::USD => "USD",
            Currency::EUR => "EUR",
            Currency::GBP => "GBP",
            Currency::JPY => "JPY",
            Currency::CHF => "CHF",
            Currency::CAD => "CAD",
            Currency::AUD => "AUD",
            Currency::CNY => "CNY",
            Currency::INR => "INR",
            Currency::BRL => "BRL",
            Currency::KRW => "KRW",
            Currency::MXN => "MXN",
            Currency::SEK => "SEK",
            Currency::NOK => "NOK",
            Currency::DKK => "DKK",
            Currency::PLN => "PLN",
            Currency::CZK => "CZK",
            Currency::HUF => "HUF",
            Currency::RUB => "RUB",
            Currency::ZAR => "ZAR",
            Currency::SGD => "SGD",
            Currency::HKD => "HKD",
            Currency::NZD => "NZD",
            Currency::THB => "THB",
            Currency::TRY => "TRY",
            Currency::AED => "AED",
            Currency::SAR => "SAR",
            Currency::NGN => "NGN",
            Currency::EGP => "EGP",
            Currency::PHP => "PHP",
            Currency::IDR => "IDR",
            Currency::MYR => "MYR",
            Currency::VND => "VND",
            Currency::PKR => "PKR",
            Currency::BDT => "BDT",
            Currency::BTC => "BTC",
            Currency::ETH => "ETH",
        }
    }

    /// Returns the display symbol for the currency.
    pub fn symbol(&self) -> &'static str {
        match self {
            Currency::USD => "$",
            Currency::EUR => "\u{20AC}",
            Currency::GBP => "\u{00A3}",
            Currency::JPY => "\u{00A5}",
            Currency::CHF => "CHF",
            Currency::CAD => "C$",
            Currency::AUD => "A$",
            Currency::CNY => "\u{00A5}",
            Currency::INR => "\u{20B9}",
            Currency::BRL => "R$",
            Currency::KRW => "\u{20A9}",
            Currency::MXN => "MX$",
            Currency::SEK => "kr",
            Currency::NOK => "kr",
            Currency::DKK => "kr",
            Currency::PLN => "z\u{0142}",
            Currency::CZK => "K\u{010D}",
            Currency::HUF => "Ft",
            Currency::RUB => "\u{20BD}",
            Currency::ZAR => "R",
            Currency::SGD => "S$",
            Currency::HKD => "HK$",
            Currency::NZD => "NZ$",
            Currency::THB => "\u{0E3F}",
            Currency::TRY => "\u{20BA}",
            Currency::AED => "AED",
            Currency::SAR => "SAR",
            Currency::NGN => "\u{20A6}",
            Currency::EGP => "E\u{00A3}",
            Currency::PHP => "\u{20B1}",
            Currency::IDR => "Rp",
            Currency::MYR => "RM",
            Currency::VND => "\u{20AB}",
            Currency::PKR => "\u{20A8}",
            Currency::BDT => "\u{09F3}",
            Currency::BTC => "\u{20BF}",
            Currency::ETH => "\u{039E}",
        }
    }

    /// Returns the number of decimal places for the currency.
    pub fn decimal_places(&self) -> u32 {
        match self {
            Currency::JPY | Currency::KRW => 0,
            Currency::BTC => 8,
            Currency::ETH => 18,
            _ => 2,
        }
    }

    /// Returns true if this is a cryptocurrency.
    pub fn is_crypto(&self) -> bool {
        matches!(self, Currency::BTC | Currency::ETH)
    }
}

impl fmt::Display for Currency {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.code())
    }
}

impl std::str::FromStr for Currency {
    type Err = crate::error::MoneyError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_uppercase().as_str() {
            "USD" => Ok(Currency::USD),
            "EUR" => Ok(Currency::EUR),
            "GBP" => Ok(Currency::GBP),
            "JPY" => Ok(Currency::JPY),
            "CHF" => Ok(Currency::CHF),
            "CAD" => Ok(Currency::CAD),
            "AUD" => Ok(Currency::AUD),
            "CNY" => Ok(Currency::CNY),
            "INR" => Ok(Currency::INR),
            "BRL" => Ok(Currency::BRL),
            "KRW" => Ok(Currency::KRW),
            "MXN" => Ok(Currency::MXN),
            "SEK" => Ok(Currency::SEK),
            "NOK" => Ok(Currency::NOK),
            "DKK" => Ok(Currency::DKK),
            "PLN" => Ok(Currency::PLN),
            "CZK" => Ok(Currency::CZK),
            "HUF" => Ok(Currency::HUF),
            "RUB" => Ok(Currency::RUB),
            "ZAR" => Ok(Currency::ZAR),
            "SGD" => Ok(Currency::SGD),
            "HKD" => Ok(Currency::HKD),
            "NZD" => Ok(Currency::NZD),
            "THB" => Ok(Currency::THB),
            "TRY" => Ok(Currency::TRY),
            "AED" => Ok(Currency::AED),
            "SAR" => Ok(Currency::SAR),
            "NGN" => Ok(Currency::NGN),
            "EGP" => Ok(Currency::EGP),
            "PHP" => Ok(Currency::PHP),
            "IDR" => Ok(Currency::IDR),
            "MYR" => Ok(Currency::MYR),
            "VND" => Ok(Currency::VND),
            "PKR" => Ok(Currency::PKR),
            "BDT" => Ok(Currency::BDT),
            "BTC" => Ok(Currency::BTC),
            "ETH" => Ok(Currency::ETH),
            _ => Err(crate::error::MoneyError::InvalidAmount(format!(
                "Unknown currency: {s}"
            ))),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_currency_codes() {
        assert_eq!(Currency::USD.code(), "USD");
        assert_eq!(Currency::BTC.code(), "BTC");
    }

    #[test]
    fn test_currency_symbols() {
        assert_eq!(Currency::USD.symbol(), "$");
        assert_eq!(Currency::EUR.symbol(), "\u{20AC}");
    }

    #[test]
    fn test_decimal_places() {
        assert_eq!(Currency::USD.decimal_places(), 2);
        assert_eq!(Currency::JPY.decimal_places(), 0);
        assert_eq!(Currency::BTC.decimal_places(), 8);
    }

    #[test]
    fn test_is_crypto() {
        assert!(Currency::BTC.is_crypto());
        assert!(Currency::ETH.is_crypto());
        assert!(!Currency::USD.is_crypto());
    }

    #[test]
    fn test_from_str() {
        assert_eq!("USD".parse::<Currency>().unwrap(), Currency::USD);
        assert_eq!("btc".parse::<Currency>().unwrap(), Currency::BTC);
        assert!("XYZ".parse::<Currency>().is_err());
    }
}