moneta 0.3.2

Currency-safe computations with money amounts.
Documentation
// ---------------------------------------------------------------------------
// Copyright:   (c) 2022 ff. Michael Amrhein (michael@adrhinum.de)
// License:     This program is part of a larger application. For license
//              details please read the file LICENSE.TXT provided together
//              with the application.
// ---------------------------------------------------------------------------
// $Source: src/exchange.rs $
// $Revision: 2022-05-29T18:05:34+02:00 $

use core::{
    cmp::min,
    ops::{Div, Mul},
};

use fpdec::{Decimal, DivRounded};
use fpdec_core::ten_pow;

use crate::{AmountT, Currency, Money, Quantity};

/// Basic representation of a conversion factor between two currencies.
///
/// An instance of `ExchangeRate` can be constructed by `fn new`, using the
/// following arguments:
/// * unit_currency: currency to be converted from, aka base currency
/// * unit_multiple: amount of base currency
/// * term_currency: currency to be converted to, aka price currency
/// * term_amount: equivalent amount of term currency
///
/// `unit_multiple` and `term_amount` will always be adjusted so that the
/// resulting unit multiple is a power to 10 and the resulting term amounts
/// magnitude is >= -1. The latter will always be rounded to 6 decimal
/// fractional digits.
///
/// Example:
///
/// ```rust
/// # use moneta::{ExchangeRate, EUR, USD, Dec, Decimal};
/// // 1 USD ≣ 0.9683 EUR =>
/// let usd_2_eur = ExchangeRate::new(USD, 1, EUR, Dec!(0.9683));
/// ```
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct ExchangeRate {
    unit_currency: Currency,
    unit_multiple: u32,
    term_currency: Currency,
    term_amount: AmountT,
}

impl ExchangeRate {
    /// Returns a new instance of `ExchangeRate`.
    ///
    /// ### Panics
    /// The function panics in the following cases:
    /// * `unit_currency` is equal to `term_currency`.
    /// * `unit_multiple` == 0
    /// * `term_amount` <= 0
    /// * adjusted unit multiple > 1_000_000_000
    #[must_use]
    pub fn new(
        unit_currency: Currency,
        unit_multiple: u32,
        term_currency: Currency,
        term_amount: AmountT,
    ) -> Self {
        assert!(
            !(unit_currency == term_currency),
            "The currencies given must not be identical."
        );
        assert!(unit_multiple != 0, "Unit multiple must be >= 1.");
        assert!(term_amount.is_positive(), "Term amount must be > 0.");
        // adjust unit_multiple and term_amount so that unit_multiple is a power
        // to 10 and term_amount.magnitude >= -1
        let magn = Decimal::from(unit_multiple).magnitude()
            - min(0, term_amount.magnitude() + 1);
        assert!(
            (0..=9).contains(&magn),
            "Adjusted unit multiple must be <= 1_000_000_000."
        );
        #[allow(clippy::cast_possible_truncation)]
        #[allow(clippy::cast_sign_loss)]
        Self {
            unit_currency,
            unit_multiple: ten_pow(magn as u8) as u32,
            term_currency,
            term_amount: (term_amount * ten_pow(magn as u8))
                .div_rounded(unit_multiple, 6),
        }
    }

    /// Currency to be converted from, aka base currency
    #[inline(always)]
    #[must_use]
    pub const fn unit_currency(&self) -> Currency {
        self.unit_currency
    }

    /// Amount of base currency
    #[inline(always)]
    #[must_use]
    pub const fn unit_multiple(&self) -> u32 {
        self.unit_multiple
    }

    /// Currency to be converted to, aka price currency
    #[inline(always)]
    #[must_use]
    pub const fn term_currency(&self) -> Currency {
        self.term_currency
    }

    /// Equivalent amount of term currency
    #[inline(always)]
    pub const fn term_amount(&self) -> AmountT {
        self.term_amount
    }

    /// Relative value of term currency to unit currency
    #[inline(always)]
    pub fn rate(&self) -> AmountT {
        self.term_amount() / self.unit_multiple()
    }

    /// Inverted rate, i.e. relative value of unit currency to term currency
    #[inline(always)]
    pub fn inverse_rate(&self) -> AmountT {
        self.unit_multiple() / self.term_amount()
    }

    /// Returns the tuple of unit currency, term currency and rate.
    #[inline(always)]
    pub fn quotation(&self) -> (Currency, Currency, AmountT) {
        (self.unit_currency(), self.term_currency(), self.rate())
    }

    /// Returns the tuple of term currency, unit currency and inverse rate.
    #[inline(always)]
    pub fn inverse_quotation(&self) -> (Currency, Currency, AmountT) {
        (
            self.term_currency(),
            self.unit_currency(),
            self.inverse_rate(),
        )
    }

    /// Returns the inversion of `self`.
    #[must_use]
    pub fn inverted(&self) -> Self {
        Self::new(
            self.term_currency,
            1,
            self.unit_currency,
            self.inverse_rate(),
        )
    }
}

impl Mul<Money> for ExchangeRate {
    type Output = Money;

    /// Returns the equivalent of `rhs` in term currency.
    ///
    /// ### Panics
    /// The function panics in the following cases:
    /// * Currency of `rhs` is not equal to `self.unit_currency`.
    fn mul(self, rhs: Money) -> Self::Output {
        assert!(
            !(self.unit_currency() != rhs.unit()),
            "Can't divide '{}' by '{}'",
            rhs.unit(),
            self.unit_currency()
        );
        Self::Output::new(rhs.amount() * self.rate(), self.term_currency)
    }
}

impl Mul<ExchangeRate> for Money {
    type Output = Self;

    /// Returns the equivalent of `self` in term currency.
    ///
    /// ### Panics
    /// The function panics in the following cases:
    /// * Currency of `self` is not equal to `rhs.unit_currency`.
    fn mul(self, rhs: ExchangeRate) -> Self::Output {
        assert!(
            !(self.unit() != rhs.unit_currency()),
            "Can't divide '{}' by '{}'",
            self.unit(),
            rhs.unit_currency()
        );
        Self::Output::new(self.amount() * rhs.rate(), rhs.term_currency)
    }
}

impl Div<ExchangeRate> for Money {
    type Output = Self;

    /// Returns the equivalent of `rhs` in unit currency.
    ///
    /// ### Panics
    /// The function panics in the following cases:
    /// * Currency of `self` is not equal to `rhs.term_currency`.
    fn div(self, rhs: ExchangeRate) -> Self::Output {
        assert!(
            !(self.unit() != rhs.term_currency()),
            "Can't divide '{}' by '{}'",
            self.unit(),
            rhs.term_currency()
        );
        Self::Output::new(self.amount() / rhs.rate(), rhs.unit_currency)
    }
}

impl Mul<Self> for ExchangeRate {
    type Output = Self;

    /// Returns the "triangulated" exchange rate.
    ///
    /// * self.unit_currency == rhs.term_currency => self.term_currency /
    ///   rhs.unit_currency
    /// * self.term_currency == rhs.unit_currency => rhs.term_currency /
    ///   self.unit_currency
    ///
    /// ### Panics
    /// The function panics in the following cases:
    /// * No unit currency of a multiplicant does equal the term currency of the
    ///   other multiplicant.
    /// * The unit currency of both multiplicants equals the term currency of
    ///   the other multiplicant.
    fn mul(self, rhs: Self) -> Self::Output {
        if self.unit_currency() == rhs.term_currency() {
            Self::new(
                rhs.unit_currency(),
                1,
                self.term_currency(),
                self.rate() * rhs.rate(),
            )
        } else if self.term_currency() == rhs.unit_currency() {
            Self::new(
                self.unit_currency(),
                1,
                rhs.term_currency(),
                self.rate() * rhs.rate(),
            )
        } else {
            panic!(
                "Can't multiply '{}/{}' and '{}/{}'.",
                self.term_currency(),
                self.unit_currency(),
                rhs.term_currency(),
                rhs.unit_currency(),
            );
        }
    }
}

impl Div<Self> for ExchangeRate {
    type Output = Self;

    /// Returns the "triangulated" exchange rate.
    ///
    /// * self.unit_currency == rhs.unit_currency => self.term_currency /
    ///   rhs.term_currency
    /// * self.term_currency == rhs.term_currency => rhs.unit_currency /
    ///   self.unit_currency
    ///
    /// ### Panics
    /// The function panics in the following cases:
    /// * unit currency of dividend != unit currency of divisor and term
    ///   currency of dividend != term currency of divisor
    /// * unit currency of dividend == unit currency of divisor and term
    ///   currency of dividend == term currency of divisor
    fn div(self, rhs: Self) -> Self::Output {
        if self.unit_currency() == rhs.unit_currency() {
            Self::new(
                self.term_currency(),
                1,
                rhs.term_currency(),
                self.rate() / rhs.rate(),
            )
        } else if self.term_currency() == rhs.term_currency() {
            Self::new(
                rhs.unit_currency(),
                1,
                self.unit_currency(),
                self.rate() / rhs.rate(),
            )
        } else {
            panic!(
                "Can't divide '{}/{}' by '{}/{}'.",
                self.term_currency(),
                self.unit_currency(),
                rhs.term_currency(),
                rhs.unit_currency(),
            );
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{Amnt, Dec, Decimal, EUR, USD};

    #[test]
    fn test_exchange_rate() {
        let rate = ExchangeRate::new(USD, 5, EUR, Amnt!(4.8307));
        assert_eq!(rate.unit_currency(), Currency::USD);
        assert_eq!(rate.term_currency(), Currency::EUR);
        assert_eq!(rate.unit_multiple(), 1);
        assert_eq!(rate.term_amount(), Amnt!(0.96614));
        assert_eq!(rate.rate(), Amnt!(0.96614));
        assert_eq!(rate.inverse_rate(), Amnt!(1.035046680605295299));
        assert_eq!(rate.quotation(), (USD, EUR, Amnt!(0.96614)));
        assert_eq!(
            rate.inverse_quotation(),
            (EUR, USD, Amnt!(1.035046680605295299))
        );
        assert_eq!(
            rate.inverted(),
            ExchangeRate::new(EUR, 1, USD, Amnt!(1.035047))
        );
    }

    #[test]
    #[should_panic]
    fn test_exchange_rate_same_curr() {
        let _r = ExchangeRate::new(USD, 1, USD, Amnt!(1));
    }

    #[test]
    #[should_panic]
    fn test_exchange_rate_zero_multiple() {
        let _r = ExchangeRate::new(USD, 0, EUR, Amnt!(1));
    }

    #[test]
    #[should_panic]
    fn test_exchange_rate_zero_term_amnt() {
        let _r = ExchangeRate::new(USD, 1, EUR, Amnt!(0));
    }
}