jobber 1.1.5-alpha

Minimalistic console work time tracker
use derive_more::Deref;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, str::FromStr, sync::LazyLock};

use crate::entities::{Error, Result};

#[derive(Deref, Copy, Clone, PartialEq)]
pub(crate) struct Currency(&'static rusty_money::iso::Currency);
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub(crate) struct Money(rusty_money::Money<'static, rusty_money::iso::Currency>);
type Decimal = rust_decimal::Decimal;

static CURRENCY_SYMBOLS: LazyLock<HashMap<&'static str, &'static str>> = LazyLock::new(|| {
    HashMap::from([
        ("$", "USD"),
        ("", "EUR"),
        ("£", "GBP"),
        ("¥", "JPY"),
        ("", "KRW"),
        ("", "INR"),
        ("", "RUB"),
        ("", "TRY"),
        ("", "UAH"),
        ("", "VND"),
        ("", "ILS"),
        ("", "NGN"),
        ("", "PHP"),
        ("฿", "THB"),
        ("", "CRC"),
        ("", "PYG"),
        ("", "GHS"),
        ("", "LAK"),
        ("", "MNT"),
        ("", "KZT"),
        ("", "AZN"),
        ("", "GEL"),
        ("", "CHF"),
    ])
});

fn replace_currency_symbol(input: &str) -> String {
    if let Some(first) = input.chars().next() {
        let symbol = &input[..first.len_utf8()];

        if let Some(code) = CURRENCY_SYMBOLS.get(symbol) {
            return format!("{}{}", code, &input[first.len_utf8()..]);
        }
    }

    input.to_owned()
}

impl FromStr for Currency {
    type Err = Error;

    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        let s = replace_currency_symbol(s);
        Ok(Currency(rusty_money::iso::find(&s).ok_or_else(|| {
            Error::CouldNotParseRate(format!("Unknown currency: '{s}'"), s)
        })?))
    }
}

impl Default for Money {
    fn default() -> Self {
        Money::from_decimal(0.into(), local_currency())
    }
}

impl std::fmt::Display for Money {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        self.0.fmt(f)
    }
}

impl Money {
    pub(crate) fn from_decimal(amount: Decimal, currency: Currency) -> Money {
        Money(rusty_money::Money::from_decimal(amount, &currency))
    }

    pub(crate) fn zero(currency: Currency) -> Money {
        Money(rusty_money::Money::from_minor(0, &currency))
    }

    pub(crate) fn add(self, rhs: Self) -> Result<Money> {
        Ok(Money(self.0.add(rhs.0)?))
    }

    pub(crate) fn mul<N: Into<Decimal>>(&self, n: N) -> Result<Money> {
        Ok(Money(self.0.mul(n)?))
    }

    pub(crate) fn currency(&self) -> Currency {
        Currency(self.0.currency())
    }

    pub(crate) fn is_zero(&self) -> bool {
        self.0.is_zero()
    }
}

#[allow(unused)]
pub(crate) fn local_currency() -> Currency {
    use rusty_money::iso;
    use sys_locale::get_locale;
    let locale = get_locale().unwrap_or_else(|| "en-US".to_string());
    let region = locale.split('-').nth(1).unwrap_or("").to_uppercase();

    Currency(match region.as_str() {
        // Eurozone
        "DE" | "AT" | "FR" | "IT" | "ES" | "NL" | "BE" | "PT" | "FI" | "IE" | "GR" | "SK"
        | "SI" | "EE" | "LV" | "LT" => iso::EUR,

        "US" => iso::USD,
        "GB" => iso::GBP,
        "CH" => iso::CHF,
        "JP" => iso::JPY,
        "CN" => iso::CNY,
        "CA" => iso::CAD,
        "AU" => iso::AUD,
        "IN" => iso::INR,
        "BR" => iso::BRL,
        "PL" => iso::PLN,
        "CZ" => iso::CZK,
        "SE" => iso::SEK,
        "NO" => iso::NOK,
        "DK" => iso::DKK,

        _ => todo!("currency error"),
    })
}