use core::cmp::Ordering;
use core::fmt;
use core::fmt::Display;
use core::fmt::Formatter;
use core::hash::Hash;
use core::hash::Hasher;
use core::ops::Add;
use core::ops::AddAssign;
use core::ops::Div;
use core::ops::DivAssign;
use core::ops::Mul;
use core::ops::MulAssign;
use core::ops::Neg;
use core::ops::Sub;
use core::ops::SubAssign;
use core::str::FromStr;
use beancount_commodity::Commodity;
use miette::Diagnostic;
use rust_decimal::Decimal;
use snafu::Backtrace;
use snafu::OptionExt as _;
use snafu::Snafu;
macro_rules! forward_commutative_binop {
(impl $trait:ident<$ltype:ty> for $rtype:ty { $fn:ident }) => {
forward_commutative_binop!(@single<'l> $trait, $fn, &'l $ltype, &$rtype);
forward_commutative_binop!(@single<'l> $trait, $fn, &'l $ltype, $rtype);
forward_commutative_binop!(@single $trait, $fn, $ltype, &$rtype);
forward_commutative_binop!(@single $trait, $fn, $ltype, $rtype);
};
(@single$(<$lt:lifetime>)? $trait:ident, $fn:ident, $ltype:ty, $rtype:ty) => {
impl$(<$lt>)? $trait<$ltype> for $rtype {
type Output = <$ltype as $trait<Self>>::Output;
#[inline]
fn $fn(self, rhs: $ltype) -> Self::Output {
rhs.$fn(self)
}
}
};
}
macro_rules! implement_binop {
(impl $trait:ident<$rtype:ty> for $ltype:ty { fn $method:ident($self:ident, $rhs:ident) $body:block }) => {
implement_binop! {@single
impl $trait<&'_ $rtype> for &'_ $ltype {
fn $method($self, $rhs) -> $ltype {
(*$self).$method($rhs)
}
}
}
implement_binop! {@single
impl $trait<&'_ $rtype> for $ltype {
fn $method($self, $rhs) -> $ltype $body
}
}
implement_binop! {@single
impl $trait<$rtype> for &'_ $ltype {
fn $method($self, $rhs) -> $ltype {
$self.$method(&$rhs)
}
}
}
implement_binop! {@single
impl $trait<$rtype> for $ltype {
fn $method($self, $rhs) -> $ltype {
$self.$method(&$rhs)
}
}
}
};
(@single impl $trait:ident<$rtype:ty> for $ltype:ty { fn $method:ident($self:ident, $rhs:ident) -> $output:ty $body:block }) => {
impl $trait<$rtype> for $ltype {
type Output = $output;
#[inline]
fn $method($self, $rhs: $rtype) -> Self::Output $body
}
};
}
macro_rules! implement_binop_assign {
(impl $trait:ident<$rtype:ty> for $ltype:ty { fn $method:ident($self:ident, $rhs:ident) $body:block }) => {
implement_binop_assign! {@single
impl $trait<&'_ $rtype> for $ltype {
fn $method($self, $rhs) $body
}
}
implement_binop_assign!(@single impl $trait<$rtype> for $ltype { fn $method($self, $rhs) { $self.$method(&$rhs)} });
};
(@single impl $trait:ident<$rtype:ty> for $ltype:ty { fn $method:ident($self:ident, $rhs:ident) $body:block }) => {
impl $trait<$rtype> for $ltype {
#[inline]
fn $method(&mut $self, $rhs: $rtype) $body
}
};
}
macro_rules! implement_binop_complete {
(@com impl $assign_trait:ident<$rtype:ty> for $ltype:ty { fn $assign_method:ident($self:ident, $rhs:ident) $body:block } .. $op_trait:ident { $op_method:ident }) => {
forward_commutative_binop! {
impl $op_trait<$ltype> for $rtype { $op_method }
}
implement_binop_complete!(impl $assign_trait<$rtype> for $ltype { fn $assign_method($self, $rhs) $body } .. $op_trait { $op_method });
};
(impl $assign_trait:ident<$rtype:ty> for $ltype:ty { fn $assign_method:ident($self:ident, $rhs:ident) $body:block } .. $op_trait:ident { $op_method:ident }) => {
implement_binop! {
impl $op_trait<$rtype> for $ltype {
fn $op_method($self, $rhs) {
let mut this = $self;
this.$assign_method($rhs);
this
}
}
}
implement_binop_assign! {
impl $assign_trait<$rtype> for $ltype {
fn $assign_method($self, $rhs) $body
}
}
};
}
#[derive(Clone, Copy, Debug, Eq)]
pub struct Amount {
pub amount: Decimal,
pub commodity: Commodity,
}
impl Amount {
#[must_use]
pub const fn new(amount: Decimal, commodity: Commodity) -> Self {
Self { amount, commodity }
}
}
impl Amount {
#[inline]
pub fn checked_add(self, rhs: Self) -> Option<Self> {
self.try_reduce(rhs, Decimal::checked_add)
}
#[inline]
pub fn checked_div(self, rhs: Self) -> Option<Self> {
self.try_reduce(rhs, Decimal::checked_div)
}
#[inline]
pub fn checked_mul(self, rhs: Self) -> Option<Self> {
self.try_reduce(rhs, Decimal::checked_mul)
}
#[inline]
pub fn checked_sub(self, rhs: Self) -> Option<Self> {
self.try_reduce(rhs, Decimal::checked_sub)
}
#[inline]
pub fn saturating_add(self, rhs: Self) -> Option<Self> {
self.reduce(rhs, Decimal::saturating_add)
}
#[inline]
pub fn saturating_mul(self, rhs: Self) -> Option<Self> {
self.reduce(rhs, Decimal::saturating_mul)
}
#[inline]
pub fn saturating_sub(self, rhs: Self) -> Option<Self> {
self.reduce(rhs, Decimal::saturating_sub)
}
}
impl Amount {
#[inline]
fn reduce(self, rhs: Self, f: impl FnOnce(Decimal, Decimal) -> Decimal) -> Option<Self> {
self.try_reduce(rhs, |lhs, rhs| Some(f(lhs, rhs)))
}
#[inline]
fn try_reduce(
self,
rhs: Self,
f: impl FnOnce(Decimal, Decimal) -> Option<Decimal>,
) -> Option<Self> {
(self.commodity == rhs.commodity)
.then_some((self.amount, rhs.amount))
.and_then(|(lhs, rhs)| f(lhs, rhs))
.map(|amount| Self { amount, ..self })
}
}
implement_binop_complete! {
impl AddAssign<Amount> for Amount {
fn add_assign(self, rhs) {
assert!(self.commodity == rhs.commodity);
self.amount.add_assign(rhs.amount);
}
}
.. Add { add }
}
impl Display for Amount {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let Self { amount, commodity } = self;
write!(f, "{amount} {commodity}")
}
}
implement_binop_complete! {
impl DivAssign<Decimal> for Amount {
fn div_assign(self, rhs) {
self.amount.div_assign(rhs);
}
}
.. Div { div }
}
impl FromStr for Amount {
type Err = <Self as TryFrom<&'static str>>::Error;
fn from_str(amount: &str) -> Result<Self, Self::Err> {
Self::try_from(amount)
}
}
impl Hash for Amount {
fn hash<H: Hasher>(&self, state: &mut H) {
self.commodity.hash(state);
self.amount.hash(state);
}
}
implement_binop_complete! {
@com
impl MulAssign<Decimal> for Amount {
fn mul_assign(self, rhs) {
self.amount.mul_assign(rhs);
}
}
.. Mul { mul }
}
impl Neg for &Amount {
type Output = Amount;
#[inline]
fn neg(self) -> Self::Output {
(*self).neg()
}
}
impl Neg for Amount {
type Output = Self;
#[inline]
fn neg(self) -> Self::Output {
let amount = self.amount.neg();
Self { amount, ..self }
}
}
impl PartialEq for Amount {
fn eq(&self, other: &Self) -> bool {
self.partial_cmp(other).is_some_and(Ordering::is_eq)
}
}
impl PartialOrd for Amount {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
(self.commodity == other.commodity).then(|| self.amount.cmp(&other.amount))
}
}
implement_binop_complete! {
impl SubAssign<Amount> for Amount {
fn sub_assign(self, rhs) {
assert!(self.commodity == rhs.commodity);
self.amount.sub_assign(rhs.amount);
}
}
.. Sub { sub }
}
impl TryFrom<&str> for Amount {
type Error = AmountError;
fn try_from(amount: &str) -> Result<Self, Self::Error> {
let context = AmountSnafu { value: amount };
let (amount, commodity) = amount.split_once(' ').context(context)?;
let amount = amount.parse().map_err(|_| context.build())?;
let commodity = commodity.parse().map_err(|_| context.build())?;
Ok(Self { amount, commodity })
}
}
#[derive(Debug, Diagnostic, Snafu)]
pub struct AmountError {
value: String,
backtrace: Backtrace,
}