investments 4.16.1

Helps you with managing your investments
Documentation
#[cfg(test)] use std::str::FromStr;

use lazy_static::lazy_static;
use regex::Regex;
use validator::ValidationError;

use crate::time::Date;
use crate::types::Decimal;
use crate::util;

mod cash;
mod cbr;
mod multi;
mod name_cache;
mod rate_cache;

pub mod converter;

pub use self::cash::{Cash, CashAssets};
pub use self::multi::MultiCurrencyCashAccount;

#[derive(Debug, Clone, Copy)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct CurrencyRate {
    date: Date,
    price: Decimal,
}

pub fn round(amount: Decimal) -> Decimal {
    util::round(amount, 2)
}

pub fn round_to(amount: Decimal, points: u32) -> Decimal {
    util::round(amount, points)
}

pub fn validate_currency(currency: &str) -> Result<(), ValidationError> {
    lazy_static! {
        static ref CURRENCY_REGEX: Regex = Regex::new(r"^[A-Z]{3}$").unwrap();
    }
    if !CURRENCY_REGEX.is_match(currency) {
        return Err(ValidationError::new("Invalid currency"));
    }
    Ok(())
}

pub fn validate_currency_list<C, I>(currencies: C) -> Result<(), ValidationError>
    where
        C: IntoIterator<Item = I>,
        I: AsRef<str>,
{
    for currency in currencies.into_iter() {
        validate_currency(currency.as_ref())?;
    }
    Ok(())
}

fn format_currency(currency: &str, mut amount: &str) -> String {
    let prefix = match currency {
        "AUD" => Some("AU$"),
        "EUR" => Some(""),
        "GBP" => Some("£"),
        "USD" => Some("$"),
        _ => None,
    };

    let mut buffer = String::with_capacity(amount.len() + prefix.map(str::len).unwrap_or(1));

    if let Some(prefix) = prefix {
        if amount.starts_with('-') || amount.starts_with('+') {
            buffer.push_str(&amount[..1]);
            amount = &amount[1..];
        }
        buffer.push_str(prefix);
    }

    buffer.push_str(amount);

    if prefix.is_none() {
        match currency {
            "HKD" => buffer.push_str(" HK$"),
            "RUB" => buffer.push(''),
            _ => {
                buffer.push(' ');
                buffer.push_str(currency);
            },
        };
    }

    buffer
}

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

    #[rstest(input, expected,
        case("1",     "1"),
        case("1.0",   "1"),
        case("1.1",   "1.1"),
        case("1.00",  "1"),
        case("1.01",  "1.01"),
        case("1.11",  "1.11"),
        case("1.004", "1"),
        case("1.005", "1.01"),
        case("1.111", "1.11"),
        case("1.114", "1.11"),
        case("1.124", "1.12"),
        case("1.115", "1.12"),
        case("1.125", "1.13"),
    )]
    fn rounding(input: &str, expected: &str) {
        let from = Decimal::from_str(input).unwrap();
        let to = Decimal::from_str(expected).unwrap();

        let rounded = round(from);
        assert_eq!(rounded, to);

        assert_eq!(&from.to_string(), input);
        assert_eq!(&rounded.to_string(), expected);
    }

    #[rstest(currency, amount, expected,
        case("USD", dec!(12.345), "$12.345"),
        case("USD", dec!(-12.345), "-$12.345"),

        case("RUB", dec!(12.345), "12.345₽"),
        case("RUB", dec!(-12.345), "-12.345₽"),

        case("UNKNOWN", dec!(12.345), "12.345 UNKNOWN"),
        case("UNKNOWN", dec!(-12.345), "-12.345 UNKNOWN"),
    )]
    fn formatting(currency: &str, amount: Decimal, expected: &str) {
        assert_eq!(Cash::new(currency, amount).to_string(), expected);
    }
}