1use std::{
50 cmp::Ordering,
51 fmt::{Debug, Display},
52 hash::{Hash, Hasher},
53 iter::Sum,
54 ops::{Add, Deref, Div, Mul, Sub},
55 str::FromStr,
56};
57
58#[cfg(feature = "defi")]
59use alloy_primitives::U256;
60use nautilus_core::{
61 correctness::{
62 CorrectnessError, CorrectnessResult, CorrectnessResultExt, FAILED,
63 check_in_range_inclusive_f64,
64 },
65 string::formatting::Separable,
66};
67use rust_decimal::Decimal;
68use serde::{Deserialize, Deserializer, Serialize};
69
70#[cfg(feature = "defi")]
71use super::fixed::compare_raw;
72use super::fixed::{
73 FIXED_PRECISION, FIXED_SCALAR, FIXED_SCALAR_RAW, canonical_raw, check_fixed_precision,
74 checked_mul_div_fixed, checked_mul_div_raw, format_scaled_u128,
75 mantissa_exponent_to_fixed_i128, mantissa_exponent_to_raw_checked, parse_decimal_mantissa,
76 raw_scale, raw_scales_match, scaled_raw_to_decimal,
77};
78#[cfg(not(feature = "high-precision"))]
79use super::fixed::{f64_to_fixed_u64, fixed_u64_to_f64};
80#[cfg(feature = "high-precision")]
81use super::fixed::{f64_to_fixed_u128, fixed_u128_to_f64};
82#[cfg(feature = "defi")]
83use crate::types::fixed::MAX_FLOAT_PRECISION;
84
85#[cfg(feature = "high-precision")]
90pub type QuantityRaw = u128;
91
92#[cfg(not(feature = "high-precision"))]
93pub type QuantityRaw = u64;
94
95#[unsafe(no_mangle)]
104#[allow(unsafe_code)]
105pub static QUANTITY_RAW_MAX: QuantityRaw =
106 (QUANTITY_MAX as QuantityRaw) * (FIXED_SCALAR as QuantityRaw);
107
108pub const QUANTITY_UNDEF: QuantityRaw = QuantityRaw::MAX;
110
111#[cfg(feature = "high-precision")]
116pub const QUANTITY_MAX: f64 = 34_028_236_692_093.0;
118
119#[cfg(not(feature = "high-precision"))]
120pub const QUANTITY_MAX: f64 = 18_446_744_073.0;
122
123pub const QUANTITY_MIN: f64 = 0.0;
127
128#[repr(C)]
139#[derive(Clone, Copy, Default, Eq)]
140#[cfg_attr(
141 feature = "python",
142 pyo3::pyclass(module = "nautilus_trader.model", frozen, from_py_object)
143)]
144#[cfg_attr(
145 feature = "python",
146 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
147)]
148pub struct Quantity {
149 pub(crate) raw: QuantityRaw,
150 pub precision: u8,
152}
153
154impl Quantity {
155 pub fn new_checked(value: f64, precision: u8) -> CorrectnessResult<Self> {
167 check_in_range_inclusive_f64(value, QUANTITY_MIN, QUANTITY_MAX, "value")?;
168
169 #[cfg(feature = "defi")]
170 if precision > MAX_FLOAT_PRECISION {
171 return Err(CorrectnessError::PredicateViolation {
173 message: format!(
174 "`precision` exceeded maximum float precision ({MAX_FLOAT_PRECISION}), use `Quantity::from_wei()` for wei values instead"
175 ),
176 });
177 }
178
179 check_fixed_precision(precision)?;
180
181 #[cfg(feature = "high-precision")]
182 let raw = f64_to_fixed_u128(value, precision);
183 #[cfg(not(feature = "high-precision"))]
184 let raw = f64_to_fixed_u64(value, precision);
185
186 Ok(Self { raw, precision })
187 }
188
189 #[must_use]
195 pub fn new(value: f64, precision: u8) -> Self {
196 Self::new_checked(value, precision).expect_display(FAILED)
197 }
198
199 #[must_use]
206 pub fn from_raw(raw: QuantityRaw, precision: u8) -> Self {
207 assert!(
208 raw == QUANTITY_UNDEF || raw <= QUANTITY_RAW_MAX,
209 "`raw` value {raw} exceeds QUANTITY_RAW_MAX={QUANTITY_RAW_MAX} for Quantity"
210 );
211
212 if raw == QUANTITY_UNDEF {
213 assert!(
214 precision == 0,
215 "`precision` must be 0 when `raw` is QUANTITY_UNDEF"
216 );
217 }
218
219 check_fixed_precision(precision).expect_display(FAILED);
220
221 Self { raw, precision }
230 }
231
232 pub fn from_raw_checked(raw: QuantityRaw, precision: u8) -> CorrectnessResult<Self> {
242 if raw == QUANTITY_UNDEF && precision != 0 {
243 return Err(CorrectnessError::PredicateViolation {
244 message: "`precision` must be 0 when `raw` is QUANTITY_UNDEF".to_string(),
245 });
246 }
247
248 if raw != QUANTITY_UNDEF && raw > QUANTITY_RAW_MAX {
249 return Err(CorrectnessError::PredicateViolation {
250 message: format!("raw value {raw} exceeds QUANTITY_RAW_MAX={QUANTITY_RAW_MAX}"),
251 });
252 }
253
254 check_fixed_precision(precision)?;
255
256 Ok(Self { raw, precision })
257 }
258
259 #[must_use]
266 pub fn checked_add(self, rhs: Self) -> Option<Self> {
267 if self.raw == QUANTITY_UNDEF || rhs.raw == QUANTITY_UNDEF {
268 return None;
269 }
270
271 if !raw_scales_match(self.precision, rhs.precision) {
272 return None;
273 }
274
275 let raw = self.raw.checked_add(rhs.raw)?;
276 if raw > QUANTITY_RAW_MAX {
277 return None;
278 }
279
280 Some(Self {
281 raw,
282 precision: self.precision.max(rhs.precision),
283 })
284 }
285
286 #[must_use]
292 pub fn checked_sub(self, rhs: Self) -> Option<Self> {
293 if self.raw == QUANTITY_UNDEF || rhs.raw == QUANTITY_UNDEF {
294 return None;
295 }
296
297 if !raw_scales_match(self.precision, rhs.precision) {
298 return None;
299 }
300
301 let raw = self.raw.checked_sub(rhs.raw)?;
302
303 Some(Self {
304 raw,
305 precision: self.precision.max(rhs.precision),
306 })
307 }
308
309 #[must_use]
317 pub fn saturating_add(self, rhs: Self) -> Self {
318 assert!(
319 raw_scales_match(self.precision, rhs.precision),
320 "Cannot add `Quantity` values with mismatched decimal scales"
321 );
322
323 Self {
324 raw: self.raw.saturating_add(rhs.raw).min(QUANTITY_RAW_MAX),
325 precision: self.precision.max(rhs.precision),
326 }
327 }
328
329 #[must_use]
341 pub fn saturating_sub(self, rhs: Self) -> Self {
342 assert!(
343 raw_scales_match(self.precision, rhs.precision),
344 "Cannot subtract `Quantity` values with mismatched decimal scales"
345 );
346 let precision = self.precision.max(rhs.precision);
347 let raw = self.raw.saturating_sub(rhs.raw);
348 if raw == 0 && self.raw < rhs.raw {
349 log::warn!(
350 "Saturating Quantity subtraction: {self} - {rhs} < 0, clamped to 0 (precision={precision})"
351 );
352 }
353
354 Self { raw, precision }
355 }
356
357 #[must_use]
363 pub fn zero(precision: u8) -> Self {
364 check_fixed_precision(precision).expect_display(FAILED);
365 Self { raw: 0, precision }
366 }
367
368 #[must_use]
370 pub fn is_undefined(&self) -> bool {
371 self.raw == QUANTITY_UNDEF
372 }
373
374 #[must_use]
387 #[inline]
388 pub const fn raw(&self) -> QuantityRaw {
389 self.raw
390 }
391
392 #[must_use]
394 #[inline]
395 pub fn is_zero(&self) -> bool {
396 self.raw == 0
397 }
398
399 #[must_use]
401 #[inline]
402 pub fn non_zero(&self) -> bool {
403 self.raw != 0
404 }
405
406 #[must_use]
408 #[inline]
409 pub fn is_positive(&self) -> bool {
410 self.raw != QUANTITY_UNDEF && self.raw > 0
411 }
412
413 #[cfg(feature = "high-precision")]
414 #[must_use]
420 pub fn as_f64(&self) -> f64 {
421 #[cfg(feature = "defi")]
422 assert!(
423 self.precision <= MAX_FLOAT_PRECISION,
424 "Invalid f64 conversion beyond `MAX_FLOAT_PRECISION` (16)"
425 );
426
427 fixed_u128_to_f64(self.raw)
428 }
429
430 #[cfg(not(feature = "high-precision"))]
431 #[must_use]
433 pub fn as_f64(&self) -> f64 {
434 fixed_u64_to_f64(self.raw)
435 }
436
437 #[must_use]
439 pub fn as_decimal(&self) -> Decimal {
440 let precision_diff = FIXED_PRECISION.saturating_sub(self.precision);
442 let rescaled_raw = self.raw / QuantityRaw::pow(10, u32::from(precision_diff));
443
444 #[allow(
448 clippy::unnecessary_cast,
449 clippy::cast_lossless,
450 reason = "cast is real when QuantityRaw is u64, no-op when u128"
451 )]
452 scaled_raw_to_decimal(rescaled_raw as i128, self.precision)
453 }
454
455 #[must_use]
457 #[allow(
458 clippy::unnecessary_fallible_conversions,
459 reason = "try_from is infallible when QuantityRaw is u64, fallible when u128"
460 )]
461 pub(crate) fn raw_as_decimal(raw: QuantityRaw) -> Decimal {
462 let whole =
463 i128::try_from(raw / FIXED_SCALAR_RAW).expect("Whole raw quantity must fit in Decimal");
464 let fractional = i128::try_from(raw % FIXED_SCALAR_RAW)
465 .expect("Fractional raw quantity must fit in Decimal");
466
467 Decimal::from(whole) + Decimal::from_i128_with_scale(fractional, u32::from(FIXED_PRECISION))
468 }
469
470 #[must_use]
472 pub fn to_formatted_string(&self) -> String {
473 format!("{self}").separate_with_underscores()
474 }
475
476 fn raw_at_precision(&self) -> QuantityRaw {
477 let precision_diff = FIXED_PRECISION.saturating_sub(self.precision);
478 self.raw / QuantityRaw::pow(10, u32::from(precision_diff))
479 }
480
481 fn raw_as_u128(raw: QuantityRaw) -> u128 {
482 #[allow(
483 clippy::useless_conversion,
484 reason = "u128::from is a widening conversion when QuantityRaw is u64"
485 )]
486 u128::from(raw)
487 }
488
489 pub fn from_decimal_dp(decimal: Decimal, precision: u8) -> CorrectnessResult<Self> {
502 if decimal.mantissa() < 0 {
503 return Err(CorrectnessError::PredicateViolation {
504 message: format!(
505 "Decimal value '{decimal}' is negative, Quantity must be non-negative"
506 ),
507 });
508 }
509
510 let exponent = -(decimal.scale() as i8);
511 let raw_i128 = mantissa_exponent_to_fixed_i128(decimal.mantissa(), exponent, precision)?;
512
513 let raw: QuantityRaw =
514 raw_i128
515 .try_into()
516 .map_err(|_| CorrectnessError::PredicateViolation {
517 message: format!(
518 "Decimal value exceeds QuantityRaw range [0, {QUANTITY_RAW_MAX}]"
519 ),
520 })?;
521
522 if raw > QUANTITY_RAW_MAX {
523 return Err(CorrectnessError::PredicateViolation {
524 message: format!(
525 "Raw value {raw} exceeds QUANTITY_RAW_MAX={QUANTITY_RAW_MAX} for Quantity"
526 ),
527 });
528 }
529
530 Ok(Self { raw, precision })
531 }
532
533 pub fn from_decimal(decimal: Decimal) -> CorrectnessResult<Self> {
545 let precision = decimal.scale() as u8;
546 Self::from_decimal_dp(decimal, precision)
547 }
548
549 #[must_use]
558 pub fn from_mantissa_exponent(mantissa: u64, exponent: i8, precision: u8) -> Self {
559 check_fixed_precision(precision).expect_display(FAILED);
560
561 if mantissa == 0 {
562 return Self { raw: 0, precision };
563 }
564
565 let raw_i128 = mantissa_exponent_to_fixed_i128(i128::from(mantissa), exponent, precision)
566 .expect("Overflow in Quantity::from_mantissa_exponent");
567
568 let raw: QuantityRaw = raw_i128
569 .try_into()
570 .expect("Raw value exceeds QuantityRaw range in Quantity::from_mantissa_exponent");
571 assert!(
572 raw <= QUANTITY_RAW_MAX,
573 "`raw` value {raw} exceeded QUANTITY_RAW_MAX={QUANTITY_RAW_MAX} for Quantity"
574 );
575
576 Self { raw, precision }
577 }
578
579 pub fn from_mantissa_exponent_checked(
586 mantissa: u64,
587 exponent: i8,
588 precision: u8,
589 ) -> CorrectnessResult<Self> {
590 let raw = mantissa_exponent_to_raw_checked::<QuantityRaw>(
591 i128::from(mantissa),
592 exponent,
593 precision,
594 "Quantity::from_mantissa_exponent",
595 "QuantityRaw",
596 "Quantity",
597 )?;
598
599 Self::from_raw_checked(raw, precision)
600 }
601
602 #[cfg(feature = "defi")]
610 pub fn from_u256(amount: U256, precision: u8) -> CorrectnessResult<Self> {
611 let scaled_amount = if precision < FIXED_PRECISION {
613 amount
614 .checked_mul(U256::from(
615 10u128.pow(u32::from(FIXED_PRECISION - precision)),
616 ))
617 .ok_or_else(|| CorrectnessError::PredicateViolation {
618 message: format!(
619 "Amount overflow during scaling to fixed precision: {} * 10^{}",
620 amount,
621 FIXED_PRECISION - precision
622 ),
623 })?
624 } else {
625 amount
626 };
627
628 let raw = QuantityRaw::try_from(scaled_amount).map_err(|_| {
629 CorrectnessError::PredicateViolation {
630 message: format!("U256 scaled amount {scaled_amount} exceeds QuantityRaw range"),
631 }
632 })?;
633
634 Self::from_raw_checked(raw, precision)
635 }
636}
637
638impl From<Quantity> for f64 {
639 fn from(qty: Quantity) -> Self {
640 qty.as_f64()
641 }
642}
643
644impl From<&Quantity> for f64 {
645 fn from(qty: &Quantity) -> Self {
646 qty.as_f64()
647 }
648}
649
650impl From<i32> for Quantity {
651 fn from(value: i32) -> Self {
657 assert!(
658 value >= 0,
659 "Cannot create Quantity from negative i32: {value}. Use u32 or check value is non-negative."
660 );
661 Self::from_mantissa_exponent(u64::from(value.cast_unsigned()), 0, 0)
662 }
663}
664
665impl From<i64> for Quantity {
666 fn from(value: i64) -> Self {
672 assert!(
673 value >= 0,
674 "Cannot create Quantity from negative i64: {value}. Use u64 or check value is non-negative."
675 );
676 Self::from_mantissa_exponent(value.cast_unsigned(), 0, 0)
677 }
678}
679
680impl From<u32> for Quantity {
681 fn from(value: u32) -> Self {
682 Self::from_mantissa_exponent(u64::from(value), 0, 0)
683 }
684}
685
686impl From<u64> for Quantity {
687 fn from(value: u64) -> Self {
688 Self::from_mantissa_exponent(value, 0, 0)
689 }
690}
691
692impl Hash for Quantity {
693 fn hash<H: Hasher>(&self, state: &mut H) {
694 canonical_raw(self.raw, self.precision).hash(state);
695 }
696}
697
698impl PartialEq for Quantity {
699 #[inline]
700 fn eq(&self, other: &Self) -> bool {
701 self.cmp(other) == Ordering::Equal
702 }
703}
704
705impl PartialOrd for Quantity {
706 #[inline]
707 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
708 Some(self.cmp(other))
709 }
710}
711
712impl Ord for Quantity {
713 #[inline]
714 fn cmp(&self, other: &Self) -> Ordering {
715 #[cfg(feature = "defi")]
716 {
717 compare_raw(self.raw, self.precision, other.raw, other.precision)
718 }
719
720 #[cfg(not(feature = "defi"))]
721 {
722 self.raw.cmp(&other.raw)
723 }
724 }
725}
726
727impl Deref for Quantity {
728 type Target = QuantityRaw;
729
730 fn deref(&self) -> &Self::Target {
731 &self.raw
732 }
733}
734
735impl Add for Quantity {
736 type Output = Self;
737 #[inline]
738 fn add(self, rhs: Self) -> Self::Output {
739 #[cfg(feature = "defi")]
740 assert!(
741 raw_scales_match(self.precision, rhs.precision),
742 "Cannot add `Quantity` values with mismatched decimal scales"
743 );
744 Self {
745 raw: self
746 .raw
747 .checked_add(rhs.raw)
748 .expect("Overflow occurred when adding `Quantity`"),
749 precision: self.precision.max(rhs.precision),
750 }
751 }
752}
753
754impl Sum for Quantity {
755 fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
756 iter.reduce(|acc, x| acc + x)
757 .unwrap_or_else(|| Self::zero(0))
758 }
759}
760
761impl<'a> Sum<&'a Self> for Quantity {
762 fn sum<I: Iterator<Item = &'a Self>>(iter: I) -> Self {
763 iter.copied().sum()
764 }
765}
766
767impl Sub for Quantity {
768 type Output = Self;
769 #[inline]
770 fn sub(self, rhs: Self) -> Self::Output {
771 #[cfg(feature = "defi")]
772 assert!(
773 raw_scales_match(self.precision, rhs.precision),
774 "Cannot subtract `Quantity` values with mismatched decimal scales"
775 );
776 Self {
777 raw: self
778 .raw
779 .checked_sub(rhs.raw)
780 .expect("Underflow occurred when subtracting `Quantity`"),
781 precision: self.precision.max(rhs.precision),
782 }
783 }
784}
785
786impl Mul for Quantity {
787 type Output = Self;
788 fn mul(self, rhs: Self) -> Self::Output {
789 let result_raw = if self.raw != QUANTITY_UNDEF
790 && rhs.raw != QUANTITY_UNDEF
791 && self.precision <= FIXED_PRECISION
792 && rhs.precision <= FIXED_PRECISION
793 {
794 checked_mul_div_fixed(self.raw, rhs.raw)
795 } else {
796 let scalar = QuantityRaw::try_from(raw_scale(self.precision.min(rhs.precision)))
797 .expect("Fixed-point scale fits QuantityRaw");
798 checked_mul_div_raw(self.raw, rhs.raw, scalar)
799 }
800 .filter(|raw| *raw <= QUANTITY_RAW_MAX)
801 .expect("Overflow occurred when multiplying `Quantity`");
802
803 Self {
804 raw: result_raw,
805 precision: self.precision.max(rhs.precision),
806 }
807 }
808}
809
810impl Add<Decimal> for Quantity {
811 type Output = Decimal;
812 fn add(self, rhs: Decimal) -> Self::Output {
813 self.as_decimal() + rhs
814 }
815}
816
817impl Sub<Decimal> for Quantity {
818 type Output = Decimal;
819 fn sub(self, rhs: Decimal) -> Self::Output {
820 self.as_decimal() - rhs
821 }
822}
823
824impl Mul<Decimal> for Quantity {
825 type Output = Decimal;
826 fn mul(self, rhs: Decimal) -> Self::Output {
827 self.as_decimal() * rhs
828 }
829}
830
831impl Div<Decimal> for Quantity {
832 type Output = Decimal;
833 fn div(self, rhs: Decimal) -> Self::Output {
834 self.as_decimal() / rhs
835 }
836}
837
838impl Add<f64> for Quantity {
839 type Output = f64;
840 fn add(self, rhs: f64) -> Self::Output {
841 self.as_f64() + rhs
842 }
843}
844
845impl Sub<f64> for Quantity {
846 type Output = f64;
847 fn sub(self, rhs: f64) -> Self::Output {
848 self.as_f64() - rhs
849 }
850}
851
852impl Mul<f64> for Quantity {
853 type Output = f64;
854 fn mul(self, rhs: f64) -> Self::Output {
855 self.as_f64() * rhs
856 }
857}
858
859impl Div<f64> for Quantity {
860 type Output = f64;
861 fn div(self, rhs: f64) -> Self::Output {
862 self.as_f64() / rhs
863 }
864}
865
866impl From<Quantity> for QuantityRaw {
867 fn from(value: Quantity) -> Self {
868 value.raw
869 }
870}
871
872impl From<&Quantity> for QuantityRaw {
873 fn from(value: &Quantity) -> Self {
874 value.raw
875 }
876}
877
878impl From<Quantity> for Decimal {
879 fn from(value: Quantity) -> Self {
880 value.as_decimal()
881 }
882}
883
884impl From<&Quantity> for Decimal {
885 fn from(value: &Quantity) -> Self {
886 value.as_decimal()
887 }
888}
889
890impl FromStr for Quantity {
891 type Err = String;
892
893 fn from_str(value: &str) -> Result<Self, Self::Err> {
894 let clean_value = value.replace('_', "");
895
896 if clean_value.contains('e') || clean_value.contains('E') {
897 let decimal = Decimal::from_scientific(&clean_value)
898 .map_err(|e| format!("Error parsing `input` string '{value}' as Decimal: {e}"))?;
899 let precision = decimal.scale() as u8;
900 return Self::from_decimal_dp(decimal, precision).map_err(|e| e.to_string());
901 }
902
903 let (mantissa, precision) = parse_decimal_mantissa(&clean_value)
904 .map_err(|e| format!("Error parsing `input` string '{value}' as Decimal: {e}"))?;
905 if mantissa < 0 {
906 return Err(format!(
907 "Decimal value '{clean_value}' is negative, Quantity must be non-negative"
908 ));
909 }
910 let exponent = -i8::try_from(precision).map_err(|e| e.to_string())?;
911 let raw = mantissa_exponent_to_raw_checked::<QuantityRaw>(
912 mantissa,
913 exponent,
914 precision,
915 "Quantity::from_str",
916 "QuantityRaw",
917 "Quantity",
918 )
919 .map_err(|e| e.to_string())?;
920 Self::from_raw_checked(raw, precision).map_err(|e| e.to_string())
921 }
922}
923
924impl From<&str> for Quantity {
925 fn from(value: &str) -> Self {
926 Self::from_str(value).expect(FAILED)
927 }
928}
929
930impl From<String> for Quantity {
931 fn from(value: String) -> Self {
932 Self::from_str(&value).expect(FAILED)
933 }
934}
935
936impl From<&String> for Quantity {
937 fn from(value: &String) -> Self {
938 Self::from_str(value).expect(FAILED)
939 }
940}
941
942impl Debug for Quantity {
943 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
944 write!(
945 f,
946 "{}({})",
947 stringify!(Quantity),
948 format_scaled_u128(Self::raw_as_u128(self.raw_at_precision()), self.precision),
949 )
950 }
951}
952
953impl Display for Quantity {
954 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
955 write!(
956 f,
957 "{}",
958 format_scaled_u128(Self::raw_as_u128(self.raw_at_precision()), self.precision),
959 )
960 }
961}
962
963impl Serialize for Quantity {
964 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
965 where
966 S: serde::Serializer,
967 {
968 serializer.serialize_str(&self.to_string())
969 }
970}
971
972impl<'de> Deserialize<'de> for Quantity {
973 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
974 where
975 D: Deserializer<'de>,
976 {
977 let qty_str: std::borrow::Cow<'de, str> = Deserialize::deserialize(deserializer)?;
978 Self::from_str(qty_str.as_ref()).map_err(serde::de::Error::custom)
979 }
980}
981
982pub fn check_positive_quantity(value: Quantity, param: &str) -> CorrectnessResult<()> {
988 if !value.is_positive() {
989 return Err(CorrectnessError::NotPositive {
990 param: param.to_string(),
991 value: value.to_string(),
992 type_name: "`Quantity`",
993 });
994 }
995 Ok(())
996}
997
998#[cfg(test)]
999mod tests {
1000 use std::str::FromStr;
1001
1002 use nautilus_core::{approx_eq, correctness::CorrectnessError};
1003 use rstest::rstest;
1004 use rust_decimal_macros::dec;
1005
1006 use super::*;
1007 #[cfg(not(feature = "defi"))]
1008 use crate::types::fixed::MAX_FLOAT_PRECISION;
1009
1010 #[cfg(feature = "high-precision")]
1011 #[rstest]
1012 #[case(QUANTITY_RAW_MAX, dec!(34028236692093))]
1013 #[case(80_000_000_000_000_000_000_000_000_000, dec!(8000000000000))]
1014 fn test_as_decimal_above_decimal_mantissa(#[case] raw: QuantityRaw, #[case] expected: Decimal) {
1015 let qty = Quantity::from_raw(raw, 16);
1018
1019 assert_eq!(qty.as_decimal(), expected);
1020 }
1021
1022 #[rstest]
1023 fn test_max_quantity_round_trips_through_raw() {
1024 let qty = Quantity::new(QUANTITY_MAX, 0);
1027
1028 assert_eq!(qty.raw, QUANTITY_RAW_MAX);
1029 assert!(Quantity::from_raw_checked(qty.raw, 0).is_ok());
1030 assert!(qty.checked_add(Quantity::zero(0)).is_some());
1031 }
1032
1033 #[rstest]
1034 fn test_check_quantity_positive() {
1035 let qty = Quantity::new(0.0, 0);
1036 let error = check_positive_quantity(qty, "qty").unwrap_err();
1037
1038 assert_eq!(
1039 error,
1040 CorrectnessError::NotPositive {
1041 param: "qty".to_string(),
1042 value: "0".to_string(),
1043 type_name: "`Quantity`",
1044 }
1045 );
1046 assert_eq!(
1047 error.to_string(),
1048 "invalid `Quantity` for 'qty' not positive, was 0"
1049 );
1050 }
1051
1052 #[rstest]
1053 #[cfg(all(not(feature = "defi"), not(feature = "high-precision")))]
1054 #[should_panic(expected = "`precision` exceeded maximum `FIXED_PRECISION` (9), was 17")]
1055 fn test_invalid_precision_new() {
1056 let _ = Quantity::new(1.0, 17);
1058 }
1059
1060 #[rstest]
1061 #[cfg(all(not(feature = "defi"), feature = "high-precision"))]
1062 #[should_panic(expected = "`precision` exceeded maximum `FIXED_PRECISION` (16), was 17")]
1063 fn test_invalid_precision_new() {
1064 let _ = Quantity::new(1.0, 17);
1066 }
1067
1068 #[rstest]
1069 #[cfg(not(feature = "defi"))]
1070 #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
1071 fn test_invalid_precision_from_raw() {
1072 let _ = Quantity::from_raw(1, FIXED_PRECISION + 1);
1074 }
1075
1076 #[rstest]
1077 #[cfg(not(feature = "defi"))]
1078 #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
1079 fn test_invalid_precision_zero() {
1080 let _ = Quantity::zero(FIXED_PRECISION + 1);
1082 }
1083
1084 #[rstest]
1085 fn test_mixed_precision_add() {
1086 let q1 = Quantity::new(1.0, 1);
1087 let q2 = Quantity::new(1.0, 2);
1088 let result = q1 + q2;
1089 assert_eq!(result.precision, 2);
1090 assert_eq!(result.as_f64(), 2.0);
1091 }
1092
1093 #[rstest]
1094 fn test_sum_owned_quantities() {
1095 let quantities = [Quantity::new(1.25, 2), Quantity::new(2.75, 2)];
1096 let result: Quantity = quantities.into_iter().sum();
1097
1098 assert_eq!(result.as_decimal(), dec!(4.00));
1099 assert_eq!(result.precision, 2);
1100 }
1101
1102 #[rstest]
1103 fn test_sum_borrowed_quantities() {
1104 let quantities = [Quantity::new(0.125, 3), Quantity::new(0.375, 3)];
1105 let result: Quantity = quantities.iter().sum();
1106
1107 assert_eq!(result.as_decimal(), dec!(0.500));
1108 assert_eq!(result.precision, 3);
1109 }
1110
1111 #[rstest]
1112 fn test_sum_mixed_precision_quantities() {
1113 let quantities = [
1114 Quantity::new(1.2, 1),
1115 Quantity::new(3.45, 2),
1116 Quantity::new(0.006, 3),
1117 ];
1118 let result: Quantity = quantities.into_iter().sum();
1119
1120 assert_eq!(result.as_decimal(), dec!(4.656));
1121 assert_eq!(result.precision, 3);
1122 }
1123
1124 #[rstest]
1125 fn test_sum_empty_quantity_iterator() {
1126 let result: Quantity = std::iter::empty::<Quantity>().sum();
1127
1128 assert_eq!(result.as_decimal(), dec!(0));
1129 assert_eq!(result.precision, 0);
1130 }
1131
1132 #[rstest]
1133 fn test_mixed_precision_sub() {
1134 let q1 = Quantity::new(2.0, 1);
1135 let q2 = Quantity::new(1.0, 2);
1136 let result = q1 - q2;
1137 assert_eq!(result.precision, 2);
1138 assert_eq!(result.as_f64(), 1.0);
1139 }
1140
1141 #[rstest]
1142 fn test_mixed_precision_mul() {
1143 let q1 = Quantity::new(2.0, 1);
1144 let q2 = Quantity::new(3.0, 2);
1145 let result = q1 * q2;
1146 assert_eq!(result.precision, 2);
1147 assert_eq!(result.as_f64(), 6.0);
1148 }
1149
1150 #[rstest]
1151 #[case(0, false)]
1152 #[case(1, true)]
1153 #[case(QUANTITY_UNDEF, true)]
1154 fn test_non_zero(#[case] raw: QuantityRaw, #[case] expected: bool) {
1155 let qty = Quantity::from_raw(raw, 0);
1156 assert_eq!(qty.non_zero(), expected);
1157 }
1158
1159 #[rstest]
1160 fn test_new() {
1161 let value = 0.00812;
1162 let qty = Quantity::new(value, 8);
1163 assert_eq!(qty, qty);
1164 assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
1165 assert_eq!(qty.precision, 8);
1166 assert_eq!(qty, Quantity::from("0.00812000"));
1167 assert_eq!(qty.as_decimal(), dec!(0.00812000));
1168 assert_eq!(qty.to_string(), "0.00812000");
1169 assert!(qty.non_zero());
1170 assert!(qty.is_positive());
1171 assert!(approx_eq!(f64, qty.as_f64(), 0.00812, epsilon = 0.000_001));
1172 }
1173
1174 #[rstest]
1175 fn test_check_quantity_positive_ok() {
1176 let qty = Quantity::new(10.0, 0);
1177 check_positive_quantity(qty, "qty").unwrap();
1178 }
1179
1180 #[rstest]
1181 fn test_negative_quantity_validation() {
1182 assert!(Quantity::new_checked(-1.0, FIXED_PRECISION).is_err());
1183 }
1184
1185 #[rstest]
1186 fn test_new_checked_returns_typed_error_with_stable_display() {
1187 let error = Quantity::new_checked(QUANTITY_MAX + 1.0, FIXED_PRECISION).unwrap_err();
1188
1189 assert!(matches!(error, CorrectnessError::OutOfRange { .. }));
1190 assert_eq!(
1191 error.to_string(),
1192 format!(
1193 "invalid f64 for 'value' not in range [{QUANTITY_MIN}, {QUANTITY_MAX}], was {}",
1194 QUANTITY_MAX + 1.0
1195 )
1196 );
1197 }
1198
1199 #[rstest]
1200 fn test_from_raw_checked_returns_typed_error_with_stable_display() {
1201 let error = Quantity::from_raw_checked(QUANTITY_UNDEF, 3).unwrap_err();
1202
1203 assert_eq!(
1204 error,
1205 CorrectnessError::PredicateViolation {
1206 message: "`precision` must be 0 when `raw` is QUANTITY_UNDEF".to_string(),
1207 }
1208 );
1209 assert_eq!(
1210 error.to_string(),
1211 "`precision` must be 0 when `raw` is QUANTITY_UNDEF"
1212 );
1213 }
1214
1215 #[rstest]
1216 fn test_undefined() {
1217 let qty = Quantity::from_raw(QUANTITY_UNDEF, 0);
1218 assert_eq!(qty.raw, QUANTITY_UNDEF);
1219 assert!(qty.is_undefined());
1220 }
1221
1222 #[rstest]
1223 fn test_zero() {
1224 let qty = Quantity::zero(8);
1225 assert_eq!(qty.raw, 0);
1226 assert_eq!(qty.precision, 8);
1227 assert!(qty.is_zero());
1228 assert!(!qty.is_positive());
1229 }
1230
1231 #[rstest]
1232 fn test_from_i32_exact() {
1233 let values = [0, 1, i32::MAX];
1234 let quantities = values.map(Quantity::from);
1235 let expected =
1236 values.map(|value| (QuantityRaw::try_from(value).unwrap() * FIXED_SCALAR_RAW, 0));
1237
1238 assert_eq!(
1239 quantities.map(|quantity| (quantity.raw, quantity.precision)),
1240 expected
1241 );
1242 }
1243
1244 #[rstest]
1245 fn test_from_i64_exact() {
1246 let max = quantity_max_i64();
1247 let values = [0, 1, max];
1248 let quantities = values.map(Quantity::from);
1249 let expected =
1250 values.map(|value| (QuantityRaw::try_from(value).unwrap() * FIXED_SCALAR_RAW, 0));
1251
1252 assert_eq!(
1253 quantities.map(|quantity| (quantity.raw, quantity.precision)),
1254 expected
1255 );
1256 }
1257
1258 #[rstest]
1259 fn test_from_u32_exact() {
1260 let values = [0, 1, u32::MAX];
1261 let quantities = values.map(Quantity::from);
1262 let expected = values.map(|value| (QuantityRaw::from(value) * FIXED_SCALAR_RAW, 0));
1263
1264 assert_eq!(
1265 quantities.map(|quantity| (quantity.raw, quantity.precision)),
1266 expected
1267 );
1268 }
1269
1270 #[rstest]
1271 fn test_from_u64_exact() {
1272 let max = quantity_max_u64();
1273 let values = [0, 1, max];
1274 let quantities = values.map(Quantity::from);
1275 let expected = values.map(|value| (QuantityRaw::from(value) * FIXED_SCALAR_RAW, 0));
1276
1277 assert_eq!(
1278 quantities.map(|quantity| (quantity.raw, quantity.precision)),
1279 expected
1280 );
1281 }
1282
1283 #[rstest]
1284 #[should_panic(
1285 expected = "Cannot create Quantity from negative i32: -1. Use u32 or check value is non-negative."
1286 )]
1287 fn test_from_i32_negative_panics() {
1288 let _ = Quantity::from(-1_i32);
1289 }
1290
1291 #[rstest]
1292 #[should_panic(
1293 expected = "Cannot create Quantity from negative i64: -1. Use u64 or check value is non-negative."
1294 )]
1295 fn test_from_i64_negative_panics() {
1296 let _ = Quantity::from(-1_i64);
1297 }
1298
1299 #[rstest]
1300 #[cfg_attr(
1301 feature = "high-precision",
1302 should_panic(expected = "exceeded QUANTITY_RAW_MAX")
1303 )]
1304 #[cfg_attr(
1305 not(feature = "high-precision"),
1306 should_panic(expected = "Raw value exceeds QuantityRaw range")
1307 )]
1308 fn test_from_i64_overflow_panics() {
1309 let max = quantity_max_i64();
1310
1311 let _ = Quantity::from(max + 1);
1312 }
1313
1314 #[rstest]
1315 #[cfg_attr(
1316 feature = "high-precision",
1317 should_panic(expected = "exceeded QUANTITY_RAW_MAX")
1318 )]
1319 #[cfg_attr(
1320 not(feature = "high-precision"),
1321 should_panic(expected = "Raw value exceeds QuantityRaw range")
1322 )]
1323 fn test_from_u64_overflow_panics() {
1324 let max = quantity_max_u64();
1325
1326 let _ = Quantity::from(max + 1);
1327 }
1328
1329 fn quantity_max_i64() -> i64 {
1330 i64::try_from(QUANTITY_RAW_MAX / FIXED_SCALAR_RAW).unwrap()
1331 }
1332
1333 #[allow(
1334 clippy::useless_conversion,
1335 reason = "try_from is a no-op when QuantityRaw is u64, and narrows when u128 (high-precision)"
1336 )]
1337 fn quantity_max_u64() -> u64 {
1338 u64::try_from(QUANTITY_RAW_MAX / FIXED_SCALAR_RAW).unwrap()
1339 }
1340
1341 #[rstest] fn test_with_maximum_value() {
1343 let qty = Quantity::new_checked(QUANTITY_MAX, 0);
1344 assert!(qty.is_ok());
1345 }
1346
1347 #[rstest]
1348 fn test_with_minimum_positive_value() {
1349 let value = 0.000_000_001;
1350 let qty = Quantity::new(value, 9);
1351 assert_eq!(qty.raw, Quantity::from("0.000000001").raw);
1352 assert_eq!(qty.as_decimal(), dec!(0.000000001));
1353 assert_eq!(qty.to_string(), "0.000000001");
1354 }
1355
1356 #[rstest]
1357 fn test_with_minimum_value() {
1358 let qty = Quantity::new(QUANTITY_MIN, 9);
1359 assert_eq!(qty.raw, 0);
1360 assert_eq!(qty.as_decimal(), dec!(0));
1361 assert_eq!(qty.to_string(), "0.000000000");
1362 }
1363
1364 #[rstest]
1365 fn test_is_zero() {
1366 let qty = Quantity::zero(8);
1367 assert_eq!(qty, qty);
1368 assert_eq!(qty.raw, 0);
1369 assert_eq!(qty.precision, 8);
1370 assert_eq!(qty, Quantity::from("0.00000000"));
1371 assert_eq!(qty.as_decimal(), dec!(0));
1372 assert_eq!(qty.to_string(), "0.00000000");
1373 assert!(qty.is_zero());
1374 }
1375
1376 #[rstest]
1377 fn test_precision() {
1378 let value = 1.001;
1379 let qty = Quantity::new(value, 2);
1380 assert_eq!(qty.to_string(), "1.00");
1381 }
1382
1383 #[rstest]
1384 fn test_new_from_str() {
1385 let qty = Quantity::new(0.008_120_00, 8);
1386 assert_eq!(qty, qty);
1387 assert_eq!(qty.precision, 8);
1388 assert_eq!(qty, Quantity::from("0.00812000"));
1389 assert_eq!(qty.to_string(), "0.00812000");
1390 }
1391
1392 #[rstest]
1393 #[case("0", 0)]
1394 #[case("1.1", 1)]
1395 #[case("1.123456789", 9)]
1396 fn test_from_str_valid_input(#[case] input: &str, #[case] expected_prec: u8) {
1397 let qty = Quantity::from(input);
1398 assert_eq!(qty.precision, expected_prec);
1399 assert_eq!(qty.as_decimal(), Decimal::from_str(input).unwrap());
1400 }
1401
1402 #[rstest]
1403 #[should_panic(expected = "ParseFloatError")]
1404 fn test_from_str_invalid_input() {
1405 let input = "invalid";
1406 let _ = Quantity::new(f64::from_str(input).unwrap(), 8);
1407 }
1408
1409 #[rstest]
1410 fn test_from_str_errors() {
1411 assert!(Quantity::from_str("invalid").is_err());
1412 assert!(Quantity::from_str("12.34.56").is_err());
1413 assert!(Quantity::from_str("").is_err());
1414 assert!(Quantity::from_str("-1").is_err()); assert!(Quantity::from_str("-0.001").is_err());
1416 }
1417
1418 #[rstest]
1419 #[case("1e7", 0, 10_000_000.0)]
1420 #[case("2.5e3", 0, 2_500.0)]
1421 #[case("1.234e-2", 5, 0.01234)]
1422 #[case("5E-3", 3, 0.005)]
1423 #[case("1.0e6", 0, 1_000_000.0)]
1424 fn test_from_str_scientific_notation(
1425 #[case] input: &str,
1426 #[case] expected_precision: u8,
1427 #[case] expected_value: f64,
1428 ) {
1429 let qty = Quantity::from_str(input).unwrap();
1430 assert_eq!(qty.precision, expected_precision);
1431 assert!(approx_eq!(
1432 f64,
1433 qty.as_f64(),
1434 expected_value,
1435 epsilon = 1e-10
1436 ));
1437 }
1438
1439 #[rstest]
1440 #[case("1_234.56", 2, 1234.56)]
1441 #[case("1000000", 0, 1_000_000.0)]
1442 #[case("99_999.999_99", 5, 99_999.999_99)]
1443 fn test_from_str_with_underscores(
1444 #[case] input: &str,
1445 #[case] expected_precision: u8,
1446 #[case] expected_value: f64,
1447 ) {
1448 let qty = Quantity::from_str(input).unwrap();
1449 assert_eq!(qty.precision, expected_precision);
1450 assert!(approx_eq!(
1451 f64,
1452 qty.as_f64(),
1453 expected_value,
1454 epsilon = 1e-10
1455 ));
1456 }
1457
1458 #[rstest]
1459 fn test_from_decimal_dp_preservation() {
1460 let decimal = dec!(123.456789);
1462 let qty = Quantity::from_decimal_dp(decimal, 6).unwrap();
1463 assert_eq!(qty.precision, 6);
1464 assert!(approx_eq!(f64, qty.as_f64(), 123.456_789, epsilon = 1e-10));
1465
1466 let expected_raw = 123_456_789_u64 * 10_u64.pow(u32::from(FIXED_PRECISION - 6));
1468 assert_eq!(qty.raw, QuantityRaw::from(expected_raw));
1469 }
1470
1471 #[rstest]
1472 fn test_from_decimal_dp_rounding() {
1473 let decimal = dec!(1.005);
1475 let qty = Quantity::from_decimal_dp(decimal, 2).unwrap();
1476 assert_eq!(qty.as_f64(), 1.0); let decimal = dec!(1.015);
1479 let qty = Quantity::from_decimal_dp(decimal, 2).unwrap();
1480 assert_eq!(qty.as_f64(), 1.02); }
1482
1483 #[rstest]
1484 fn test_from_decimal_infers_precision() {
1485 let decimal = dec!(123.456);
1487 let qty = Quantity::from_decimal(decimal).unwrap();
1488 assert_eq!(qty.precision, 3);
1489 assert!(approx_eq!(f64, qty.as_f64(), 123.456, epsilon = 1e-10));
1490
1491 let decimal = dec!(100);
1493 let qty = Quantity::from_decimal(decimal).unwrap();
1494 assert_eq!(qty.precision, 0);
1495 assert_eq!(qty.as_f64(), 100.0);
1496
1497 let decimal = dec!(1.23456789);
1499 let qty = Quantity::from_decimal(decimal).unwrap();
1500 assert_eq!(qty.precision, 8);
1501 assert!(approx_eq!(f64, qty.as_f64(), 1.234_567_89, epsilon = 1e-10));
1502 }
1503
1504 #[rstest]
1505 fn test_from_decimal_trailing_zeros() {
1506 let decimal = dec!(5.670);
1508 assert_eq!(decimal.scale(), 3); let qty = Quantity::from_decimal(decimal).unwrap();
1512 assert_eq!(qty.precision, 3);
1513 assert!(approx_eq!(f64, qty.as_f64(), 5.67, epsilon = 1e-10));
1514
1515 let normalized = decimal.normalize();
1517 assert_eq!(normalized.scale(), 2);
1518 let qty_normalized = Quantity::from_decimal(normalized).unwrap();
1519 assert_eq!(qty_normalized.precision, 2);
1520 }
1521
1522 #[rstest]
1523 #[case("1.00", 2)]
1524 #[case("1.0", 1)]
1525 #[case("1.000", 3)]
1526 #[case("100.00", 2)]
1527 #[case("0.10", 2)]
1528 #[case("0.100", 3)]
1529 fn test_from_str_preserves_trailing_zeros(#[case] input: &str, #[case] expected_precision: u8) {
1530 let qty = Quantity::from_str(input).unwrap();
1531 assert_eq!(qty.precision, expected_precision);
1532 }
1533
1534 #[rstest]
1535 fn test_from_decimal_excessive_precision_inference() {
1536 let decimal = dec!(1.1234567890123456789012345678);
1539
1540 if decimal.scale() > u32::from(FIXED_PRECISION) {
1542 assert!(Quantity::from_decimal(decimal).is_err());
1543 }
1544 }
1545
1546 #[rstest]
1547 fn test_from_decimal_negative_quantity_errors() {
1548 let decimal = dec!(-123.45);
1550 let result = Quantity::from_decimal(decimal);
1551 assert!(result.is_err());
1552
1553 let result = Quantity::from_decimal_dp(decimal, 2);
1555 assert!(result.is_err());
1556 }
1557
1558 #[rstest]
1559 fn test_from_decimal_dp_negative_returns_typed_error_with_stable_display() {
1560 let error = Quantity::from_decimal_dp(dec!(-1.5), 2).unwrap_err();
1561 assert_eq!(
1562 error,
1563 CorrectnessError::PredicateViolation {
1564 message: "Decimal value '-1.5' is negative, Quantity must be non-negative"
1565 .to_string(),
1566 }
1567 );
1568 assert_eq!(
1569 error.to_string(),
1570 "Decimal value '-1.5' is negative, Quantity must be non-negative",
1571 );
1572 }
1573
1574 #[cfg(not(feature = "high-precision"))]
1575 #[rstest]
1576 fn test_from_decimal_dp_rejects_quantity_raw_overflow() {
1577 let error = Quantity::from_decimal_dp(dec!(20_000_000_000), 0).unwrap_err();
1578
1579 assert_eq!(
1580 error,
1581 CorrectnessError::PredicateViolation {
1582 message: format!("Decimal value exceeds QuantityRaw range [0, {QUANTITY_RAW_MAX}]"),
1583 }
1584 );
1585 }
1586
1587 #[cfg(feature = "high-precision")]
1588 #[rstest]
1589 fn test_from_decimal_dp_rejects_value_above_quantity_max() {
1590 let value = 34_028_236_692_094_u64;
1591 let raw = u128::from(value) * FIXED_SCALAR_RAW;
1592 let error = Quantity::from_decimal_dp(Decimal::from(value), 0).unwrap_err();
1593
1594 assert_eq!(
1595 error,
1596 CorrectnessError::PredicateViolation {
1597 message: format!(
1598 "Raw value {raw} exceeds QUANTITY_RAW_MAX={QUANTITY_RAW_MAX} for Quantity"
1599 ),
1600 }
1601 );
1602 }
1603
1604 #[rstest]
1605 fn test_add() {
1606 let a = 1.0;
1607 let b = 2.0;
1608 let quantity1 = Quantity::new(1.0, 0);
1609 let quantity2 = Quantity::new(2.0, 0);
1610 let quantity3 = quantity1 + quantity2;
1611 assert_eq!(quantity3.raw, Quantity::new(a + b, 0).raw);
1612 }
1613
1614 #[rstest]
1615 fn test_sub() {
1616 let a = 3.0;
1617 let b = 2.0;
1618 let quantity1 = Quantity::new(a, 0);
1619 let quantity2 = Quantity::new(b, 0);
1620 let quantity3 = quantity1 - quantity2;
1621 assert_eq!(quantity3.raw, Quantity::new(a - b, 0).raw);
1622 }
1623
1624 #[rstest]
1625 fn test_quantity_checked_add_within_bounds() {
1626 let a = Quantity::new(10.0, 2);
1627 let b = Quantity::new(5.0, 2);
1628 assert_eq!(a.checked_add(b), Some(Quantity::new(15.0, 2)));
1629 }
1630
1631 #[rstest]
1632 fn test_quantity_checked_add_above_max_returns_none() {
1633 let near_max = Quantity::from_raw(QUANTITY_RAW_MAX, 0);
1634 let one = Quantity::new(1.0, 0);
1635 assert_eq!(near_max.checked_add(one), None);
1636 }
1637
1638 #[rstest]
1639 fn test_quantity_checked_sub_within_bounds() {
1640 let a = Quantity::new(10.0, 2);
1641 let b = Quantity::new(3.0, 2);
1642 assert_eq!(a.checked_sub(b), Some(Quantity::new(7.0, 2)));
1643 }
1644
1645 #[rstest]
1646 fn test_quantity_checked_sub_underflow_returns_none() {
1647 let a = Quantity::new(3.0, 2);
1648 let b = Quantity::new(10.0, 2);
1649 assert_eq!(a.checked_sub(b), None);
1650 }
1651
1652 #[rstest]
1653 fn test_quantity_checked_sub_to_zero() {
1654 let a = Quantity::new(5.0, 2);
1655 assert_eq!(a.checked_sub(a), Some(Quantity::zero(2)));
1656 }
1657
1658 #[rstest]
1659 fn test_quantity_from_raw_checked_allows_undef_and_rejects_above_max() {
1660 assert_eq!(
1661 Quantity::from_raw_checked(QUANTITY_UNDEF, 0).unwrap().raw,
1662 QUANTITY_UNDEF
1663 );
1664 assert_eq!(
1665 Quantity::from_raw_checked(QUANTITY_RAW_MAX + 1, 0)
1666 .unwrap_err()
1667 .to_string(),
1668 format!(
1669 "raw value {} exceeds QUANTITY_RAW_MAX={QUANTITY_RAW_MAX}",
1670 QUANTITY_RAW_MAX + 1
1671 )
1672 );
1673 }
1674
1675 #[rstest]
1676 fn test_quantity_as_f64() {
1677 assert_eq!(Quantity::new(2.5, 1).as_f64(), 2.5);
1678 assert_eq!(Quantity::new(0.0, 1).as_f64(), 0.0);
1679 }
1680
1681 #[rstest]
1682 fn test_quantity_deref_yields_raw() {
1683 assert_eq!(*Quantity::from_raw(2_500, 1), 2_500);
1684 }
1685
1686 #[rstest]
1687 fn test_quantity_checked_arith_rejects_undef() {
1688 let undef = Quantity::from_raw(QUANTITY_UNDEF, 0);
1689 let one = Quantity::new(1.0, 0);
1690 assert_eq!(undef.checked_add(one), None);
1691 assert_eq!(one.checked_add(undef), None);
1692 assert_eq!(undef.checked_sub(one), None);
1693 assert_eq!(one.checked_sub(undef), None);
1694 }
1695
1696 #[rstest]
1697 fn test_quantity_checked_add_at_exact_max_returns_some() {
1698 let near_max = Quantity::from_raw(QUANTITY_RAW_MAX - 1, 0);
1699 let one_unit = Quantity::from_raw(1, 0);
1700 assert_eq!(
1701 near_max.checked_add(one_unit),
1702 Some(Quantity::from_raw(QUANTITY_RAW_MAX, 0)),
1703 );
1704 }
1705
1706 #[rstest]
1707 fn test_quantity_checked_arith_uses_max_precision() {
1708 let a = Quantity::new(10.5, 1);
1709 let b = Quantity::new(2.25, 2);
1710 let sum = a.checked_add(b).unwrap();
1711 assert_eq!(sum.precision, 2);
1712 assert_eq!(sum.as_f64(), 12.75);
1713
1714 let diff = a.checked_sub(b).unwrap();
1715 assert_eq!(diff.precision, 2);
1716 assert_eq!(diff.as_f64(), 8.25);
1717 }
1718
1719 #[rstest]
1720 fn test_mul() {
1721 let value = 2.0;
1722 let quantity1 = Quantity::new(value, 1);
1723 let quantity2 = Quantity::new(value, 1);
1724 let quantity3 = quantity1 * quantity2;
1725 assert_eq!(quantity3.raw, Quantity::new(value * value, 0).raw);
1726 }
1727
1728 #[rstest]
1729 fn test_mul_avoids_intermediate_raw_overflow() {
1730 let scalar = FIXED_SCALAR_RAW;
1731 #[cfg(feature = "high-precision")]
1732 let (lhs_raw, rhs_raw, expected_raw) =
1733 (100_000 * scalar, 100 * scalar, 10_000_000 * scalar);
1734 #[cfg(not(feature = "high-precision"))]
1735 let (lhs_raw, rhs_raw, expected_raw) = (
1736 9_000_000_000 * scalar,
1737 2 * scalar + 1,
1738 18_000_000_009 * scalar,
1739 );
1740 let lhs = Quantity::from_raw(lhs_raw, FIXED_PRECISION);
1741 let rhs = Quantity::from_raw(rhs_raw, FIXED_PRECISION);
1742 let result = lhs * rhs;
1743
1744 assert_eq!(lhs_raw.checked_mul(rhs_raw), None);
1745 assert_eq!(result.raw, expected_raw);
1746 assert_eq!(result.precision, FIXED_PRECISION);
1747 }
1748
1749 #[rstest]
1750 #[should_panic(expected = "Overflow occurred when multiplying `Quantity`")]
1751 fn test_mul_panics_when_scaled_result_exceeds_quantity_max() {
1752 let lhs = Quantity::from_raw(QUANTITY_RAW_MAX, FIXED_PRECISION);
1753 let rhs = Quantity::from(2);
1754
1755 let _ = lhs * rhs;
1756 }
1757
1758 #[rstest]
1759 fn test_comparisons() {
1760 assert_eq!(Quantity::new(1.0, 1), Quantity::new(1.0, 1));
1761 assert_eq!(Quantity::new(1.0, 1), Quantity::new(1.0, 2));
1762 assert_ne!(Quantity::new(1.1, 1), Quantity::new(1.0, 1));
1763 assert!(Quantity::new(1.0, 1) <= Quantity::new(1.0, 2));
1764 assert!(Quantity::new(1.1, 1) > Quantity::new(1.0, 1));
1765 assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 1));
1766 assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 2));
1767 assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 2));
1768 assert!(Quantity::new(0.9, 1) < Quantity::new(1.0, 1));
1769 assert!(Quantity::new(0.9, 1) <= Quantity::new(1.0, 2));
1770 assert!(Quantity::new(0.9, 1) <= Quantity::new(1.0, 1));
1771 }
1772
1773 #[rstest]
1774 fn test_debug() {
1775 let quantity = Quantity::from_str("44.12").unwrap();
1776 let result = format!("{quantity:?}");
1777 assert_eq!(result, "Quantity(44.12)");
1778 }
1779
1780 #[rstest]
1781 fn test_display() {
1782 let quantity = Quantity::from_str("44.12").unwrap();
1783 let result = format!("{quantity}");
1784 assert_eq!(result, "44.12");
1785 }
1786
1787 #[rstest]
1788 #[case(44.12, 2, "Quantity(44.12)", "44.12")] #[case(1234.567, 8, "Quantity(1234.56700000)", "1234.56700000")] #[cfg_attr(
1791 feature = "defi",
1792 case(
1793 1_000_000_000_000_000_000.0,
1794 18,
1795 "Quantity(1.000000000000000000)",
1796 "1.000000000000000000"
1797 )
1798 )] fn test_debug_display_precision_handling(
1800 #[case] value: f64,
1801 #[case] precision: u8,
1802 #[case] expected_debug: &str,
1803 #[case] expected_display: &str,
1804 ) {
1805 let quantity = if precision > MAX_FLOAT_PRECISION {
1806 Quantity::from_raw(value as QuantityRaw, precision)
1808 } else {
1809 Quantity::new(value, precision)
1810 };
1811
1812 assert_eq!(format!("{quantity:?}"), expected_debug);
1813 assert_eq!(format!("{quantity}"), expected_display);
1814 }
1815
1816 #[rstest]
1817 fn test_to_formatted_string() {
1818 let qty = Quantity::new(1234.5678, 4);
1819 let formatted = qty.to_formatted_string();
1820 assert_eq!(formatted, "1_234.5678");
1821 assert_eq!(qty.to_string(), "1234.5678");
1822 }
1823
1824 #[rstest]
1825 fn test_saturating_sub() {
1826 let q1 = Quantity::new(100.0, 2);
1827 let q2 = Quantity::new(50.0, 2);
1828 let q3 = Quantity::new(150.0, 2);
1829
1830 let result = q1.saturating_sub(q2);
1831 assert_eq!(result, Quantity::new(50.0, 2));
1832
1833 let result = q1.saturating_sub(q3);
1834 assert_eq!(result, Quantity::zero(2));
1835 assert_eq!(result.raw, 0);
1836 }
1837
1838 #[rstest]
1839 fn test_saturating_sub_overflow_bug() {
1840 use crate::types::fixed::FIXED_PRECISION;
1843 let precision = 3;
1844 let scale = QuantityRaw::from(10u64.pow(u32::from(FIXED_PRECISION - precision)));
1845
1846 let peak_qty = Quantity::from_raw(79 * scale, precision);
1848 let order_qty = Quantity::from_raw(80 * scale, precision);
1849
1850 let result = peak_qty.saturating_sub(order_qty);
1852 assert_eq!(result.raw, 0);
1853 assert_eq!(result, Quantity::zero(precision));
1854 }
1855
1856 #[rstest]
1857 fn test_hash() {
1858 use std::{
1859 collections::hash_map::DefaultHasher,
1860 hash::{Hash, Hasher},
1861 };
1862
1863 let q1 = Quantity::new(100.0, 1);
1864 let q2 = Quantity::new(100.0, 1);
1865 let q3 = Quantity::new(200.0, 1);
1866
1867 let mut s1 = DefaultHasher::new();
1868 let mut s2 = DefaultHasher::new();
1869 let mut s3 = DefaultHasher::new();
1870
1871 q1.hash(&mut s1);
1872 q2.hash(&mut s2);
1873 q3.hash(&mut s3);
1874
1875 assert_eq!(
1876 s1.finish(),
1877 s2.finish(),
1878 "Equal quantities must hash equally"
1879 );
1880 assert_ne!(
1881 s1.finish(),
1882 s3.finish(),
1883 "Different quantities must hash differently"
1884 );
1885 }
1886
1887 #[rstest]
1888 fn test_quantity_serde_json_round_trip() {
1889 let original = Quantity::new(123.456, 3);
1890 let json_str = serde_json::to_string(&original).unwrap();
1891 assert_eq!(json_str, "\"123.456\"");
1892
1893 let deserialized: Quantity = serde_json::from_str(&json_str).unwrap();
1894 assert_eq!(deserialized, original);
1895 assert_eq!(deserialized.precision, 3);
1896 }
1897
1898 #[rstest]
1899 fn test_quantity_serde_json_from_value_round_trip() {
1900 let original = Quantity::new(123.456, 3);
1901 let value = serde_json::to_value(original).unwrap();
1902 assert_eq!(value, serde_json::json!("123.456"));
1903
1904 let deserialized: Quantity = serde_json::from_value(value).unwrap();
1905 assert_eq!(deserialized, original);
1906 assert_eq!(deserialized.precision, 3);
1907 }
1908
1909 #[rstest]
1910 fn test_quantity_deserialize_invalid_string_returns_error() {
1911 let result = serde_json::from_str::<Quantity>("\"not-a-quantity\"");
1912 let error = result.unwrap_err();
1913 assert!(
1914 error.to_string().contains("Error parsing"),
1915 "unexpected message: {error}"
1916 );
1917 }
1918
1919 #[rstest]
1920 fn test_quantity_deserialize_negative_returns_error() {
1921 let result = serde_json::from_str::<Quantity>("\"-1.5\"");
1922 let error = result.unwrap_err();
1923 assert!(
1924 error.to_string().contains("negative"),
1925 "unexpected message: {error}"
1926 );
1927 }
1928
1929 #[rstest]
1930 fn test_from_mantissa_exponent_exact_precision() {
1931 let qty = Quantity::from_mantissa_exponent(12345, -2, 2);
1932 assert_eq!(qty.as_f64(), 123.45);
1933 }
1934
1935 #[rstest]
1936 fn test_from_mantissa_exponent_excess_rounds_down() {
1937 let qty = Quantity::from_mantissa_exponent(12345, -3, 2);
1940 assert_eq!(qty.as_f64(), 12.34);
1941 }
1942
1943 #[rstest]
1944 fn test_from_mantissa_exponent_excess_rounds_up() {
1945 let qty = Quantity::from_mantissa_exponent(12355, -3, 2);
1947 assert_eq!(qty.as_f64(), 12.36);
1948 }
1949
1950 #[rstest]
1951 fn test_from_mantissa_exponent_positive_exponent() {
1952 let qty = Quantity::from_mantissa_exponent(5, 2, 0);
1953 assert_eq!(qty.as_f64(), 500.0);
1954 }
1955
1956 #[rstest]
1957 fn test_from_mantissa_exponent_zero() {
1958 let qty = Quantity::from_mantissa_exponent(0, 2, 2);
1959 assert_eq!(qty.as_f64(), 0.0);
1960 }
1961
1962 #[cfg(feature = "high-precision")]
1963 #[rstest]
1964 fn test_max_raw_precision_16_string_round_trip() {
1965 let quantity = Quantity::from_raw(QUANTITY_RAW_MAX, 16);
1966
1967 let decoded = quantity.to_string().parse::<Quantity>().unwrap();
1968
1969 assert_eq!(decoded, quantity);
1970 assert_eq!(decoded.raw, QUANTITY_RAW_MAX);
1971 assert_eq!(decoded.precision, 16);
1972 }
1973
1974 #[cfg(all(feature = "defi", feature = "high-precision"))]
1975 #[rstest]
1976 fn test_wei_above_decimal_mantissa_formats_exactly() {
1977 let raw = 80_000_000_000_000_000_250_000_000_000_u128;
1978 let qty = Quantity::from_raw(raw, 18);
1979
1980 assert_eq!(qty.to_string(), "80000000000.000000250000000000");
1981 }
1982
1983 #[rstest]
1984 fn test_from_mantissa_exponent_checked_exact_precision() {
1985 let qty = Quantity::from_mantissa_exponent_checked(12345, -2, 2).unwrap();
1986 assert_eq!(qty.as_decimal(), dec!(123.45));
1987 }
1988
1989 #[rstest]
1990 fn test_from_mantissa_exponent_checked_zero_with_large_exponent() {
1991 let qty = Quantity::from_mantissa_exponent_checked(0, 119, 2).unwrap();
1992 assert_eq!(qty.as_decimal(), dec!(0.00));
1993 }
1994
1995 #[rstest]
1996 fn test_from_mantissa_exponent_checked_invalid_precision() {
1997 #[cfg(feature = "defi")]
1998 let invalid_precision = crate::defi::WEI_PRECISION + 1;
1999 #[cfg(not(feature = "defi"))]
2000 let invalid_precision = FIXED_PRECISION + 1;
2001
2002 let error = Quantity::from_mantissa_exponent_checked(1, 0, invalid_precision).unwrap_err();
2003 assert!(error.to_string().contains("`precision` exceeded maximum"));
2004 }
2005
2006 #[rstest]
2007 fn test_from_mantissa_exponent_checked_overflow_returns_error() {
2008 let error = Quantity::from_mantissa_exponent_checked(u64::MAX, 100, 0).unwrap_err();
2009 assert!(
2010 error
2011 .to_string()
2012 .contains("Overflow in Quantity::from_mantissa_exponent")
2013 );
2014 }
2015
2016 #[rstest]
2017 #[should_panic(expected = "Quantity::from_mantissa_exponent")]
2018 fn test_from_mantissa_exponent_overflow_panics() {
2019 let _ = Quantity::from_mantissa_exponent(u64::MAX, 9, 0);
2020 }
2021
2022 #[rstest]
2023 #[should_panic(expected = "exceeds i128 range")]
2024 fn test_from_mantissa_exponent_large_exponent_panics() {
2025 let _ = Quantity::from_mantissa_exponent(1, 119, 0);
2026 }
2027
2028 #[rstest]
2029 fn test_from_mantissa_exponent_zero_with_large_exponent() {
2030 let qty = Quantity::from_mantissa_exponent(0, 119, 0);
2031 assert_eq!(qty.as_f64(), 0.0);
2032 }
2033
2034 #[rstest]
2035 fn test_from_mantissa_exponent_very_negative_exponent_rounds_to_zero() {
2036 let qty = Quantity::from_mantissa_exponent(12345, -120, 2);
2037 assert_eq!(qty.as_f64(), 0.0);
2038 }
2039
2040 #[rstest]
2041 fn test_f64_operations() {
2042 let q = Quantity::new(10.5, 2);
2043 assert_eq!(q + 1.0, 11.5);
2044 assert_eq!(q - 1.0, 9.5);
2045 assert_eq!(q * 2.0, 21.0);
2046 assert_eq!(q / 2.0, 5.25);
2047 }
2048
2049 #[rstest]
2050 fn test_decimal_arithmetic_operations() {
2051 let qty = Quantity::new(100.0, 2);
2052 assert_eq!(qty + dec!(50.25), dec!(150.25));
2053 assert_eq!(qty - dec!(30.50), dec!(69.50));
2054 assert_eq!(qty * dec!(1.5), dec!(150.00));
2055 assert_eq!(qty / dec!(4), dec!(25.00));
2056 }
2057
2058 #[rstest]
2062 #[cfg(feature = "defi")]
2063 #[case::sell_tx_rain_amount(
2064 U256::from_str_radix("42193532365637161405123", 10).unwrap(),
2065 18,
2066 "42193.532365637161405123"
2067 )]
2068 #[case::sell_tx_weth_amount(
2069 U256::from_str_radix("112633187203033110", 10).unwrap(),
2070 18,
2071 "0.112633187203033110"
2072 )]
2073 fn test_from_u256_real_swap_data(
2074 #[case] amount: U256,
2075 #[case] precision: u8,
2076 #[case] expected_str: &str,
2077 ) {
2078 let qty = Quantity::from_u256(amount, precision).unwrap();
2079 assert_eq!(qty.precision, precision);
2080 assert_eq!(qty.as_decimal().to_string(), expected_str);
2081 }
2082
2083 #[rstest]
2084 #[cfg(feature = "defi")]
2085 fn test_from_u256_overflow_returns_typed_error_with_stable_display() {
2086 let error = Quantity::from_u256(U256::MAX, 0).unwrap_err();
2087 match error {
2088 CorrectnessError::PredicateViolation { ref message } => {
2089 assert!(
2090 message.contains("Amount overflow during scaling to fixed precision"),
2091 "unexpected message: {message:?}",
2092 );
2093 }
2094 _ => panic!("expected PredicateViolation, was {error:?}"),
2095 }
2096 }
2097
2098 #[rstest]
2099 #[cfg(feature = "defi")]
2100 fn test_from_u256_rejects_amount_above_quantity_raw_range() {
2101 let amount = U256::from(u128::MAX) + U256::from(1_u8);
2102 let error = Quantity::from_u256(amount, 18).unwrap_err();
2103
2104 assert_eq!(
2105 error,
2106 CorrectnessError::PredicateViolation {
2107 message: format!("U256 scaled amount {amount} exceeds QuantityRaw range"),
2108 }
2109 );
2110 }
2111
2112 #[rstest]
2113 #[cfg(feature = "defi")]
2114 fn test_from_u256_invalid_precision_returns_typed_error() {
2115 let error = Quantity::from_u256(U256::from(1u8), 19).unwrap_err();
2116 match error {
2117 CorrectnessError::PredicateViolation { ref message } => {
2118 assert!(
2119 message.contains("WEI_PRECISION"),
2120 "unexpected message: {message:?}",
2121 );
2122 }
2123 _ => panic!("expected PredicateViolation, was {error:?}"),
2124 }
2125 }
2126
2127 #[rstest]
2128 #[cfg(feature = "defi")]
2129 fn test_from_u256_raw_above_max_returns_typed_error() {
2130 let raw = QUANTITY_RAW_MAX + 1;
2133 let error = Quantity::from_u256(U256::from(raw), FIXED_PRECISION).unwrap_err();
2134 match error {
2135 CorrectnessError::PredicateViolation { ref message } => {
2136 assert!(
2137 message.contains("QUANTITY_RAW_MAX"),
2138 "unexpected message: {message:?}",
2139 );
2140 }
2141 _ => panic!("expected PredicateViolation, was {error:?}"),
2142 }
2143 }
2144}
2145
2146#[cfg(test)]
2147mod property_tests {
2148 use proptest::prelude::*;
2149 use rstest::rstest;
2150
2151 use super::*;
2152 #[cfg(not(feature = "defi"))]
2153 use crate::types::fixed::MAX_FLOAT_PRECISION;
2154
2155 fn quantity_value_strategy() -> impl Strategy<Value = f64> {
2157 prop_oneof![
2159 0.00001..1.0,
2161 1.0..100_000.0,
2163 100_000.0..1_000_000.0,
2165 Just(0.0),
2167 Just(QUANTITY_MAX / 2.0),
2169 ]
2170 }
2171
2172 fn precision_strategy() -> impl Strategy<Value = u8> {
2174 let upper = FIXED_PRECISION.min(MAX_FLOAT_PRECISION);
2175 prop_oneof![Just(0u8), 0u8..=upper, Just(FIXED_PRECISION),]
2176 }
2177
2178 fn precision_strategy_non_zero() -> impl Strategy<Value = u8> {
2179 let upper = FIXED_PRECISION.clamp(1, MAX_FLOAT_PRECISION);
2180 prop_oneof![Just(upper), Just(FIXED_PRECISION.max(1)), 1u8..=upper,]
2181 }
2182
2183 fn raw_for_precision_strategy() -> impl Strategy<Value = (QuantityRaw, u8)> {
2184 precision_strategy().prop_flat_map(|precision| {
2185 let step_u128 = 10u128.pow(u32::from(FIXED_PRECISION.saturating_sub(precision)));
2186 #[cfg(feature = "high-precision")]
2187 let max_steps_u128 = QUANTITY_RAW_MAX / step_u128;
2188 #[cfg(not(feature = "high-precision"))]
2189 let max_steps_u128 = u128::from(QUANTITY_RAW_MAX) / step_u128;
2190
2191 (0u128..=max_steps_u128).prop_map(move |steps_u128| {
2192 let raw_u128 = steps_u128 * step_u128;
2193 #[cfg(feature = "high-precision")]
2194 let raw = raw_u128;
2195 #[cfg(not(feature = "high-precision"))]
2196 let raw = raw_u128
2197 .try_into()
2198 .expect("raw value should fit in QuantityRaw");
2199 (raw, precision)
2200 })
2201 })
2202 }
2203
2204 const DECIMAL_MAX_MANTISSA: u128 = 79_228_162_514_264_337_593_543_950_335;
2205
2206 fn decimal_compatible(raw: QuantityRaw, precision: u8) -> bool {
2207 if precision > MAX_FLOAT_PRECISION {
2208 return false;
2209 }
2210 let precision_diff = u32::from(FIXED_PRECISION.saturating_sub(precision));
2211 let divisor = 10u128.pow(precision_diff);
2212 #[cfg(feature = "high-precision")]
2213 let rescaled_raw = raw / divisor;
2214 #[cfg(not(feature = "high-precision"))]
2215 let rescaled_raw = u128::from(raw) / divisor;
2216 rescaled_raw <= DECIMAL_MAX_MANTISSA
2219 }
2220
2221 proptest! {
2222 #[rstest]
2224 fn prop_quantity_serde_round_trip(
2225 (raw, precision) in raw_for_precision_strategy()
2226 ) {
2227 let original = Quantity::from_raw(raw, precision);
2228
2229 let string_repr = original.to_string();
2231 let from_string: Quantity = string_repr.parse().unwrap();
2232 prop_assert_eq!(from_string.raw, original.raw);
2233 prop_assert_eq!(from_string.precision, original.precision);
2234
2235 let json = serde_json::to_string(&original).unwrap();
2237 let from_json: Quantity = serde_json::from_str(&json).unwrap();
2238 prop_assert_eq!(from_json.precision, original.precision);
2239 prop_assert_eq!(from_json.raw, original.raw);
2240 }
2241
2242 #[rstest]
2244 fn prop_quantity_arithmetic_associative(
2245 a in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
2246 b in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
2247 c in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
2248 precision in precision_strategy()
2249 ) {
2250 let q_a = Quantity::new(a, precision);
2251 let q_b = Quantity::new(b, precision);
2252 let q_c = Quantity::new(c, precision);
2253
2254 let expected = q_a
2255 .raw
2256 .checked_add(q_b.raw)
2257 .and_then(|sum| sum.checked_add(q_c.raw))
2258 .filter(|sum| *sum <= QUANTITY_RAW_MAX);
2259
2260 if let Some(expected) = expected {
2261 let left = (q_a + q_b) + q_c;
2262 let right = q_a + (q_b + q_c);
2263 prop_assert_eq!(left.raw, expected);
2264 prop_assert_eq!(right.raw, expected);
2265 }
2266 }
2267
2268 #[rstest]
2270 fn prop_quantity_addition_subtraction_inverse(
2271 base in quantity_value_strategy().prop_filter("Reasonable values", |&x| x < 1e6),
2272 delta in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
2273 precision in precision_strategy()
2274 ) {
2275 let q_base = Quantity::new(base, precision);
2276 let q_delta = Quantity::new(delta, precision);
2277
2278 let expected = q_base
2279 .raw
2280 .checked_add(q_delta.raw)
2281 .filter(|sum| *sum <= QUANTITY_RAW_MAX);
2282
2283 if expected.is_some() {
2284 prop_assert_eq!((q_base + q_delta) - q_delta, q_base);
2285 }
2286 }
2287
2288 #[rstest]
2291 fn prop_quantity_checked_add_matches_spec(
2292 a in quantity_value_strategy(),
2293 b in quantity_value_strategy(),
2294 precision in precision_strategy()
2295 ) {
2296 let q_a = Quantity::new(a, precision);
2297 let q_b = Quantity::new(b, precision);
2298 let expected = q_a.raw
2299 .checked_add(q_b.raw)
2300 .filter(|r| *r <= QUANTITY_RAW_MAX)
2301 .filter(|_| q_a.raw != QUANTITY_UNDEF && q_b.raw != QUANTITY_UNDEF)
2302 .map(|raw| Quantity { raw, precision: q_a.precision.max(q_b.precision) });
2303 prop_assert_eq!(q_a.checked_add(q_b), expected);
2304 }
2305
2306 #[rstest]
2309 fn prop_quantity_checked_sub_matches_spec(
2310 a in quantity_value_strategy(),
2311 b in quantity_value_strategy(),
2312 precision in precision_strategy()
2313 ) {
2314 let q_a = Quantity::new(a, precision);
2315 let q_b = Quantity::new(b, precision);
2316 let expected = q_a.raw
2317 .checked_sub(q_b.raw)
2318 .filter(|_| q_a.raw != QUANTITY_UNDEF && q_b.raw != QUANTITY_UNDEF)
2319 .map(|raw| Quantity { raw, precision: q_a.precision.max(q_b.precision) });
2320 prop_assert_eq!(q_a.checked_sub(q_b), expected);
2321 }
2322
2323 #[rstest]
2325 fn prop_quantity_ordering_transitive(
2326 a in quantity_value_strategy(),
2327 b in quantity_value_strategy(),
2328 c in quantity_value_strategy(),
2329 precision in precision_strategy()
2330 ) {
2331 let q_a = Quantity::new(a, precision);
2332 let q_b = Quantity::new(b, precision);
2333 let q_c = Quantity::new(c, precision);
2334
2335 if q_a <= q_b && q_b <= q_c {
2337 prop_assert!(q_a <= q_c, "Transitivity failed: {} <= {} <= {} but {} > {}",
2338 q_a.as_f64(), q_b.as_f64(), q_c.as_f64(), q_a.as_f64(), q_c.as_f64());
2339 }
2340 }
2341
2342 #[rstest]
2344 fn prop_quantity_string_parsing_precision(
2345 integral in 0u32..1_000_000,
2346 fractional in 0u32..1_000_000,
2347 precision in precision_strategy_non_zero()
2348 ) {
2349 let pow = 10u128.pow(u32::from(precision));
2351 let fractional_mod = u128::from(fractional) % pow;
2352 let fractional_str = format!("{:0width$}", fractional_mod, width = precision as usize);
2353 let quantity_str = format!("{integral}.{fractional_str}");
2354
2355 let parsed: Quantity = quantity_str.parse().unwrap();
2356 prop_assert_eq!(parsed.precision, precision);
2357
2358 let round_trip = parsed.to_string();
2360 let expected_value = format!("{integral}.{fractional_str}");
2361 prop_assert_eq!(round_trip, expected_value);
2362 }
2363
2364 #[rstest]
2366 fn prop_quantity_arithmetic_bounds(
2367 a in quantity_value_strategy(),
2368 b in quantity_value_strategy(),
2369 precision in precision_strategy()
2370 ) {
2371 let q_a = Quantity::new(a, precision);
2372 let q_b = Quantity::new(b, precision);
2373
2374 let sum_f64 = q_a.as_f64() + q_b.as_f64();
2376 if sum_f64.is_finite() && (QUANTITY_MIN..=QUANTITY_MAX).contains(&sum_f64) {
2377 let sum = q_a + q_b;
2378 prop_assert!(sum.as_f64().is_finite());
2379 prop_assert!(!sum.is_undefined());
2380 }
2381
2382 let diff_f64 = q_a.as_f64() - q_b.as_f64();
2384 if diff_f64.is_finite() && (QUANTITY_MIN..=QUANTITY_MAX).contains(&diff_f64) {
2385 let diff = q_a - q_b;
2386 prop_assert!(diff.as_f64().is_finite());
2387 prop_assert!(!diff.is_undefined());
2388 }
2389 }
2390
2391 #[rstest]
2393 fn prop_quantity_multiplication_non_negative(
2394 a in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 0.0 && x < 10.0),
2395 b in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 0.0 && x < 10.0),
2396 precision in precision_strategy()
2397 ) {
2398 let q_a = Quantity::new(a, precision);
2399 let q_b = Quantity::new(b, precision);
2400
2401 let raw_product_check = q_a.raw.checked_mul(q_b.raw);
2403
2404 if let Some(raw_product) = raw_product_check {
2405 let scaled_raw = raw_product / FIXED_SCALAR_RAW;
2407 if scaled_raw <= QUANTITY_RAW_MAX {
2408 let product = q_a * q_b;
2410 prop_assert!(product.as_f64() >= 0.0, "Quantity multiplication produced negative value: {}", product.as_f64());
2411 }
2412 }
2413 }
2414
2415 #[rstest]
2417 fn prop_quantity_zero_addition_identity(
2418 value in quantity_value_strategy(),
2419 precision in precision_strategy()
2420 ) {
2421 let q = Quantity::new(value, precision);
2422 let zero = Quantity::zero(precision);
2423
2424 prop_assert_eq!(q + zero, q);
2426 prop_assert_eq!(zero + q, q);
2427 }
2428 }
2429
2430 proptest! {
2431 #[rstest]
2433 fn prop_quantity_as_decimal_preserves_precision(
2434 (raw, precision) in raw_for_precision_strategy()
2435 ) {
2436 prop_assume!(decimal_compatible(raw, precision));
2437 let quantity = Quantity::from_raw(raw, precision);
2438 let decimal = quantity.as_decimal();
2439 prop_assert_eq!(decimal.scale(), u32::from(precision));
2440 }
2441
2442 #[rstest]
2444 fn prop_quantity_as_decimal_matches_display(
2445 (raw, precision) in raw_for_precision_strategy()
2446 ) {
2447 prop_assume!(decimal_compatible(raw, precision));
2448 let quantity = Quantity::from_raw(raw, precision);
2449 let display_str = format!("{quantity}");
2450 let decimal_str = quantity.as_decimal().to_string();
2451 prop_assert_eq!(display_str, decimal_str);
2452 }
2453
2454 #[rstest]
2456 fn prop_quantity_from_decimal_roundtrip(
2457 (raw, precision) in raw_for_precision_strategy()
2458 ) {
2459 prop_assume!(decimal_compatible(raw, precision));
2460 let original = Quantity::from_raw(raw, precision);
2461 let decimal = original.as_decimal();
2462 let reconstructed = Quantity::from_decimal(decimal).unwrap();
2463 prop_assert_eq!(original.raw, reconstructed.raw);
2464 prop_assert_eq!(original.precision, reconstructed.precision);
2465 }
2466
2467 #[rstest]
2469 fn prop_quantity_from_raw_round_trip(
2470 (raw, precision) in raw_for_precision_strategy()
2471 ) {
2472 let quantity = Quantity::from_raw(raw, precision);
2473 prop_assert_eq!(quantity.raw, raw);
2474 prop_assert_eq!(quantity.precision, precision);
2475 }
2476 }
2477}