use std::fmt;
use std::ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign};
use std::str::FromStr;
use num_traits::{ToPrimitive, Zero};
use separator::Separatable;
use crate::core::{GenericResult, EmptyResult};
use crate::time::Date;
use crate::types::Decimal;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Cash {
pub currency: &'static str,
pub amount: Decimal,
}
impl Cash {
pub fn new(currency: &str, amount: Decimal) -> Cash {
Cash {
currency: super::name_cache::get(currency),
amount: amount,
}
}
pub fn zero(currency: &str) -> Cash {
Cash::new(currency, Decimal::zero())
}
pub fn new_from_string(currency: &str, amount: &str) -> GenericResult<Cash> {
Ok(Cash::new(currency, Decimal::from_str(amount).map_err(|_| format!(
"Invalid cash amount: {:?}", amount))?))
}
pub fn is_zero(&self) -> bool {
self.amount.is_zero()
}
pub fn is_positive(&self) -> bool {
!self.amount.is_zero() && self.amount.is_sign_positive()
}
pub fn is_negative(&self) -> bool {
!self.amount.is_zero() && self.amount.is_sign_negative()
}
#[allow(clippy::should_implement_trait)]
pub fn add(mut self, amount: Cash) -> GenericResult<Cash> {
self.add_assign(amount)?;
Ok(self)
}
pub fn add_assign(&mut self, amount: Cash) -> EmptyResult {
self.ensure_same_currency(amount)?;
self.amount += amount.amount;
Ok(())
}
pub fn sub(self, amount: Cash) -> GenericResult<Cash> {
self.add(-amount)
}
pub fn sub_assign(&mut self, amount: Cash) -> EmptyResult {
self.add_assign(-amount)
}
#[allow(clippy::should_implement_trait)]
pub fn div(self, amount: Cash) -> GenericResult<Decimal> {
self.ensure_same_currency(amount)?;
Ok(self.amount / amount.amount)
}
pub fn round(mut self) -> Cash {
self.amount = super::round(self.amount);
self
}
pub fn round_to(mut self, points: u32) -> Cash {
self.amount = super::round_to(self.amount, points);
self
}
pub fn normalize(mut self) -> Cash {
self.amount = self.amount.normalize();
self
}
pub fn format_rounded(&self) -> String {
let amount = super::round_to(self.amount, 0).to_i64().unwrap().separated_string();
super::format_currency(self.currency, &amount)
}
fn ensure_same_currency(self, other: Cash) -> EmptyResult {
if self.currency == other.currency {
Ok(())
} else {
Err!("Currency mismatch: {} and {}", self.currency, other.currency)
}
}
}
impl Neg for Cash {
type Output = Cash;
fn neg(mut self) -> Cash {
self.amount = -self.amount;
self
}
}
impl Add for Cash {
type Output = Cash;
fn add(self, rhs: Cash) -> Cash {
self.add(rhs).unwrap()
}
}
impl AddAssign for Cash {
fn add_assign(&mut self, rhs: Cash) {
self.add_assign(rhs).unwrap()
}
}
impl Sub for Cash {
type Output = Cash;
fn sub(self, rhs: Cash) -> Cash {
self.sub(rhs).unwrap()
}
}
impl SubAssign for Cash {
fn sub_assign(&mut self, rhs: Cash) {
self.sub_assign(rhs).unwrap()
}
}
impl<T> Mul<T> for Cash where T: Into<Decimal> {
type Output = Cash;
fn mul(mut self, rhs: T) -> Cash {
self.amount *= rhs.into();
self
}
}
impl Div for Cash {
type Output = Decimal;
fn div(self, rhs: Cash) -> Decimal {
self.div(rhs).unwrap()
}
}
impl<T> Div<T> for Cash where T: Into<Decimal> {
type Output = Cash;
fn div(mut self, rhs: T) -> Cash {
self.amount /= rhs.into();
self
}
}
impl fmt::Display for Cash {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut amount = self.amount.normalize();
if amount.scale() == 1 {
amount.set_scale(0).unwrap();
amount = Decimal::new(amount.to_i64().unwrap() * 10, 2)
}
write!(f, "{}", super::format_currency(self.currency, &separated_float!(amount.to_string())))
}
}
#[derive(Clone, Copy)]
pub struct CashAssets {
pub date: Date,
pub cash: Cash,
}
impl CashAssets {
pub fn new(date: Date, currency: &str, amount: Decimal) -> CashAssets {
CashAssets::new_from_cash(date, Cash::new(currency, amount))
}
pub fn new_from_cash(date: Date, cash: Cash) -> CashAssets {
CashAssets {date, cash}
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest(input, expected,
case("12", "12"),
case("12.3", "12.30"),
case("12.30", "12.30"),
case("12.34", "12.34"),
case("12.345", "12.345"),
case("12.001", "12.001"),
)]
fn formatting(input: &str, expected: &str) {
let currency = "CURRENCY";
for sign in &["", "-"] {
let input = Cash::new(currency, Decimal::from_str(&format!("{}{}", sign, input)).unwrap());
let expected = format!("{}{} {}", sign, expected, currency);
assert_eq!(input.to_string(), expected);
}
}
}