use serde::{Deserialize, Serialize};
use std::ops::{Add, Sub};
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Serialize,
Deserialize,
PartialOrd,
Ord,
Default,
Hash,
rkyv::Archive,
rkyv::Serialize,
rkyv::Deserialize,
)]
pub struct Money(pub i64);
impl Money {
pub const MONEY_SCALE_PER_USD: i64 = 10_000;
pub const MONEY_SCALE_PER_CENT: i64 = Self::MONEY_SCALE_PER_USD / 100;
pub fn zero() -> Self {
Money(0)
}
pub fn is_positive(self) -> bool {
self.0 > 0
}
pub fn saturating_sub(self, other: Money) -> Money {
Money(self.0.saturating_sub(other.0))
}
pub fn saturating_add(self, other: Money) -> Money {
Money(self.0.saturating_add(other.0))
}
pub fn clamp_non_negative(self) -> Money {
Money(self.0.max(0))
}
pub fn from_cents(cents: i64) -> Self {
Money(cents.saturating_mul(Self::MONEY_SCALE_PER_CENT))
}
pub fn to_cents_trunc(self) -> i64 {
self.0 / Self::MONEY_SCALE_PER_CENT
}
}
impl Add for Money {
type Output = Money;
fn add(self, rhs: Money) -> Money {
Money(self.0.saturating_add(rhs.0))
}
}
impl Sub for Money {
type Output = Money;
fn sub(self, rhs: Money) -> Money {
Money(self.0.saturating_sub(rhs.0))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_saturates_at_max() {
let max = Money(i64::MAX);
let one = Money(1);
assert_eq!(max + one, Money(i64::MAX));
assert_eq!(max + max, Money(i64::MAX));
}
#[test]
fn sub_saturates_at_min() {
let min = Money(i64::MIN);
let one = Money(1);
assert_eq!(min - one, Money(i64::MIN));
assert_eq!(min - Money(i64::MAX), Money(i64::MIN));
}
#[test]
fn add_normal_case() {
assert_eq!(Money(100) + Money(200), Money(300));
assert_eq!(Money(-50) + Money(100), Money(50));
}
#[test]
fn sub_normal_case() {
assert_eq!(Money(300) - Money(100), Money(200));
assert_eq!(Money(50) - Money(100), Money(-50));
}
#[test]
fn saturating_add_method_matches_trait() {
let a = Money(i64::MAX - 10);
let b = Money(100);
assert_eq!(a.saturating_add(b), a + b);
}
#[test]
fn saturating_sub_method_matches_trait() {
let a = Money(i64::MIN + 10);
let b = Money(100);
assert_eq!(a.saturating_sub(b), a - b);
}
}