Skip to content

Instantly share code, notes, and snippets.

@2garryn
Created April 15, 2021 20:01
Show Gist options
  • Save 2garryn/b8623b38a87364537816dfac511cc6fd to your computer and use it in GitHub Desktop.
Save 2garryn/b8623b38a87364537816dfac511cc6fd to your computer and use it in GitHub Desktop.
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