use serde::{Deserialize, Serialize};
use std::fmt;
use std::ops::{Add, AddAssign, Neg, Sub, SubAssign};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Money(i64);
impl Money {
pub const fn from_cents(cents: i64) -> Self {
Self(cents)
}
pub const fn from_dollars_cents(dollars: i64, cents: i64) -> Self {
Self(dollars * 100 + cents)
}
pub const fn zero() -> Self {
Self(0)
}
pub const fn cents(&self) -> i64 {
self.0
}
pub const fn dollars(&self) -> i64 {
self.0 / 100
}
pub const fn cents_part(&self) -> i64 {
(self.0 % 100).abs()
}
pub const fn is_zero(&self) -> bool {
self.0 == 0
}
pub const fn is_positive(&self) -> bool {
self.0 > 0
}
pub const fn is_negative(&self) -> bool {
self.0 < 0
}
pub const fn abs(&self) -> Self {
Self(self.0.abs())
}
pub fn parse(s: &str) -> Result<Self, MoneyParseError> {
let s = s.trim();
let (negative, s) = if let Some(stripped) = s.strip_prefix('-') {
(true, stripped)
} else {
(false, s)
};
let s = s.strip_prefix('$').unwrap_or(s);
let cents = if s.contains('.') {
let parts: Vec<&str> = s.split('.').collect();
if parts.len() != 2 {
return Err(MoneyParseError::InvalidFormat(s.to_string()));
}
let dollars: i64 = parts[0]
.parse()
.map_err(|_| MoneyParseError::InvalidFormat(s.to_string()))?;
let cents_str = parts[1];
let cents: i64 = match cents_str.len() {
0 => 0,
1 => {
cents_str
.parse::<i64>()
.map_err(|_| MoneyParseError::InvalidFormat(s.to_string()))?
* 10
}
_ => cents_str[..2]
.parse()
.map_err(|_| MoneyParseError::InvalidFormat(s.to_string()))?,
};
dollars * 100 + cents
} else {
s.parse::<i64>()
.map_err(|_| MoneyParseError::InvalidFormat(s.to_string()))?
* 100
};
Ok(Self(if negative { -cents } else { cents }))
}
pub fn format_with_symbol(&self, symbol: &str) -> String {
if self.is_negative() {
format!(
"-{}{}.{:02}",
symbol,
self.dollars().abs(),
self.cents_part()
)
} else {
format!("{}{}.{:02}", symbol, self.dollars(), self.cents_part())
}
}
}
impl Default for Money {
fn default() -> Self {
Self::zero()
}
}
impl fmt::Display for Money {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let formatted = if self.is_negative() {
format!("-${}.{:02}", self.dollars().abs(), self.cents_part())
} else {
format!("${}.{:02}", self.dollars(), self.cents_part())
};
if let Some(width) = f.width() {
if f.align() == Some(fmt::Alignment::Left) {
write!(f, "{:<width$}", formatted, width = width)
} else {
write!(f, "{:>width$}", formatted, width = width)
}
} else {
write!(f, "{}", formatted)
}
}
}
impl Add for Money {
type Output = Self;
fn add(self, other: Self) -> Self {
Self(self.0 + other.0)
}
}
impl AddAssign for Money {
fn add_assign(&mut self, other: Self) {
self.0 += other.0;
}
}
impl Sub for Money {
type Output = Self;
fn sub(self, other: Self) -> Self {
Self(self.0 - other.0)
}
}
impl SubAssign for Money {
fn sub_assign(&mut self, other: Self) {
self.0 -= other.0;
}
}
impl Neg for Money {
type Output = Self;
fn neg(self) -> Self {
Self(-self.0)
}
}
impl std::iter::Sum for Money {
fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
iter.fold(Money::zero(), |acc, m| acc + m)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MoneyParseError {
InvalidFormat(String),
}
impl fmt::Display for MoneyParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MoneyParseError::InvalidFormat(s) => write!(f, "Invalid money format: {}", s),
}
}
}
impl std::error::Error for MoneyParseError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_cents() {
let m = Money::from_cents(1050);
assert_eq!(m.cents(), 1050);
assert_eq!(m.dollars(), 10);
assert_eq!(m.cents_part(), 50);
}
#[test]
fn test_from_dollars_cents() {
let m = Money::from_dollars_cents(10, 50);
assert_eq!(m.cents(), 1050);
}
#[test]
fn test_display() {
assert_eq!(format!("{}", Money::from_cents(1050)), "$10.50");
assert_eq!(format!("{}", Money::from_cents(0)), "$0.00");
assert_eq!(format!("{}", Money::from_cents(-1050)), "-$10.50");
assert_eq!(format!("{}", Money::from_cents(5)), "$0.05");
}
#[test]
fn test_arithmetic() {
let a = Money::from_cents(1000);
let b = Money::from_cents(500);
assert_eq!((a + b).cents(), 1500);
assert_eq!((a - b).cents(), 500);
assert_eq!((-a).cents(), -1000);
}
#[test]
fn test_parse() {
assert_eq!(Money::parse("10.50").unwrap().cents(), 1050);
assert_eq!(Money::parse("$10.50").unwrap().cents(), 1050);
assert_eq!(Money::parse("-10.50").unwrap().cents(), -1050);
assert_eq!(Money::parse("10").unwrap().cents(), 1000);
assert_eq!(Money::parse("10.5").unwrap().cents(), 1050);
assert_eq!(Money::parse("0.05").unwrap().cents(), 5);
}
#[test]
fn test_comparison() {
let a = Money::from_cents(1000);
let b = Money::from_cents(500);
let c = Money::from_cents(1000);
assert!(a > b);
assert!(b < a);
assert_eq!(a, c);
}
#[test]
fn test_is_checks() {
assert!(Money::zero().is_zero());
assert!(Money::from_cents(100).is_positive());
assert!(Money::from_cents(-100).is_negative());
}
#[test]
fn test_sum() {
let amounts = vec![
Money::from_cents(100),
Money::from_cents(200),
Money::from_cents(300),
];
let total: Money = amounts.into_iter().sum();
assert_eq!(total.cents(), 600);
}
#[test]
fn test_serialization() {
let m = Money::from_cents(1050);
let json = serde_json::to_string(&m).unwrap();
assert_eq!(json, "1050");
let deserialized: Money = serde_json::from_str(&json).unwrap();
assert_eq!(m, deserialized);
}
}