typed_money/amount/
precision.rs1use super::type_def::Amount;
4use crate::{Currency, MoneyError, MoneyResult};
5
6impl<C: Currency> Amount<C> {
7 #[cfg(all(feature = "use_rust_decimal", not(feature = "use_bigdecimal")))]
31 pub fn has_excess_precision(&self) -> bool {
32 let scale = self.value.scale();
33 scale > u32::from(C::DECIMALS)
34 }
35
36 #[cfg(all(feature = "use_bigdecimal", not(feature = "use_rust_decimal")))]
37 pub fn has_excess_precision(&self) -> bool {
38 use bigdecimal::ToPrimitive;
39
40 let (_, scale) = self.value.as_bigint_and_exponent();
41 scale > i64::from(C::DECIMALS)
42 }
43
44 #[cfg(all(feature = "use_rust_decimal", not(feature = "use_bigdecimal")))]
61 pub fn precision(&self) -> u32 {
62 self.value.scale()
63 }
64
65 #[cfg(all(feature = "use_bigdecimal", not(feature = "use_rust_decimal")))]
66 pub fn precision(&self) -> i64 {
67 let (_, scale) = self.value.as_bigint_and_exponent();
68 scale
69 }
70
71 #[inline]
85 pub const fn currency_precision() -> u8 {
86 C::DECIMALS
87 }
88
89 pub fn normalize(&self) -> Self {
107 use crate::RoundingMode;
108 self.round(RoundingMode::HalfEven)
109 }
110
111 #[cfg(all(feature = "use_rust_decimal", not(feature = "use_bigdecimal")))]
133 pub fn check_precision(&self) -> MoneyResult<()> {
134 if self.has_excess_precision() {
135 Err(MoneyError::PrecisionError {
136 currency: C::CODE,
137 expected: C::DECIMALS,
138 actual: self.precision(),
139 suggestion: format!(
140 "Use normalize() or round() to {} decimal places",
141 C::DECIMALS
142 ),
143 })
144 } else {
145 Ok(())
146 }
147 }
148
149 #[cfg(all(feature = "use_bigdecimal", not(feature = "use_rust_decimal")))]
150 pub fn check_precision(&self) -> MoneyResult<()> {
151 if self.has_excess_precision() {
152 Err(MoneyError::PrecisionError {
153 currency: C::CODE,
154 expected: C::DECIMALS,
155 actual: self.precision() as u32,
156 suggestion: format!(
157 "Use normalize() or round() to adjust precision to {} decimal places",
158 C::DECIMALS
159 ),
160 })
161 } else {
162 Ok(())
163 }
164 }
165
166 #[cfg(all(feature = "use_rust_decimal", feature = "use_bigdecimal"))]
171 pub fn check_precision(&self) -> MoneyResult<()> {
172 Ok(())
175 }
176}
177
178#[cfg(test)]
179#[cfg(not(all(feature = "use_rust_decimal", feature = "use_bigdecimal")))]
180mod tests {
181 use super::*;
182 use crate::{BTC, EUR, JPY, USD};
183
184 #[test]
189 fn test_has_excess_precision_usd() {
190 let exact = Amount::<USD>::from_minor(1234); assert!(!exact.has_excess_precision());
193
194 let divided = Amount::<USD>::from_major(100) / 3; assert!(divided.has_excess_precision());
196 }
197
198 #[test]
199 fn test_has_excess_precision_jpy() {
200 let exact = Amount::<JPY>::from_major(100);
202 assert!(!exact.has_excess_precision());
203
204 let divided = Amount::<JPY>::from_major(100) / 3; assert!(divided.has_excess_precision());
206 }
207
208 #[test]
209 fn test_has_excess_precision_btc() {
210 let exact = Amount::<BTC>::from_minor(12345678); assert!(!exact.has_excess_precision());
213
214 let divided = Amount::<BTC>::from_major(1) / 3; assert!(divided.has_excess_precision());
216 }
217
218 #[test]
219 fn test_precision_method() {
220 let usd = Amount::<USD>::from_minor(1234); assert_eq!(usd.precision(), 2);
222
223 let jpy = Amount::<JPY>::from_major(1234); assert_eq!(jpy.precision(), 0);
225 }
226
227 #[test]
228 fn test_precision_after_division() {
229 let amount = Amount::<USD>::from_major(10);
230 let divided = amount / 3; assert!(divided.precision() > 2);
234 assert!(divided.has_excess_precision());
235 }
236
237 #[test]
238 fn test_currency_precision() {
239 assert_eq!(Amount::<USD>::currency_precision(), 2);
240 assert_eq!(Amount::<EUR>::currency_precision(), 2);
241 assert_eq!(Amount::<JPY>::currency_precision(), 0);
242 assert_eq!(Amount::<BTC>::currency_precision(), 8);
243 }
244
245 #[test]
250 fn test_normalize_removes_excess_precision() {
251 let amount = Amount::<USD>::from_major(100) / 3; assert!(amount.has_excess_precision());
253
254 let normalized = amount.normalize();
255 assert!(!normalized.has_excess_precision());
256 }
257
258 #[test]
259 fn test_normalize_uses_half_even() {
260 use rust_decimal::Decimal;
261 use std::marker::PhantomData;
262
263 let value = Decimal::new(12345, 3);
265 let amount = Amount::<USD> {
266 value,
267 _currency: PhantomData,
268 };
269
270 let normalized = amount.normalize();
271 assert_eq!(normalized.to_minor(), 1234);
272 }
273
274 #[test]
275 fn test_normalize_already_normalized() {
276 let amount = Amount::<USD>::from_minor(1234); let normalized = amount.normalize();
278
279 assert_eq!(amount, normalized);
280 }
281
282 #[test]
283 fn test_normalize_jpy() {
284 let amount = Amount::<JPY>::from_major(100) / 3; let normalized = amount.normalize();
286
287 assert_eq!(normalized.to_major_floor(), 33);
288 assert!(!normalized.has_excess_precision());
289 }
290
291 #[test]
296 fn test_arithmetic_preserves_precision() {
297 let a = Amount::<USD>::from_minor(1000); let b = Amount::<USD>::from_minor(333); let sum = a + b; assert_eq!(sum.precision(), 2);
302 assert!(!sum.has_excess_precision());
303 }
304
305 #[test]
306 fn test_division_increases_precision() {
307 let amount = Amount::<USD>::from_major(10);
308 let divided = amount / 3;
309
310 assert!(divided.precision() > Amount::<USD>::currency_precision().into());
312 assert!(divided.has_excess_precision());
313 }
314
315 #[test]
316 fn test_multiplication_can_increase_precision() {
317 let amount = Amount::<USD>::from_minor(1000); let multiplied = amount * 3; assert!(multiplied.precision() >= 2);
322 }
323
324 #[test]
329 fn test_precision_detection_deterministic() {
330 let amount1 = Amount::<USD>::from_major(100) / 3;
332 let amount2 = Amount::<USD>::from_major(100) / 3;
333
334 assert_eq!(
335 amount1.has_excess_precision(),
336 amount2.has_excess_precision()
337 );
338 }
339
340 #[test]
341 fn test_normalize_deterministic() {
342 let amount1 = Amount::<USD>::from_major(100) / 3;
343 let amount2 = Amount::<USD>::from_major(100) / 3;
344
345 let norm1 = amount1.normalize();
346 let norm2 = amount2.normalize();
347
348 assert_eq!(norm1, norm2);
349 }
350
351 #[test]
352 fn test_cross_platform_precision_behavior() {
353 let operations = [
355 Amount::<USD>::from_major(10) / 3,
356 Amount::<USD>::from_major(100) / 7,
357 Amount::<USD>::from_minor(1) / 3,
358 ];
359
360 for op in operations {
361 assert!(op.has_excess_precision());
363
364 let normalized = op.normalize();
366 assert!(!normalized.has_excess_precision());
367 }
368 }
369
370 #[test]
375 fn test_zero_precision() {
376 let zero = Amount::<USD>::from_major(0);
377 assert!(!zero.has_excess_precision());
378 assert_eq!(zero.normalize(), zero);
379 }
380
381 #[test]
382 fn test_negative_precision() {
383 let neg = Amount::<USD>::from_major(-100) / 3;
384 assert!(neg.has_excess_precision());
385
386 let normalized = neg.normalize();
387 assert!(!normalized.has_excess_precision());
388 }
389
390 #[test]
391 fn test_very_large_precision() {
392 let btc = Amount::<BTC>::from_major(1);
394 let divided = btc / 7; assert!(divided.has_excess_precision());
397
398 let normalized = divided.normalize();
399 assert!(!normalized.has_excess_precision());
400 }
401
402 #[test]
407 fn test_check_precision_ok() {
408 let amount = Amount::<USD>::from_minor(1234); assert!(amount.check_precision().is_ok());
410 }
411
412 #[test]
413 fn test_check_precision_error() {
414 let amount = Amount::<USD>::from_major(100) / 3; let result = amount.check_precision();
416 assert!(result.is_err());
417
418 if let Err(e) = result {
419 assert!(matches!(e, MoneyError::PrecisionError { .. }));
420 assert_eq!(e.currency(), Some("USD"));
421 let msg = e.to_string();
422 assert!(
423 msg.contains("Precision") || msg.contains("precision"),
424 "Message: {}",
425 msg
426 );
427 }
428 }
429
430 #[test]
431 fn test_check_precision_error_recovery() {
432 let amount = Amount::<USD>::from_major(100) / 3; assert!(amount.check_precision().is_err());
436
437 let normalized = amount.normalize();
439 assert!(normalized.check_precision().is_ok());
440 }
441
442 #[test]
443 fn test_check_precision_jpy() {
444 let jpy = Amount::<JPY>::from_major(100);
445 assert!(jpy.check_precision().is_ok());
446
447 let divided = jpy / 3;
448 assert!(divided.check_precision().is_err());
449 }
450
451 #[test]
452 fn test_precision_error_message() {
453 let amount = Amount::<USD>::from_major(100) / 3;
454 if let Err(e) = amount.check_precision() {
455 let msg = e.to_string();
456 assert!(msg.contains("USD"));
457 assert!(msg.contains("2 decimal places"));
458 }
459 }
460
461 #[test]
462 fn test_precision_error_suggestion() {
463 let amount = Amount::<USD>::from_major(100) / 3;
464 if let Err(e) = amount.check_precision() {
465 let suggestion = e.suggestion();
466 assert!(suggestion.contains("normalize") || suggestion.contains("round"));
467 }
468 }
469}