1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::fmt;
5use std::error::Error;
6
7const MAX_SCALE: u8 = 18;
8
9pub mod prelude {
11 pub use crate::{Amount, AmountError};
12}
13
14#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
16pub struct Amount {
17 minor_units: i128,
18 scale: u8,
19}
20
21impl Amount {
22 pub const fn from_minor_units(minor_units: i128, scale: u8) -> Result<Self, AmountError> {
28 if scale > MAX_SCALE {
29 return Err(AmountError::ScaleTooLarge);
30 }
31
32 Ok(Self { minor_units, scale })
33 }
34
35 pub const fn zero(scale: u8) -> Result<Self, AmountError> {
41 Self::from_minor_units(0, scale)
42 }
43
44 #[must_use]
46 pub const fn minor_units(self) -> i128 {
47 self.minor_units
48 }
49
50 #[must_use]
52 pub const fn scale(self) -> u8 {
53 self.scale
54 }
55
56 #[must_use]
58 pub const fn is_zero(self) -> bool {
59 self.minor_units == 0
60 }
61
62 #[must_use]
64 pub const fn is_positive(self) -> bool {
65 self.minor_units > 0
66 }
67
68 #[must_use]
70 pub const fn is_negative(self) -> bool {
71 self.minor_units < 0
72 }
73
74 pub const fn checked_abs(self) -> Result<Self, AmountError> {
80 match self.minor_units.checked_abs() {
81 Some(minor_units) => Self::from_minor_units(minor_units, self.scale),
82 None => Err(AmountError::Overflow),
83 }
84 }
85
86 pub fn checked_add(self, other: Self) -> Result<Self, AmountError> {
93 self.ensure_same_scale(other)?;
94 let minor_units = self
95 .minor_units
96 .checked_add(other.minor_units)
97 .ok_or(AmountError::Overflow)?;
98 Self::from_minor_units(minor_units, self.scale)
99 }
100
101 pub fn checked_sub(self, other: Self) -> Result<Self, AmountError> {
108 self.ensure_same_scale(other)?;
109 let minor_units = self
110 .minor_units
111 .checked_sub(other.minor_units)
112 .ok_or(AmountError::Overflow)?;
113 Self::from_minor_units(minor_units, self.scale)
114 }
115
116 pub fn checked_rescale(self, new_scale: u8) -> Result<Self, AmountError> {
124 if new_scale > MAX_SCALE {
125 return Err(AmountError::ScaleTooLarge);
126 }
127
128 if new_scale == self.scale {
129 return Ok(self);
130 }
131
132 if new_scale > self.scale {
133 let multiplier = pow10(new_scale - self.scale)?;
134 let minor_units = self
135 .minor_units
136 .checked_mul(multiplier)
137 .ok_or(AmountError::Overflow)?;
138 return Self::from_minor_units(minor_units, new_scale);
139 }
140
141 let divisor = pow10(self.scale - new_scale)?;
142 if self.minor_units % divisor != 0 {
143 return Err(AmountError::PrecisionLoss);
144 }
145
146 Self::from_minor_units(self.minor_units / divisor, new_scale)
147 }
148
149 #[must_use]
151 pub const fn normalize(self) -> Self {
152 let mut minor_units = self.minor_units;
153 let mut scale = self.scale;
154
155 while scale > 0 && minor_units % 10 == 0 {
156 minor_units /= 10;
157 scale -= 1;
158 }
159
160 Self { minor_units, scale }
161 }
162
163 const fn ensure_same_scale(self, other: Self) -> Result<(), AmountError> {
164 if self.scale == other.scale {
165 Ok(())
166 } else {
167 Err(AmountError::ScaleMismatch {
168 left: self.scale,
169 right: other.scale,
170 })
171 }
172 }
173}
174
175impl fmt::Display for Amount {
176 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
177 if self.scale == 0 {
178 return write!(formatter, "{}", self.minor_units);
179 }
180
181 let negative = self.minor_units < 0;
182 let absolute = self.minor_units.unsigned_abs();
183 let divisor = 10_u128.pow(u32::from(self.scale));
184 let whole = absolute / divisor;
185 let fraction = absolute % divisor;
186
187 if negative {
188 write!(formatter, "-")?;
189 }
190
191 write!(
192 formatter,
193 "{}.{:0width$}",
194 whole,
195 fraction,
196 width = usize::from(self.scale)
197 )
198 }
199}
200
201#[derive(Clone, Copy, Debug, Eq, PartialEq)]
203pub enum AmountError {
204 ScaleTooLarge,
206 ScaleMismatch {
208 left: u8,
210 right: u8,
212 },
213 Overflow,
215 PrecisionLoss,
217}
218
219impl fmt::Display for AmountError {
220 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
221 match self {
222 Self::ScaleTooLarge => formatter.write_str("amount scale cannot exceed 18"),
223 Self::ScaleMismatch { left, right } => write!(
224 formatter,
225 "amount scales must match, got {left} and {right}"
226 ),
227 Self::Overflow => formatter.write_str("amount arithmetic overflowed"),
228 Self::PrecisionLoss => formatter.write_str("amount rescale would lose precision"),
229 }
230 }
231}
232
233impl Error for AmountError {}
234
235fn pow10(exponent: u8) -> Result<i128, AmountError> {
236 10_i128
237 .checked_pow(u32::from(exponent))
238 .ok_or(AmountError::Overflow)
239}
240
241#[cfg(test)]
242mod tests {
243 use super::{Amount, AmountError};
244
245 #[test]
246 fn formats_scaled_amounts() -> Result<(), AmountError> {
247 assert_eq!(Amount::from_minor_units(12_345, 2)?.to_string(), "123.45");
248 assert_eq!(Amount::from_minor_units(-5, 2)?.to_string(), "-0.05");
249 assert_eq!(Amount::from_minor_units(42, 0)?.to_string(), "42");
250 Ok(())
251 }
252
253 #[test]
254 fn adds_and_subtracts_same_scale_amounts() -> Result<(), AmountError> {
255 let left = Amount::from_minor_units(10_000, 2)?;
256 let right = Amount::from_minor_units(2_500, 2)?;
257
258 assert_eq!(left.checked_add(right)?.minor_units(), 12_500);
259 assert_eq!(left.checked_sub(right)?.minor_units(), 7_500);
260 Ok(())
261 }
262
263 #[test]
264 fn rejects_mismatched_scales() -> Result<(), AmountError> {
265 let left = Amount::from_minor_units(100, 2)?;
266 let right = Amount::from_minor_units(100, 3)?;
267
268 assert_eq!(
269 left.checked_add(right),
270 Err(AmountError::ScaleMismatch { left: 2, right: 3 })
271 );
272 Ok(())
273 }
274
275 #[test]
276 fn rescales_without_precision_loss() -> Result<(), AmountError> {
277 let amount = Amount::from_minor_units(123, 2)?;
278 assert_eq!(amount.checked_rescale(4)?.minor_units(), 12_300);
279 assert_eq!(
280 Amount::from_minor_units(12_300, 4)?.checked_rescale(2)?,
281 amount
282 );
283 assert_eq!(amount.checked_rescale(1), Err(AmountError::PrecisionLoss));
284 Ok(())
285 }
286
287 #[test]
288 fn normalizes_trailing_zeroes() -> Result<(), AmountError> {
289 assert_eq!(
290 Amount::from_minor_units(12_300, 4)?.normalize(),
291 Amount::from_minor_units(123, 2)?
292 );
293 assert_eq!(
294 Amount::from_minor_units(0, 4)?.normalize(),
295 Amount::from_minor_units(0, 0)?
296 );
297 Ok(())
298 }
299}