envelope_cli/models/
money.rs1use serde::{Deserialize, Serialize};
7use std::fmt;
8use std::ops::{Add, AddAssign, Neg, Sub, SubAssign};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
15#[serde(transparent)]
16pub struct Money(i64);
17
18impl Money {
19 pub const fn from_cents(cents: i64) -> Self {
27 Self(cents)
28 }
29
30 pub const fn from_dollars_cents(dollars: i64, cents: i64) -> Self {
38 Self(dollars * 100 + cents)
39 }
40
41 pub const fn zero() -> Self {
43 Self(0)
44 }
45
46 pub const fn cents(&self) -> i64 {
48 self.0
49 }
50
51 pub const fn dollars(&self) -> i64 {
53 self.0 / 100
54 }
55
56 pub const fn cents_part(&self) -> i64 {
58 (self.0 % 100).abs()
59 }
60
61 pub const fn is_zero(&self) -> bool {
63 self.0 == 0
64 }
65
66 pub const fn is_positive(&self) -> bool {
68 self.0 > 0
69 }
70
71 pub const fn is_negative(&self) -> bool {
73 self.0 < 0
74 }
75
76 pub const fn abs(&self) -> Self {
78 Self(self.0.abs())
79 }
80
81 pub fn parse(s: &str) -> Result<Self, MoneyParseError> {
85 let s = s.trim();
86
87 let (negative, s) = if let Some(stripped) = s.strip_prefix('-') {
89 (true, stripped)
90 } else {
91 (false, s)
92 };
93
94 let s = s.strip_prefix('$').unwrap_or(s);
96
97 let cents = if s.contains('.') {
99 let parts: Vec<&str> = s.split('.').collect();
101 if parts.len() != 2 {
102 return Err(MoneyParseError::InvalidFormat(s.to_string()));
103 }
104
105 let dollars: i64 = parts[0]
106 .parse()
107 .map_err(|_| MoneyParseError::InvalidFormat(s.to_string()))?;
108
109 let cents_str = parts[1];
111 let cents: i64 = match cents_str.len() {
112 0 => 0,
113 1 => {
114 cents_str
115 .parse::<i64>()
116 .map_err(|_| MoneyParseError::InvalidFormat(s.to_string()))?
117 * 10
118 }
119 _ => cents_str[..2]
120 .parse()
121 .map_err(|_| MoneyParseError::InvalidFormat(s.to_string()))?,
122 };
123
124 dollars * 100 + cents
125 } else {
126 s.parse::<i64>()
128 .map_err(|_| MoneyParseError::InvalidFormat(s.to_string()))?
129 * 100
130 };
131
132 Ok(Self(if negative { -cents } else { cents }))
133 }
134
135 pub fn format_with_symbol(&self, symbol: &str) -> String {
137 if self.is_negative() {
138 format!(
139 "-{}{}.{:02}",
140 symbol,
141 self.dollars().abs(),
142 self.cents_part()
143 )
144 } else {
145 format!("{}{}.{:02}", symbol, self.dollars(), self.cents_part())
146 }
147 }
148}
149
150impl Default for Money {
151 fn default() -> Self {
152 Self::zero()
153 }
154}
155
156impl fmt::Display for Money {
157 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158 let formatted = if self.is_negative() {
159 format!("-${}.{:02}", self.dollars().abs(), self.cents_part())
160 } else {
161 format!("${}.{:02}", self.dollars(), self.cents_part())
162 };
163
164 if let Some(width) = f.width() {
166 if f.align() == Some(fmt::Alignment::Left) {
167 write!(f, "{:<width$}", formatted, width = width)
168 } else {
169 write!(f, "{:>width$}", formatted, width = width)
171 }
172 } else {
173 write!(f, "{}", formatted)
174 }
175 }
176}
177
178impl Add for Money {
179 type Output = Self;
180
181 fn add(self, other: Self) -> Self {
182 Self(self.0 + other.0)
183 }
184}
185
186impl AddAssign for Money {
187 fn add_assign(&mut self, other: Self) {
188 self.0 += other.0;
189 }
190}
191
192impl Sub for Money {
193 type Output = Self;
194
195 fn sub(self, other: Self) -> Self {
196 Self(self.0 - other.0)
197 }
198}
199
200impl SubAssign for Money {
201 fn sub_assign(&mut self, other: Self) {
202 self.0 -= other.0;
203 }
204}
205
206impl Neg for Money {
207 type Output = Self;
208
209 fn neg(self) -> Self {
210 Self(-self.0)
211 }
212}
213
214impl std::iter::Sum for Money {
215 fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
216 iter.fold(Money::zero(), |acc, m| acc + m)
217 }
218}
219
220#[derive(Debug, Clone, PartialEq, Eq)]
222pub enum MoneyParseError {
223 InvalidFormat(String),
224}
225
226impl fmt::Display for MoneyParseError {
227 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
228 match self {
229 MoneyParseError::InvalidFormat(s) => write!(f, "Invalid money format: {}", s),
230 }
231 }
232}
233
234impl std::error::Error for MoneyParseError {}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239
240 #[test]
241 fn test_from_cents() {
242 let m = Money::from_cents(1050);
243 assert_eq!(m.cents(), 1050);
244 assert_eq!(m.dollars(), 10);
245 assert_eq!(m.cents_part(), 50);
246 }
247
248 #[test]
249 fn test_from_dollars_cents() {
250 let m = Money::from_dollars_cents(10, 50);
251 assert_eq!(m.cents(), 1050);
252 }
253
254 #[test]
255 fn test_display() {
256 assert_eq!(format!("{}", Money::from_cents(1050)), "$10.50");
257 assert_eq!(format!("{}", Money::from_cents(0)), "$0.00");
258 assert_eq!(format!("{}", Money::from_cents(-1050)), "-$10.50");
259 assert_eq!(format!("{}", Money::from_cents(5)), "$0.05");
260 }
261
262 #[test]
263 fn test_arithmetic() {
264 let a = Money::from_cents(1000);
265 let b = Money::from_cents(500);
266
267 assert_eq!((a + b).cents(), 1500);
268 assert_eq!((a - b).cents(), 500);
269 assert_eq!((-a).cents(), -1000);
270 }
271
272 #[test]
273 fn test_parse() {
274 assert_eq!(Money::parse("10.50").unwrap().cents(), 1050);
275 assert_eq!(Money::parse("$10.50").unwrap().cents(), 1050);
276 assert_eq!(Money::parse("-10.50").unwrap().cents(), -1050);
277 assert_eq!(Money::parse("10").unwrap().cents(), 1000);
278 assert_eq!(Money::parse("10.5").unwrap().cents(), 1050);
279 assert_eq!(Money::parse("0.05").unwrap().cents(), 5);
280 }
281
282 #[test]
283 fn test_comparison() {
284 let a = Money::from_cents(1000);
285 let b = Money::from_cents(500);
286 let c = Money::from_cents(1000);
287
288 assert!(a > b);
289 assert!(b < a);
290 assert_eq!(a, c);
291 }
292
293 #[test]
294 fn test_is_checks() {
295 assert!(Money::zero().is_zero());
296 assert!(Money::from_cents(100).is_positive());
297 assert!(Money::from_cents(-100).is_negative());
298 }
299
300 #[test]
301 fn test_sum() {
302 let amounts = vec![
303 Money::from_cents(100),
304 Money::from_cents(200),
305 Money::from_cents(300),
306 ];
307 let total: Money = amounts.into_iter().sum();
308 assert_eq!(total.cents(), 600);
309 }
310
311 #[test]
312 fn test_serialization() {
313 let m = Money::from_cents(1050);
314 let json = serde_json::to_string(&m).unwrap();
315 assert_eq!(json, "1050");
316
317 let deserialized: Money = serde_json::from_str(&json).unwrap();
318 assert_eq!(m, deserialized);
319 }
320}