1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
#[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$"),
        "CNY" => Some("¥"),
        "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);
    }
}