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