Created
April 15, 2021 20:01
-
-
Save 2garryn/b8623b38a87364537816dfac511cc6fd to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use std::ops; | |
use std::fmt; | |
use rust_decimal::prelude::*; | |
use std::cmp::Ordering; | |
use rust_decimal; | |
use crate::currencies::Currency as Currency; | |
use crate::percent::Percent as Percent; | |
use crate::currency_rate::CurrencyRate as CurrencyRate; | |
use crate::rounding::RoundStrategy as RoundStrategy; | |
#[derive(Clone)] | |
pub enum ErrKind { | |
ParseError, | |
CurrencyNotFound, | |
DivideByZero, | |
DiffirentCurrenciesOp, | |
DecimalOverflow, | |
RuleViolation | |
} | |
#[derive(Clone)] | |
pub struct MoneyError { | |
pub kind: ErrKind, | |
pub err_message: String | |
} | |
impl fmt::Debug for MoneyError { | |
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |
write!(f, "{}", self.err_message) | |
} | |
} | |
pub type Rule = fn(a: &Amount) -> Result<(), String>; | |
#[derive(Clone)] | |
pub struct Amount { | |
value: Decimal, | |
currency: Currency | |
} | |
impl Amount { | |
pub fn from_str(value: &str, c: Currency) -> Result<Amount, MoneyError> { | |
match Decimal::from_str(value) { | |
Ok(dec) => Ok(Amount::new_c(dec, c)), | |
_ => Err(ret_err(ErrKind::ParseError, format!("Can not parse {}", value))) | |
} | |
} | |
pub fn from_str_c(value: &str, c: &str) -> Result<Amount, MoneyError> { | |
Currency::from_str(c).map_or_else( | |
|| Err(ret_err(ErrKind::CurrencyNotFound, format!("Invalid currency {}", c))), | |
|cur| Ok(Amount::from_str(value, cur)?) | |
) | |
} | |
pub fn zero(c: Currency) -> Amount { | |
Amount::from_str("0", c).ok().unwrap() | |
} | |
pub fn zero_c(c: &str) -> Result<Amount, MoneyError> { | |
Amount::from_str_c("0", c) | |
} | |
pub fn validated_str(&self, rules: Vec<Rule>) -> Result<String, MoneyError> { | |
Ok(self.validated_decimal(rules)?.to_string()) | |
} | |
pub fn validated_decimal(&self, rules: Vec<Rule>) -> Result<Decimal, MoneyError> { | |
for rule in rules { | |
match rule(self) { | |
Err(msg) => return Err(ret_err(ErrKind::RuleViolation, | |
format!("Rule violation: {}", msg))), | |
Ok(()) => () | |
} | |
}; | |
Ok(self.value) | |
} | |
pub fn currency(&self) -> Currency { | |
self.currency.clone() | |
} | |
pub fn percent(&self, p: Percent) -> Amount { | |
self.new(p.raw_value() * self.value) | |
} | |
pub fn divide(&self, a: Amount) -> Result<Percent, MoneyError> { | |
self.check_currency(&a)?; | |
if a.value.is_zero() { | |
return Err(ret_err(ErrKind::DivideByZero, "Divide by zero".to_string())) | |
} | |
Ok(Percent::new(self.value / a.value)) | |
} | |
pub fn multiply(&self, p: Percent) -> Amount { | |
self.new(self.value * p.raw_value()) | |
} | |
pub fn convert(&self, cr: CurrencyRate, p: Percent) -> Result<Amount, MoneyError> { | |
if cr.source_currency != self.currency { | |
return Err(ret_err(ErrKind::DiffirentCurrenciesOp, | |
"Currency and source currency must be the same".to_string())) | |
} | |
let d = self.value / cr.source * cr.dest; | |
let d0 = d - (d * p.raw_value()); | |
Ok(Amount::new_c(d0, cr.dest_currency)) | |
} | |
pub fn round(&self, rs: RoundStrategy) -> Amount { | |
let r = match rs { | |
RoundStrategy::Bankers => RoundingStrategy::BankersRounding, | |
RoundStrategy::Down => RoundingStrategy::RoundDown, | |
RoundStrategy::Up => RoundingStrategy::RoundUp, | |
RoundStrategy::HalfDown => RoundingStrategy::RoundHalfDown, | |
RoundStrategy::HalfUp => RoundingStrategy::RoundHalfUp | |
}; | |
self.new(self.value.round_dp_with_strategy(self.currency().decimal_pos(), r)) | |
} | |
fn new(&self, d: Decimal) -> Amount { | |
Amount::new_c(d, self.currency.clone()) | |
} | |
fn new_c(d: Decimal, c: Currency) -> Amount { | |
Amount {currency: c, value: d} | |
} | |
fn check_currency(&self, am: &Amount) -> Result<(), MoneyError>{ | |
if self.currency != am.currency { | |
Err(ret_err(ErrKind::DiffirentCurrenciesOp, | |
format!("Try to operation different currencies"))) | |
} else { | |
Ok(()) | |
} | |
} | |
} | |
fn ret_err(e: ErrKind, err_msg: String) -> MoneyError { | |
MoneyError{ | |
kind: e, | |
err_message: err_msg | |
} | |
} | |
impl fmt::Display for Amount { | |
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { | |
fmt.write_str(&self.value.to_string())?; | |
fmt.write_str(" ")?; | |
fmt.write_str(&self.currency.to_string())?; | |
Ok(()) | |
} | |
} | |
impl ops::Add for Amount { | |
type Output = Result<Amount, MoneyError>; | |
fn add(self, am: Amount) -> Result<Amount, MoneyError> { | |
self.check_currency(&am)?; | |
match self.value.checked_add(am.value) { | |
Some(d) => Ok(self.new(d)), | |
None => Err(ret_err(ErrKind::DecimalOverflow, "Overflow".to_string())) | |
} | |
} | |
} | |
impl ops::Sub for Amount { | |
type Output = Result<Amount, MoneyError>; | |
fn sub(self, am: Amount) -> Result<Amount, MoneyError> { | |
match self.value.checked_add(am.value) { | |
Some(d) => Ok(self.new(d)), | |
None => Err(ret_err(ErrKind::DecimalOverflow, "Overflow".to_string())) | |
} | |
} | |
} | |
impl PartialEq for Amount { | |
fn eq(&self, other: &Self) -> bool { | |
match self.check_currency(other) { | |
Ok(()) => self.value == other.value, | |
Err(err) => panic!("{}", err.err_message) | |
} | |
} | |
} | |
impl Eq for Amount {} | |
impl Ord for Amount { | |
fn cmp(&self, other: &Self) -> Ordering { | |
match self.check_currency(other) { | |
Ok(()) => (self.value).cmp(&(other.value)), | |
Err(err) => panic!("{}", err.err_message) | |
} | |
} | |
} | |
impl PartialOrd for Amount { | |
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { | |
Some(self.cmp(other)) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment