1use std::{
43 cmp::Ordering,
44 fmt::{Debug, Display},
45 hash::{Hash, Hasher},
46 ops::{Add, Deref, Div, Mul, Sub},
47 str::FromStr,
48};
49
50#[cfg(feature = "defi")]
51use alloy_primitives::U256;
52use nautilus_core::{
53 correctness::{
54 CorrectnessError, CorrectnessResult, CorrectnessResultExt, FAILED,
55 check_in_range_inclusive_f64, check_predicate_true,
56 },
57 string::formatting::Separable,
58};
59use rust_decimal::Decimal;
60use serde::{Deserialize, Deserializer, Serialize};
61
62use super::fixed::{
63 FIXED_PRECISION, FIXED_SCALAR, MAX_FLOAT_PRECISION, check_fixed_precision,
64 mantissa_exponent_to_fixed_i128, mantissa_exponent_to_raw_checked, raw_scales_match,
65};
66#[cfg(not(feature = "high-precision"))]
67use super::fixed::{f64_to_fixed_u64, fixed_u64_to_f64};
68#[cfg(feature = "high-precision")]
69use super::fixed::{f64_to_fixed_u128, fixed_u128_to_f64};
70
71#[cfg(feature = "high-precision")]
76pub type QuantityRaw = u128;
77
78#[cfg(not(feature = "high-precision"))]
79pub type QuantityRaw = u64;
80
81#[unsafe(no_mangle)]
90#[allow(unsafe_code)]
91pub static QUANTITY_RAW_MAX: QuantityRaw =
92 (QUANTITY_MAX as QuantityRaw) * (FIXED_SCALAR as QuantityRaw);
93
94pub const QUANTITY_UNDEF: QuantityRaw = QuantityRaw::MAX;
96
97#[cfg(feature = "high-precision")]
102pub const QUANTITY_MAX: f64 = 34_028_236_692_093.0;
104
105#[cfg(not(feature = "high-precision"))]
106pub const QUANTITY_MAX: f64 = 18_446_744_073.0;
108
109pub const QUANTITY_MIN: f64 = 0.0;
113
114#[repr(C)]
125#[derive(Clone, Copy, Default, Eq)]
126#[cfg_attr(
127 feature = "python",
128 pyo3::pyclass(
129 module = "nautilus_trader.core.nautilus_pyo3.model",
130 frozen,
131 from_py_object
132 )
133)]
134#[cfg_attr(
135 feature = "python",
136 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
137)]
138pub struct Quantity {
139 pub raw: QuantityRaw,
141 pub precision: u8,
143}
144
145impl Quantity {
146 pub fn new_checked(value: f64, precision: u8) -> CorrectnessResult<Self> {
158 check_in_range_inclusive_f64(value, QUANTITY_MIN, QUANTITY_MAX, "value")?;
159
160 #[cfg(feature = "defi")]
161 if precision > MAX_FLOAT_PRECISION {
162 return Err(CorrectnessError::PredicateViolation {
164 message: format!(
165 "`precision` exceeded maximum float precision ({MAX_FLOAT_PRECISION}), use `Quantity::from_wei()` for wei values instead"
166 ),
167 });
168 }
169
170 check_fixed_precision(precision)?;
171
172 #[cfg(feature = "high-precision")]
173 let raw = f64_to_fixed_u128(value, precision);
174 #[cfg(not(feature = "high-precision"))]
175 let raw = f64_to_fixed_u64(value, precision);
176
177 Ok(Self { raw, precision })
178 }
179
180 pub fn non_zero_checked(value: f64, precision: u8) -> CorrectnessResult<Self> {
194 check_predicate_true(value != 0.0, "value was zero")?;
195 check_fixed_precision(precision)?;
196 let rounded_value = (value * 10.0_f64.powi(i32::from(precision))).round()
197 / 10.0_f64.powi(i32::from(precision));
198 check_predicate_true(
199 rounded_value != 0.0,
200 &format!("value {value} was zero after rounding to precision {precision}"),
201 )?;
202
203 Self::new_checked(value, precision)
204 }
205
206 #[must_use]
212 pub fn new(value: f64, precision: u8) -> Self {
213 Self::new_checked(value, precision).expect_display(FAILED)
214 }
215
216 #[must_use]
222 pub fn non_zero(value: f64, precision: u8) -> Self {
223 Self::non_zero_checked(value, precision).expect_display(FAILED)
224 }
225
226 #[must_use]
233 pub fn from_raw(raw: QuantityRaw, precision: u8) -> Self {
234 assert!(
235 raw == QUANTITY_UNDEF || raw <= QUANTITY_RAW_MAX,
236 "`raw` value {raw} exceeds QUANTITY_RAW_MAX={QUANTITY_RAW_MAX} for Quantity"
237 );
238
239 if raw == QUANTITY_UNDEF {
240 assert!(
241 precision == 0,
242 "`precision` must be 0 when `raw` is QUANTITY_UNDEF"
243 );
244 }
245 check_fixed_precision(precision).expect_display(FAILED);
246
247 Self { raw, precision }
256 }
257
258 pub fn from_raw_checked(raw: QuantityRaw, precision: u8) -> CorrectnessResult<Self> {
268 if raw == QUANTITY_UNDEF && precision != 0 {
269 return Err(CorrectnessError::PredicateViolation {
270 message: "`precision` must be 0 when `raw` is QUANTITY_UNDEF".to_string(),
271 });
272 }
273
274 if raw != QUANTITY_UNDEF && raw > QUANTITY_RAW_MAX {
275 return Err(CorrectnessError::PredicateViolation {
276 message: format!("raw value {raw} exceeds QUANTITY_RAW_MAX={QUANTITY_RAW_MAX}"),
277 });
278 }
279
280 check_fixed_precision(precision)?;
281
282 Ok(Self { raw, precision })
283 }
284
285 #[must_use]
292 pub fn checked_add(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 let raw = self.raw.checked_add(rhs.raw)?;
301 if raw > QUANTITY_RAW_MAX {
302 return None;
303 }
304 Some(Self {
305 raw,
306 precision: self.precision.max(rhs.precision),
307 })
308 }
309
310 #[must_use]
316 pub fn checked_sub(self, rhs: Self) -> Option<Self> {
317 if self.raw == QUANTITY_UNDEF || rhs.raw == QUANTITY_UNDEF {
318 return None;
319 }
320
321 if !raw_scales_match(self.precision, rhs.precision) {
322 return None;
323 }
324 let raw = self.raw.checked_sub(rhs.raw)?;
325 Some(Self {
326 raw,
327 precision: self.precision.max(rhs.precision),
328 })
329 }
330
331 #[must_use]
336 pub fn saturating_sub(self, rhs: Self) -> Self {
337 let precision = self.precision.max(rhs.precision);
338 let raw = self.raw.saturating_sub(rhs.raw);
339 if raw == 0 && self.raw < rhs.raw {
340 log::warn!(
341 "Saturating Quantity subtraction: {self} - {rhs} < 0, clamped to 0 (precision={precision})"
342 );
343 }
344
345 Self { raw, precision }
346 }
347
348 #[must_use]
354 pub fn zero(precision: u8) -> Self {
355 check_fixed_precision(precision).expect_display(FAILED);
356 Self::new(0.0, precision)
357 }
358
359 #[must_use]
361 pub fn is_undefined(&self) -> bool {
362 self.raw == QUANTITY_UNDEF
363 }
364
365 #[must_use]
367 pub fn is_zero(&self) -> bool {
368 self.raw == 0
369 }
370
371 #[must_use]
373 pub fn is_positive(&self) -> bool {
374 self.raw != QUANTITY_UNDEF && self.raw > 0
375 }
376
377 #[cfg(feature = "high-precision")]
378 #[must_use]
384 pub fn as_f64(&self) -> f64 {
385 #[cfg(feature = "defi")]
386 assert!(
387 self.precision <= MAX_FLOAT_PRECISION,
388 "Invalid f64 conversion beyond `MAX_FLOAT_PRECISION` (16)"
389 );
390
391 fixed_u128_to_f64(self.raw)
392 }
393
394 #[cfg(not(feature = "high-precision"))]
395 #[must_use]
401 pub fn as_f64(&self) -> f64 {
402 #[cfg(feature = "defi")]
403 if self.precision > MAX_FLOAT_PRECISION {
404 panic!("Invalid f64 conversion beyond `MAX_FLOAT_PRECISION` (16)");
405 }
406
407 fixed_u64_to_f64(self.raw)
408 }
409
410 #[must_use]
412 pub fn as_decimal(&self) -> Decimal {
413 let precision_diff = FIXED_PRECISION.saturating_sub(self.precision);
415 let rescaled_raw = self.raw / QuantityRaw::pow(10, u32::from(precision_diff));
416
417 #[allow(
421 clippy::unnecessary_cast,
422 clippy::cast_lossless,
423 reason = "cast is real when QuantityRaw is u64, no-op when u128"
424 )]
425 Decimal::from_i128_with_scale(rescaled_raw as i128, u32::from(self.precision))
426 }
427
428 #[must_use]
430 pub fn to_formatted_string(&self) -> String {
431 format!("{self}").separate_with_underscores()
432 }
433
434 pub fn from_decimal_dp(decimal: Decimal, precision: u8) -> CorrectnessResult<Self> {
447 if decimal.mantissa() < 0 {
448 return Err(CorrectnessError::PredicateViolation {
449 message: format!(
450 "Decimal value '{decimal}' is negative, Quantity must be non-negative"
451 ),
452 });
453 }
454
455 let exponent = -(decimal.scale() as i8);
456 let raw_i128 = mantissa_exponent_to_fixed_i128(decimal.mantissa(), exponent, precision)?;
457
458 let raw: QuantityRaw =
459 raw_i128
460 .try_into()
461 .map_err(|_| CorrectnessError::PredicateViolation {
462 message: format!(
463 "Decimal value exceeds QuantityRaw range [0, {QUANTITY_RAW_MAX}]"
464 ),
465 })?;
466
467 if raw > QUANTITY_RAW_MAX {
468 return Err(CorrectnessError::PredicateViolation {
469 message: format!(
470 "Raw value {raw} exceeds QUANTITY_RAW_MAX={QUANTITY_RAW_MAX} for Quantity"
471 ),
472 });
473 }
474
475 Ok(Self { raw, precision })
476 }
477
478 pub fn from_decimal(decimal: Decimal) -> CorrectnessResult<Self> {
490 let precision = decimal.scale() as u8;
491 Self::from_decimal_dp(decimal, precision)
492 }
493
494 #[must_use]
503 pub fn from_mantissa_exponent(mantissa: u64, exponent: i8, precision: u8) -> Self {
504 check_fixed_precision(precision).expect_display(FAILED);
505
506 if mantissa == 0 {
507 return Self { raw: 0, precision };
508 }
509
510 let raw_i128 = mantissa_exponent_to_fixed_i128(i128::from(mantissa), exponent, precision)
511 .expect("Overflow in Quantity::from_mantissa_exponent");
512
513 let raw: QuantityRaw = raw_i128
514 .try_into()
515 .expect("Raw value exceeds QuantityRaw range in Quantity::from_mantissa_exponent");
516 assert!(
517 raw <= QUANTITY_RAW_MAX,
518 "`raw` value {raw} exceeded QUANTITY_RAW_MAX={QUANTITY_RAW_MAX} for Quantity"
519 );
520
521 Self { raw, precision }
522 }
523
524 pub fn from_mantissa_exponent_checked(
531 mantissa: u64,
532 exponent: i8,
533 precision: u8,
534 ) -> CorrectnessResult<Self> {
535 let raw = mantissa_exponent_to_raw_checked::<QuantityRaw>(
536 i128::from(mantissa),
537 exponent,
538 precision,
539 "Quantity::from_mantissa_exponent",
540 "QuantityRaw",
541 "Quantity",
542 )?;
543
544 Self::from_raw_checked(raw, precision)
545 }
546
547 #[cfg(feature = "defi")]
555 pub fn from_u256(amount: U256, precision: u8) -> CorrectnessResult<Self> {
556 let scaled_amount = if precision < FIXED_PRECISION {
558 amount
559 .checked_mul(U256::from(
560 10u128.pow(u32::from(FIXED_PRECISION - precision)),
561 ))
562 .ok_or_else(|| CorrectnessError::PredicateViolation {
563 message: format!(
564 "Amount overflow during scaling to fixed precision: {} * 10^{}",
565 amount,
566 FIXED_PRECISION - precision
567 ),
568 })?
569 } else {
570 amount
571 };
572
573 let raw = QuantityRaw::try_from(scaled_amount).map_err(|_| {
574 CorrectnessError::PredicateViolation {
575 message: format!("U256 scaled amount {scaled_amount} exceeds QuantityRaw range"),
576 }
577 })?;
578
579 Self::from_raw_checked(raw, precision)
580 }
581}
582
583impl From<Quantity> for f64 {
584 fn from(qty: Quantity) -> Self {
585 qty.as_f64()
586 }
587}
588
589impl From<&Quantity> for f64 {
590 fn from(qty: &Quantity) -> Self {
591 qty.as_f64()
592 }
593}
594
595impl From<i32> for Quantity {
596 fn from(value: i32) -> Self {
602 assert!(
603 value >= 0,
604 "Cannot create Quantity from negative i32: {value}. Use u32 or check value is non-negative."
605 );
606 Self::new(f64::from(value), 0)
607 }
608}
609
610impl From<i64> for Quantity {
611 fn from(value: i64) -> Self {
617 assert!(
618 value >= 0,
619 "Cannot create Quantity from negative i64: {value}. Use u64 or check value is non-negative."
620 );
621 Self::new(value as f64, 0)
622 }
623}
624
625impl From<u32> for Quantity {
626 fn from(value: u32) -> Self {
627 Self::new(f64::from(value), 0)
628 }
629}
630
631impl From<u64> for Quantity {
632 fn from(value: u64) -> Self {
633 Self::new(value as f64, 0)
634 }
635}
636
637impl Hash for Quantity {
638 fn hash<H: Hasher>(&self, state: &mut H) {
639 self.raw.hash(state);
640 }
641}
642
643impl PartialEq for Quantity {
644 fn eq(&self, other: &Self) -> bool {
645 self.raw == other.raw
646 }
647}
648
649impl PartialOrd for Quantity {
650 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
651 Some(self.cmp(other))
652 }
653
654 fn lt(&self, other: &Self) -> bool {
655 self.raw.lt(&other.raw)
656 }
657
658 fn le(&self, other: &Self) -> bool {
659 self.raw.le(&other.raw)
660 }
661
662 fn gt(&self, other: &Self) -> bool {
663 self.raw.gt(&other.raw)
664 }
665
666 fn ge(&self, other: &Self) -> bool {
667 self.raw.ge(&other.raw)
668 }
669}
670
671impl Ord for Quantity {
672 fn cmp(&self, other: &Self) -> Ordering {
673 self.raw.cmp(&other.raw)
674 }
675}
676
677impl Deref for Quantity {
678 type Target = QuantityRaw;
679
680 fn deref(&self) -> &Self::Target {
681 &self.raw
682 }
683}
684
685impl Add for Quantity {
686 type Output = Self;
687 fn add(self, rhs: Self) -> Self::Output {
688 Self {
689 raw: self
690 .raw
691 .checked_add(rhs.raw)
692 .expect("Overflow occurred when adding `Quantity`"),
693 precision: self.precision.max(rhs.precision),
694 }
695 }
696}
697
698impl Sub for Quantity {
699 type Output = Self;
700 fn sub(self, rhs: Self) -> Self::Output {
701 Self {
702 raw: self
703 .raw
704 .checked_sub(rhs.raw)
705 .expect("Underflow occurred when subtracting `Quantity`"),
706 precision: self.precision.max(rhs.precision),
707 }
708 }
709}
710
711#[expect(
712 clippy::suspicious_arithmetic_impl,
713 reason = "Can use division to scale back"
714)]
715impl Mul for Quantity {
716 type Output = Self;
717 fn mul(self, rhs: Self) -> Self::Output {
718 let result_raw = self
719 .raw
720 .checked_mul(rhs.raw)
721 .expect("Overflow occurred when multiplying `Quantity`");
722
723 Self {
724 raw: result_raw / (FIXED_SCALAR as QuantityRaw),
725 precision: self.precision.max(rhs.precision),
726 }
727 }
728}
729
730impl Add<Decimal> for Quantity {
731 type Output = Decimal;
732 fn add(self, rhs: Decimal) -> Self::Output {
733 self.as_decimal() + rhs
734 }
735}
736
737impl Sub<Decimal> for Quantity {
738 type Output = Decimal;
739 fn sub(self, rhs: Decimal) -> Self::Output {
740 self.as_decimal() - rhs
741 }
742}
743
744impl Mul<Decimal> for Quantity {
745 type Output = Decimal;
746 fn mul(self, rhs: Decimal) -> Self::Output {
747 self.as_decimal() * rhs
748 }
749}
750
751impl Div<Decimal> for Quantity {
752 type Output = Decimal;
753 fn div(self, rhs: Decimal) -> Self::Output {
754 self.as_decimal() / rhs
755 }
756}
757
758impl Add<f64> for Quantity {
759 type Output = f64;
760 fn add(self, rhs: f64) -> Self::Output {
761 self.as_f64() + rhs
762 }
763}
764
765impl Sub<f64> for Quantity {
766 type Output = f64;
767 fn sub(self, rhs: f64) -> Self::Output {
768 self.as_f64() - rhs
769 }
770}
771
772impl Mul<f64> for Quantity {
773 type Output = f64;
774 fn mul(self, rhs: f64) -> Self::Output {
775 self.as_f64() * rhs
776 }
777}
778
779impl Div<f64> for Quantity {
780 type Output = f64;
781 fn div(self, rhs: f64) -> Self::Output {
782 self.as_f64() / rhs
783 }
784}
785
786impl From<Quantity> for QuantityRaw {
787 fn from(value: Quantity) -> Self {
788 value.raw
789 }
790}
791
792impl From<&Quantity> for QuantityRaw {
793 fn from(value: &Quantity) -> Self {
794 value.raw
795 }
796}
797
798impl From<Quantity> for Decimal {
799 fn from(value: Quantity) -> Self {
800 value.as_decimal()
801 }
802}
803
804impl From<&Quantity> for Decimal {
805 fn from(value: &Quantity) -> Self {
806 value.as_decimal()
807 }
808}
809
810impl FromStr for Quantity {
811 type Err = String;
812
813 fn from_str(value: &str) -> Result<Self, Self::Err> {
814 let clean_value = value.replace('_', "");
815
816 let decimal = if clean_value.contains('e') || clean_value.contains('E') {
817 Decimal::from_scientific(&clean_value)
818 .map_err(|e| format!("Error parsing `input` string '{value}' as Decimal: {e}"))?
819 } else {
820 Decimal::from_str(&clean_value)
821 .map_err(|e| format!("Error parsing `input` string '{value}' as Decimal: {e}"))?
822 };
823
824 let precision = decimal.scale() as u8;
826
827 Self::from_decimal_dp(decimal, precision).map_err(|e| e.to_string())
828 }
829}
830
831impl From<&str> for Quantity {
832 fn from(value: &str) -> Self {
833 Self::from_str(value).expect(FAILED)
834 }
835}
836
837impl From<String> for Quantity {
838 fn from(value: String) -> Self {
839 Self::from_str(&value).expect(FAILED)
840 }
841}
842
843impl From<&String> for Quantity {
844 fn from(value: &String) -> Self {
845 Self::from_str(value).expect(FAILED)
846 }
847}
848
849impl Debug for Quantity {
850 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
851 if self.precision > MAX_FLOAT_PRECISION {
852 write!(f, "{}({})", stringify!(Quantity), self.raw)
853 } else {
854 write!(f, "{}({})", stringify!(Quantity), self.as_decimal())
855 }
856 }
857}
858
859impl Display for Quantity {
860 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
861 if self.precision > MAX_FLOAT_PRECISION {
862 write!(f, "{}", self.raw)
863 } else {
864 write!(f, "{}", self.as_decimal())
865 }
866 }
867}
868
869impl Serialize for Quantity {
870 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
871 where
872 S: serde::Serializer,
873 {
874 serializer.serialize_str(&self.to_string())
875 }
876}
877
878impl<'de> Deserialize<'de> for Quantity {
879 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
880 where
881 D: Deserializer<'de>,
882 {
883 let qty_str: std::borrow::Cow<'de, str> = Deserialize::deserialize(deserializer)?;
884 let qty: Self = qty_str.as_ref().into();
885 Ok(qty)
886 }
887}
888
889pub fn check_positive_quantity(value: Quantity, param: &str) -> CorrectnessResult<()> {
895 if !value.is_positive() {
896 return Err(CorrectnessError::NotPositive {
897 param: param.to_string(),
898 value: value.to_string(),
899 type_name: "`Quantity`",
900 });
901 }
902 Ok(())
903}
904
905#[cfg(test)]
906mod tests {
907 use std::str::FromStr;
908
909 use nautilus_core::{approx_eq, correctness::CorrectnessError};
910 use rstest::rstest;
911 use rust_decimal_macros::dec;
912
913 use super::*;
914
915 #[rstest]
916 fn test_max_quantity_round_trips_through_raw() {
917 let qty = Quantity::new(QUANTITY_MAX, 0);
920
921 assert_eq!(qty.raw, QUANTITY_RAW_MAX);
922 assert!(Quantity::from_raw_checked(qty.raw, 0).is_ok());
923 assert!(qty.checked_add(Quantity::zero(0)).is_some());
924 }
925
926 #[rstest]
927 fn test_check_quantity_positive() {
928 let qty = Quantity::new(0.0, 0);
929 let error = check_positive_quantity(qty, "qty").unwrap_err();
930
931 assert_eq!(
932 error,
933 CorrectnessError::NotPositive {
934 param: "qty".to_string(),
935 value: "0".to_string(),
936 type_name: "`Quantity`",
937 }
938 );
939 assert_eq!(
940 error.to_string(),
941 "invalid `Quantity` for 'qty' not positive, was 0"
942 );
943 }
944
945 #[rstest]
946 #[cfg(all(not(feature = "defi"), not(feature = "high-precision")))]
947 #[should_panic(expected = "`precision` exceeded maximum `FIXED_PRECISION` (9), was 17")]
948 fn test_invalid_precision_new() {
949 let _ = Quantity::new(1.0, 17);
951 }
952
953 #[rstest]
954 #[cfg(all(not(feature = "defi"), feature = "high-precision"))]
955 #[should_panic(expected = "`precision` exceeded maximum `FIXED_PRECISION` (16), was 17")]
956 fn test_invalid_precision_new() {
957 let _ = Quantity::new(1.0, 17);
959 }
960
961 #[rstest]
962 #[cfg(not(feature = "defi"))]
963 #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
964 fn test_invalid_precision_from_raw() {
965 let _ = Quantity::from_raw(1, FIXED_PRECISION + 1);
967 }
968
969 #[rstest]
970 #[cfg(not(feature = "defi"))]
971 #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
972 fn test_invalid_precision_zero() {
973 let _ = Quantity::zero(FIXED_PRECISION + 1);
975 }
976
977 #[rstest]
978 fn test_mixed_precision_add() {
979 let q1 = Quantity::new(1.0, 1);
980 let q2 = Quantity::new(1.0, 2);
981 let result = q1 + q2;
982 assert_eq!(result.precision, 2);
983 assert_eq!(result.as_f64(), 2.0);
984 }
985
986 #[rstest]
987 fn test_mixed_precision_sub() {
988 let q1 = Quantity::new(2.0, 1);
989 let q2 = Quantity::new(1.0, 2);
990 let result = q1 - q2;
991 assert_eq!(result.precision, 2);
992 assert_eq!(result.as_f64(), 1.0);
993 }
994
995 #[rstest]
996 fn test_mixed_precision_mul() {
997 let q1 = Quantity::new(2.0, 1);
998 let q2 = Quantity::new(3.0, 2);
999 let result = q1 * q2;
1000 assert_eq!(result.precision, 2);
1001 assert_eq!(result.as_f64(), 6.0);
1002 }
1003
1004 #[rstest]
1005 fn test_new_non_zero_ok() {
1006 let qty = Quantity::non_zero_checked(123.456, 3).unwrap();
1007 assert_eq!(qty.raw, Quantity::new(123.456, 3).raw);
1008 assert!(qty.is_positive());
1009 }
1010
1011 #[rstest]
1012 fn test_new_non_zero_zero_input() {
1013 assert!(Quantity::non_zero_checked(0.0, 0).is_err());
1014 }
1015
1016 #[rstest]
1017 fn test_new_non_zero_rounds_to_zero() {
1018 assert!(Quantity::non_zero_checked(0.0004, 3).is_err());
1020 }
1021
1022 #[rstest]
1023 fn test_new_non_zero_negative() {
1024 assert!(Quantity::non_zero_checked(-1.0, 0).is_err());
1025 }
1026
1027 #[rstest]
1028 fn test_new_non_zero_exceeds_max() {
1029 assert!(Quantity::non_zero_checked(QUANTITY_MAX * 10.0, 0).is_err());
1030 }
1031
1032 #[rstest]
1033 fn test_new_non_zero_invalid_precision() {
1034 assert!(Quantity::non_zero_checked(1.0, FIXED_PRECISION + 1).is_err());
1035 }
1036
1037 #[rstest]
1038 fn test_new() {
1039 let value = 0.00812;
1040 let qty = Quantity::new(value, 8);
1041 assert_eq!(qty, qty);
1042 assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
1043 assert_eq!(qty.precision, 8);
1044 assert_eq!(qty, Quantity::from("0.00812000"));
1045 assert_eq!(qty.as_decimal(), dec!(0.00812000));
1046 assert_eq!(qty.to_string(), "0.00812000");
1047 assert!(!qty.is_zero());
1048 assert!(qty.is_positive());
1049 assert!(approx_eq!(f64, qty.as_f64(), 0.00812, epsilon = 0.000_001));
1050 }
1051
1052 #[rstest]
1053 fn test_check_quantity_positive_ok() {
1054 let qty = Quantity::new(10.0, 0);
1055 check_positive_quantity(qty, "qty").unwrap();
1056 }
1057
1058 #[rstest]
1059 fn test_negative_quantity_validation() {
1060 assert!(Quantity::new_checked(-1.0, FIXED_PRECISION).is_err());
1061 }
1062
1063 #[rstest]
1064 fn test_new_checked_returns_typed_error_with_stable_display() {
1065 let error = Quantity::new_checked(QUANTITY_MAX + 1.0, FIXED_PRECISION).unwrap_err();
1066
1067 assert!(matches!(error, CorrectnessError::OutOfRange { .. }));
1068 assert_eq!(
1069 error.to_string(),
1070 format!(
1071 "invalid f64 for 'value' not in range [{QUANTITY_MIN}, {QUANTITY_MAX}], was {}",
1072 QUANTITY_MAX + 1.0
1073 )
1074 );
1075 }
1076
1077 #[rstest]
1078 fn test_from_raw_checked_returns_typed_error_with_stable_display() {
1079 let error = Quantity::from_raw_checked(QUANTITY_UNDEF, 3).unwrap_err();
1080
1081 assert_eq!(
1082 error,
1083 CorrectnessError::PredicateViolation {
1084 message: "`precision` must be 0 when `raw` is QUANTITY_UNDEF".to_string(),
1085 }
1086 );
1087 assert_eq!(
1088 error.to_string(),
1089 "`precision` must be 0 when `raw` is QUANTITY_UNDEF"
1090 );
1091 }
1092
1093 #[rstest]
1094 fn test_undefined() {
1095 let qty = Quantity::from_raw(QUANTITY_UNDEF, 0);
1096 assert_eq!(qty.raw, QUANTITY_UNDEF);
1097 assert!(qty.is_undefined());
1098 }
1099
1100 #[rstest]
1101 fn test_zero() {
1102 let qty = Quantity::zero(8);
1103 assert_eq!(qty.raw, 0);
1104 assert_eq!(qty.precision, 8);
1105 assert!(qty.is_zero());
1106 assert!(!qty.is_positive());
1107 }
1108
1109 #[rstest]
1110 fn test_from_i32() {
1111 let value = 100_000i32;
1112 let qty = Quantity::from(value);
1113 assert_eq!(qty, qty);
1114 assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
1115 assert_eq!(qty.precision, 0);
1116 }
1117
1118 #[rstest]
1119 fn test_from_u32() {
1120 let value: u32 = 5000;
1121 let qty = Quantity::from(value);
1122 assert_eq!(qty.raw, Quantity::from(format!("{value}")).raw);
1123 assert_eq!(qty.precision, 0);
1124 }
1125
1126 #[rstest]
1127 fn test_from_i64() {
1128 let value = 100_000i64;
1129 let qty = Quantity::from(value);
1130 assert_eq!(qty, qty);
1131 assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
1132 assert_eq!(qty.precision, 0);
1133 }
1134
1135 #[rstest]
1136 fn test_from_u64() {
1137 let value = 100_000u64;
1138 let qty = Quantity::from(value);
1139 assert_eq!(qty, qty);
1140 assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
1141 assert_eq!(qty.precision, 0);
1142 }
1143
1144 #[rstest] fn test_with_maximum_value() {
1146 let qty = Quantity::new_checked(QUANTITY_MAX, 0);
1147 assert!(qty.is_ok());
1148 }
1149
1150 #[rstest]
1151 fn test_with_minimum_positive_value() {
1152 let value = 0.000_000_001;
1153 let qty = Quantity::new(value, 9);
1154 assert_eq!(qty.raw, Quantity::from("0.000000001").raw);
1155 assert_eq!(qty.as_decimal(), dec!(0.000000001));
1156 assert_eq!(qty.to_string(), "0.000000001");
1157 }
1158
1159 #[rstest]
1160 fn test_with_minimum_value() {
1161 let qty = Quantity::new(QUANTITY_MIN, 9);
1162 assert_eq!(qty.raw, 0);
1163 assert_eq!(qty.as_decimal(), dec!(0));
1164 assert_eq!(qty.to_string(), "0.000000000");
1165 }
1166
1167 #[rstest]
1168 fn test_is_zero() {
1169 let qty = Quantity::zero(8);
1170 assert_eq!(qty, qty);
1171 assert_eq!(qty.raw, 0);
1172 assert_eq!(qty.precision, 8);
1173 assert_eq!(qty, Quantity::from("0.00000000"));
1174 assert_eq!(qty.as_decimal(), dec!(0));
1175 assert_eq!(qty.to_string(), "0.00000000");
1176 assert!(qty.is_zero());
1177 }
1178
1179 #[rstest]
1180 fn test_precision() {
1181 let value = 1.001;
1182 let qty = Quantity::new(value, 2);
1183 assert_eq!(qty.to_string(), "1.00");
1184 }
1185
1186 #[rstest]
1187 fn test_new_from_str() {
1188 let qty = Quantity::new(0.008_120_00, 8);
1189 assert_eq!(qty, qty);
1190 assert_eq!(qty.precision, 8);
1191 assert_eq!(qty, Quantity::from("0.00812000"));
1192 assert_eq!(qty.to_string(), "0.00812000");
1193 }
1194
1195 #[rstest]
1196 #[case("0", 0)]
1197 #[case("1.1", 1)]
1198 #[case("1.123456789", 9)]
1199 fn test_from_str_valid_input(#[case] input: &str, #[case] expected_prec: u8) {
1200 let qty = Quantity::from(input);
1201 assert_eq!(qty.precision, expected_prec);
1202 assert_eq!(qty.as_decimal(), Decimal::from_str(input).unwrap());
1203 }
1204
1205 #[rstest]
1206 #[should_panic(expected = "ParseFloatError")]
1207 fn test_from_str_invalid_input() {
1208 let input = "invalid";
1209 let _ = Quantity::new(f64::from_str(input).unwrap(), 8);
1210 }
1211
1212 #[rstest]
1213 fn test_from_str_errors() {
1214 assert!(Quantity::from_str("invalid").is_err());
1215 assert!(Quantity::from_str("12.34.56").is_err());
1216 assert!(Quantity::from_str("").is_err());
1217 assert!(Quantity::from_str("-1").is_err()); assert!(Quantity::from_str("-0.001").is_err());
1219 }
1220
1221 #[rstest]
1222 #[case("1e7", 0, 10_000_000.0)]
1223 #[case("2.5e3", 0, 2_500.0)]
1224 #[case("1.234e-2", 5, 0.01234)]
1225 #[case("5E-3", 3, 0.005)]
1226 #[case("1.0e6", 0, 1_000_000.0)]
1227 fn test_from_str_scientific_notation(
1228 #[case] input: &str,
1229 #[case] expected_precision: u8,
1230 #[case] expected_value: f64,
1231 ) {
1232 let qty = Quantity::from_str(input).unwrap();
1233 assert_eq!(qty.precision, expected_precision);
1234 assert!(approx_eq!(
1235 f64,
1236 qty.as_f64(),
1237 expected_value,
1238 epsilon = 1e-10
1239 ));
1240 }
1241
1242 #[rstest]
1243 #[case("1_234.56", 2, 1234.56)]
1244 #[case("1000000", 0, 1_000_000.0)]
1245 #[case("99_999.999_99", 5, 99_999.999_99)]
1246 fn test_from_str_with_underscores(
1247 #[case] input: &str,
1248 #[case] expected_precision: u8,
1249 #[case] expected_value: f64,
1250 ) {
1251 let qty = Quantity::from_str(input).unwrap();
1252 assert_eq!(qty.precision, expected_precision);
1253 assert!(approx_eq!(
1254 f64,
1255 qty.as_f64(),
1256 expected_value,
1257 epsilon = 1e-10
1258 ));
1259 }
1260
1261 #[rstest]
1262 fn test_from_decimal_dp_preservation() {
1263 let decimal = dec!(123.456789);
1265 let qty = Quantity::from_decimal_dp(decimal, 6).unwrap();
1266 assert_eq!(qty.precision, 6);
1267 assert!(approx_eq!(f64, qty.as_f64(), 123.456_789, epsilon = 1e-10));
1268
1269 let expected_raw = 123_456_789_u64 * 10_u64.pow(u32::from(FIXED_PRECISION - 6));
1271 assert_eq!(qty.raw, QuantityRaw::from(expected_raw));
1272 }
1273
1274 #[rstest]
1275 fn test_from_decimal_dp_rounding() {
1276 let decimal = dec!(1.005);
1278 let qty = Quantity::from_decimal_dp(decimal, 2).unwrap();
1279 assert_eq!(qty.as_f64(), 1.0); let decimal = dec!(1.015);
1282 let qty = Quantity::from_decimal_dp(decimal, 2).unwrap();
1283 assert_eq!(qty.as_f64(), 1.02); }
1285
1286 #[rstest]
1287 fn test_from_decimal_infers_precision() {
1288 let decimal = dec!(123.456);
1290 let qty = Quantity::from_decimal(decimal).unwrap();
1291 assert_eq!(qty.precision, 3);
1292 assert!(approx_eq!(f64, qty.as_f64(), 123.456, epsilon = 1e-10));
1293
1294 let decimal = dec!(100);
1296 let qty = Quantity::from_decimal(decimal).unwrap();
1297 assert_eq!(qty.precision, 0);
1298 assert_eq!(qty.as_f64(), 100.0);
1299
1300 let decimal = dec!(1.23456789);
1302 let qty = Quantity::from_decimal(decimal).unwrap();
1303 assert_eq!(qty.precision, 8);
1304 assert!(approx_eq!(f64, qty.as_f64(), 1.234_567_89, epsilon = 1e-10));
1305 }
1306
1307 #[rstest]
1308 fn test_from_decimal_trailing_zeros() {
1309 let decimal = dec!(5.670);
1311 assert_eq!(decimal.scale(), 3); let qty = Quantity::from_decimal(decimal).unwrap();
1315 assert_eq!(qty.precision, 3);
1316 assert!(approx_eq!(f64, qty.as_f64(), 5.67, epsilon = 1e-10));
1317
1318 let normalized = decimal.normalize();
1320 assert_eq!(normalized.scale(), 2);
1321 let qty_normalized = Quantity::from_decimal(normalized).unwrap();
1322 assert_eq!(qty_normalized.precision, 2);
1323 }
1324
1325 #[rstest]
1326 #[case("1.00", 2)]
1327 #[case("1.0", 1)]
1328 #[case("1.000", 3)]
1329 #[case("100.00", 2)]
1330 #[case("0.10", 2)]
1331 #[case("0.100", 3)]
1332 fn test_from_str_preserves_trailing_zeros(#[case] input: &str, #[case] expected_precision: u8) {
1333 let qty = Quantity::from_str(input).unwrap();
1334 assert_eq!(qty.precision, expected_precision);
1335 }
1336
1337 #[rstest]
1338 fn test_from_decimal_excessive_precision_inference() {
1339 let decimal = dec!(1.1234567890123456789012345678);
1342
1343 if decimal.scale() > u32::from(FIXED_PRECISION) {
1345 assert!(Quantity::from_decimal(decimal).is_err());
1346 }
1347 }
1348
1349 #[rstest]
1350 fn test_from_decimal_negative_quantity_errors() {
1351 let decimal = dec!(-123.45);
1353 let result = Quantity::from_decimal(decimal);
1354 assert!(result.is_err());
1355
1356 let result = Quantity::from_decimal_dp(decimal, 2);
1358 assert!(result.is_err());
1359 }
1360
1361 #[rstest]
1362 fn test_from_decimal_dp_negative_returns_typed_error_with_stable_display() {
1363 let error = Quantity::from_decimal_dp(dec!(-1.5), 2).unwrap_err();
1364 assert_eq!(
1365 error,
1366 CorrectnessError::PredicateViolation {
1367 message: "Decimal value '-1.5' is negative, Quantity must be non-negative"
1368 .to_string(),
1369 }
1370 );
1371 assert_eq!(
1372 error.to_string(),
1373 "Decimal value '-1.5' is negative, Quantity must be non-negative",
1374 );
1375 }
1376
1377 #[rstest]
1378 fn test_add() {
1379 let a = 1.0;
1380 let b = 2.0;
1381 let quantity1 = Quantity::new(1.0, 0);
1382 let quantity2 = Quantity::new(2.0, 0);
1383 let quantity3 = quantity1 + quantity2;
1384 assert_eq!(quantity3.raw, Quantity::new(a + b, 0).raw);
1385 }
1386
1387 #[rstest]
1388 fn test_sub() {
1389 let a = 3.0;
1390 let b = 2.0;
1391 let quantity1 = Quantity::new(a, 0);
1392 let quantity2 = Quantity::new(b, 0);
1393 let quantity3 = quantity1 - quantity2;
1394 assert_eq!(quantity3.raw, Quantity::new(a - b, 0).raw);
1395 }
1396
1397 #[rstest]
1398 fn test_quantity_checked_add_within_bounds() {
1399 let a = Quantity::new(10.0, 2);
1400 let b = Quantity::new(5.0, 2);
1401 assert_eq!(a.checked_add(b), Some(Quantity::new(15.0, 2)));
1402 }
1403
1404 #[rstest]
1405 fn test_quantity_checked_add_above_max_returns_none() {
1406 let near_max = Quantity::from_raw(QUANTITY_RAW_MAX, 0);
1407 let one = Quantity::new(1.0, 0);
1408 assert_eq!(near_max.checked_add(one), None);
1409 }
1410
1411 #[rstest]
1412 fn test_quantity_checked_sub_within_bounds() {
1413 let a = Quantity::new(10.0, 2);
1414 let b = Quantity::new(3.0, 2);
1415 assert_eq!(a.checked_sub(b), Some(Quantity::new(7.0, 2)));
1416 }
1417
1418 #[rstest]
1419 fn test_quantity_checked_sub_underflow_returns_none() {
1420 let a = Quantity::new(3.0, 2);
1421 let b = Quantity::new(10.0, 2);
1422 assert_eq!(a.checked_sub(b), None);
1423 }
1424
1425 #[rstest]
1426 fn test_quantity_checked_sub_to_zero() {
1427 let a = Quantity::new(5.0, 2);
1428 assert_eq!(a.checked_sub(a), Some(Quantity::zero(2)));
1429 }
1430
1431 #[rstest]
1432 fn test_quantity_checked_arith_rejects_undef() {
1433 let undef = Quantity::from_raw(QUANTITY_UNDEF, 0);
1434 let one = Quantity::new(1.0, 0);
1435 assert_eq!(undef.checked_add(one), None);
1436 assert_eq!(one.checked_add(undef), None);
1437 assert_eq!(undef.checked_sub(one), None);
1438 assert_eq!(one.checked_sub(undef), None);
1439 }
1440
1441 #[rstest]
1442 fn test_quantity_checked_add_at_exact_max_returns_some() {
1443 let near_max = Quantity::from_raw(QUANTITY_RAW_MAX - 1, 0);
1444 let one_unit = Quantity::from_raw(1, 0);
1445 assert_eq!(
1446 near_max.checked_add(one_unit),
1447 Some(Quantity::from_raw(QUANTITY_RAW_MAX, 0)),
1448 );
1449 }
1450
1451 #[rstest]
1452 fn test_quantity_checked_arith_uses_max_precision() {
1453 let a = Quantity::new(10.5, 1);
1454 let b = Quantity::new(2.25, 2);
1455 let sum = a.checked_add(b).unwrap();
1456 assert_eq!(sum.precision, 2);
1457 assert_eq!(sum.as_f64(), 12.75);
1458
1459 let diff = a.checked_sub(b).unwrap();
1460 assert_eq!(diff.precision, 2);
1461 assert_eq!(diff.as_f64(), 8.25);
1462 }
1463
1464 #[rstest]
1465 fn test_mul() {
1466 let value = 2.0;
1467 let quantity1 = Quantity::new(value, 1);
1468 let quantity2 = Quantity::new(value, 1);
1469 let quantity3 = quantity1 * quantity2;
1470 assert_eq!(quantity3.raw, Quantity::new(value * value, 0).raw);
1471 }
1472
1473 #[rstest]
1474 fn test_comparisons() {
1475 assert_eq!(Quantity::new(1.0, 1), Quantity::new(1.0, 1));
1476 assert_eq!(Quantity::new(1.0, 1), Quantity::new(1.0, 2));
1477 assert_ne!(Quantity::new(1.1, 1), Quantity::new(1.0, 1));
1478 assert!(Quantity::new(1.0, 1) <= Quantity::new(1.0, 2));
1479 assert!(Quantity::new(1.1, 1) > Quantity::new(1.0, 1));
1480 assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 1));
1481 assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 2));
1482 assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 2));
1483 assert!(Quantity::new(0.9, 1) < Quantity::new(1.0, 1));
1484 assert!(Quantity::new(0.9, 1) <= Quantity::new(1.0, 2));
1485 assert!(Quantity::new(0.9, 1) <= Quantity::new(1.0, 1));
1486 }
1487
1488 #[rstest]
1489 fn test_debug() {
1490 let quantity = Quantity::from_str("44.12").unwrap();
1491 let result = format!("{quantity:?}");
1492 assert_eq!(result, "Quantity(44.12)");
1493 }
1494
1495 #[rstest]
1496 fn test_display() {
1497 let quantity = Quantity::from_str("44.12").unwrap();
1498 let result = format!("{quantity}");
1499 assert_eq!(result, "44.12");
1500 }
1501
1502 #[rstest]
1503 #[case(44.12, 2, "Quantity(44.12)", "44.12")] #[case(1234.567, 8, "Quantity(1234.56700000)", "1234.56700000")] #[cfg_attr(
1506 feature = "defi",
1507 case(
1508 1_000_000_000_000_000_000.0,
1509 18,
1510 "Quantity(1000000000000000000)",
1511 "1000000000000000000"
1512 )
1513 )] fn test_debug_display_precision_handling(
1515 #[case] value: f64,
1516 #[case] precision: u8,
1517 #[case] expected_debug: &str,
1518 #[case] expected_display: &str,
1519 ) {
1520 let quantity = if precision > MAX_FLOAT_PRECISION {
1521 Quantity::from_raw(value as QuantityRaw, precision)
1523 } else {
1524 Quantity::new(value, precision)
1525 };
1526
1527 assert_eq!(format!("{quantity:?}"), expected_debug);
1528 assert_eq!(format!("{quantity}"), expected_display);
1529 }
1530
1531 #[rstest]
1532 fn test_to_formatted_string() {
1533 let qty = Quantity::new(1234.5678, 4);
1534 let formatted = qty.to_formatted_string();
1535 assert_eq!(formatted, "1_234.5678");
1536 assert_eq!(qty.to_string(), "1234.5678");
1537 }
1538
1539 #[rstest]
1540 fn test_saturating_sub() {
1541 let q1 = Quantity::new(100.0, 2);
1542 let q2 = Quantity::new(50.0, 2);
1543 let q3 = Quantity::new(150.0, 2);
1544
1545 let result = q1.saturating_sub(q2);
1546 assert_eq!(result, Quantity::new(50.0, 2));
1547
1548 let result = q1.saturating_sub(q3);
1549 assert_eq!(result, Quantity::zero(2));
1550 assert_eq!(result.raw, 0);
1551 }
1552
1553 #[rstest]
1554 fn test_saturating_sub_overflow_bug() {
1555 use crate::types::fixed::FIXED_PRECISION;
1558 let precision = 3;
1559 let scale = QuantityRaw::from(10u64.pow(u32::from(FIXED_PRECISION - precision)));
1560
1561 let peak_qty = Quantity::from_raw(79 * scale, precision);
1563 let order_qty = Quantity::from_raw(80 * scale, precision);
1564
1565 let result = peak_qty.saturating_sub(order_qty);
1567 assert_eq!(result.raw, 0);
1568 assert_eq!(result, Quantity::zero(precision));
1569 }
1570
1571 #[rstest]
1572 fn test_hash() {
1573 use std::{
1574 collections::hash_map::DefaultHasher,
1575 hash::{Hash, Hasher},
1576 };
1577
1578 let q1 = Quantity::new(100.0, 1);
1579 let q2 = Quantity::new(100.0, 1);
1580 let q3 = Quantity::new(200.0, 1);
1581
1582 let mut s1 = DefaultHasher::new();
1583 let mut s2 = DefaultHasher::new();
1584 let mut s3 = DefaultHasher::new();
1585
1586 q1.hash(&mut s1);
1587 q2.hash(&mut s2);
1588 q3.hash(&mut s3);
1589
1590 assert_eq!(
1591 s1.finish(),
1592 s2.finish(),
1593 "Equal quantities must hash equally"
1594 );
1595 assert_ne!(
1596 s1.finish(),
1597 s3.finish(),
1598 "Different quantities must hash differently"
1599 );
1600 }
1601
1602 #[rstest]
1603 fn test_quantity_serde_json_round_trip() {
1604 let original = Quantity::new(123.456, 3);
1605 let json_str = serde_json::to_string(&original).unwrap();
1606 assert_eq!(json_str, "\"123.456\"");
1607
1608 let deserialized: Quantity = serde_json::from_str(&json_str).unwrap();
1609 assert_eq!(deserialized, original);
1610 assert_eq!(deserialized.precision, 3);
1611 }
1612
1613 #[rstest]
1614 fn test_quantity_serde_json_from_value_round_trip() {
1615 let original = Quantity::new(123.456, 3);
1616 let value = serde_json::to_value(original).unwrap();
1617 assert_eq!(value, serde_json::json!("123.456"));
1618
1619 let deserialized: Quantity = serde_json::from_value(value).unwrap();
1620 assert_eq!(deserialized, original);
1621 assert_eq!(deserialized.precision, 3);
1622 }
1623
1624 #[rstest]
1625 fn test_from_mantissa_exponent_exact_precision() {
1626 let qty = Quantity::from_mantissa_exponent(12345, -2, 2);
1627 assert_eq!(qty.as_f64(), 123.45);
1628 }
1629
1630 #[rstest]
1631 fn test_from_mantissa_exponent_excess_rounds_down() {
1632 let qty = Quantity::from_mantissa_exponent(12345, -3, 2);
1635 assert_eq!(qty.as_f64(), 12.34);
1636 }
1637
1638 #[rstest]
1639 fn test_from_mantissa_exponent_excess_rounds_up() {
1640 let qty = Quantity::from_mantissa_exponent(12355, -3, 2);
1642 assert_eq!(qty.as_f64(), 12.36);
1643 }
1644
1645 #[rstest]
1646 fn test_from_mantissa_exponent_positive_exponent() {
1647 let qty = Quantity::from_mantissa_exponent(5, 2, 0);
1648 assert_eq!(qty.as_f64(), 500.0);
1649 }
1650
1651 #[rstest]
1652 fn test_from_mantissa_exponent_zero() {
1653 let qty = Quantity::from_mantissa_exponent(0, 2, 2);
1654 assert_eq!(qty.as_f64(), 0.0);
1655 }
1656
1657 #[rstest]
1658 fn test_from_mantissa_exponent_checked_exact_precision() {
1659 let qty = Quantity::from_mantissa_exponent_checked(12345, -2, 2).unwrap();
1660 assert_eq!(qty.as_decimal(), dec!(123.45));
1661 }
1662
1663 #[rstest]
1664 fn test_from_mantissa_exponent_checked_zero_with_large_exponent() {
1665 let qty = Quantity::from_mantissa_exponent_checked(0, 119, 2).unwrap();
1666 assert_eq!(qty.as_decimal(), dec!(0.00));
1667 }
1668
1669 #[rstest]
1670 fn test_from_mantissa_exponent_checked_invalid_precision() {
1671 #[cfg(feature = "defi")]
1672 let invalid_precision = crate::defi::WEI_PRECISION + 1;
1673 #[cfg(not(feature = "defi"))]
1674 let invalid_precision = FIXED_PRECISION + 1;
1675
1676 let error = Quantity::from_mantissa_exponent_checked(1, 0, invalid_precision).unwrap_err();
1677 assert!(error.to_string().contains("`precision` exceeded maximum"));
1678 }
1679
1680 #[rstest]
1681 fn test_from_mantissa_exponent_checked_overflow_returns_error() {
1682 let error = Quantity::from_mantissa_exponent_checked(u64::MAX, 100, 0).unwrap_err();
1683 assert!(
1684 error
1685 .to_string()
1686 .contains("Overflow in Quantity::from_mantissa_exponent")
1687 );
1688 }
1689
1690 #[rstest]
1691 #[should_panic(expected = "Quantity::from_mantissa_exponent")]
1692 fn test_from_mantissa_exponent_overflow_panics() {
1693 let _ = Quantity::from_mantissa_exponent(u64::MAX, 9, 0);
1694 }
1695
1696 #[rstest]
1697 #[should_panic(expected = "exceeds i128 range")]
1698 fn test_from_mantissa_exponent_large_exponent_panics() {
1699 let _ = Quantity::from_mantissa_exponent(1, 119, 0);
1700 }
1701
1702 #[rstest]
1703 fn test_from_mantissa_exponent_zero_with_large_exponent() {
1704 let qty = Quantity::from_mantissa_exponent(0, 119, 0);
1705 assert_eq!(qty.as_f64(), 0.0);
1706 }
1707
1708 #[rstest]
1709 fn test_from_mantissa_exponent_very_negative_exponent_rounds_to_zero() {
1710 let qty = Quantity::from_mantissa_exponent(12345, -120, 2);
1711 assert_eq!(qty.as_f64(), 0.0);
1712 }
1713
1714 #[rstest]
1715 fn test_f64_operations() {
1716 let q = Quantity::new(10.5, 2);
1717 assert_eq!(q + 1.0, 11.5);
1718 assert_eq!(q - 1.0, 9.5);
1719 assert_eq!(q * 2.0, 21.0);
1720 assert_eq!(q / 2.0, 5.25);
1721 }
1722
1723 #[rstest]
1724 fn test_decimal_arithmetic_operations() {
1725 let qty = Quantity::new(100.0, 2);
1726 assert_eq!(qty + dec!(50.25), dec!(150.25));
1727 assert_eq!(qty - dec!(30.50), dec!(69.50));
1728 assert_eq!(qty * dec!(1.5), dec!(150.00));
1729 assert_eq!(qty / dec!(4), dec!(25.00));
1730 }
1731
1732 #[rstest]
1736 #[cfg(feature = "defi")]
1737 #[case::sell_tx_rain_amount(
1738 U256::from_str_radix("42193532365637161405123", 10).unwrap(),
1739 18,
1740 "42193.532365637161405123"
1741 )]
1742 #[case::sell_tx_weth_amount(
1743 U256::from_str_radix("112633187203033110", 10).unwrap(),
1744 18,
1745 "0.112633187203033110"
1746 )]
1747 fn test_from_u256_real_swap_data(
1748 #[case] amount: U256,
1749 #[case] precision: u8,
1750 #[case] expected_str: &str,
1751 ) {
1752 let qty = Quantity::from_u256(amount, precision).unwrap();
1753 assert_eq!(qty.precision, precision);
1754 assert_eq!(qty.as_decimal().to_string(), expected_str);
1755 }
1756
1757 #[rstest]
1758 #[cfg(feature = "defi")]
1759 fn test_from_u256_overflow_returns_typed_error_with_stable_display() {
1760 let error = Quantity::from_u256(U256::MAX, 0).unwrap_err();
1761 match error {
1762 CorrectnessError::PredicateViolation { ref message } => {
1763 assert!(
1764 message.contains("Amount overflow during scaling to fixed precision"),
1765 "unexpected message: {message:?}",
1766 );
1767 }
1768 _ => panic!("expected PredicateViolation, was {error:?}"),
1769 }
1770 }
1771
1772 #[rstest]
1773 #[cfg(feature = "defi")]
1774 fn test_from_u256_invalid_precision_returns_typed_error() {
1775 let error = Quantity::from_u256(U256::from(1u8), 19).unwrap_err();
1776 match error {
1777 CorrectnessError::PredicateViolation { ref message } => {
1778 assert!(
1779 message.contains("WEI_PRECISION"),
1780 "unexpected message: {message:?}",
1781 );
1782 }
1783 _ => panic!("expected PredicateViolation, was {error:?}"),
1784 }
1785 }
1786
1787 #[rstest]
1788 #[cfg(feature = "defi")]
1789 fn test_from_u256_raw_above_max_returns_typed_error() {
1790 let raw = QUANTITY_RAW_MAX + 1;
1793 let error = Quantity::from_u256(U256::from(raw), FIXED_PRECISION).unwrap_err();
1794 match error {
1795 CorrectnessError::PredicateViolation { ref message } => {
1796 assert!(
1797 message.contains("QUANTITY_RAW_MAX"),
1798 "unexpected message: {message:?}",
1799 );
1800 }
1801 _ => panic!("expected PredicateViolation, was {error:?}"),
1802 }
1803 }
1804}
1805
1806#[cfg(test)]
1807mod property_tests {
1808 use proptest::prelude::*;
1809 use rstest::rstest;
1810
1811 use super::*;
1812
1813 fn quantity_value_strategy() -> impl Strategy<Value = f64> {
1815 prop_oneof![
1817 0.00001..1.0,
1819 1.0..100_000.0,
1821 100_000.0..1_000_000.0,
1823 Just(0.0),
1825 Just(QUANTITY_MAX / 2.0),
1827 ]
1828 }
1829
1830 fn precision_strategy() -> impl Strategy<Value = u8> {
1832 let upper = FIXED_PRECISION.min(MAX_FLOAT_PRECISION);
1833 prop_oneof![Just(0u8), 0u8..=upper, Just(FIXED_PRECISION),]
1834 }
1835
1836 fn precision_strategy_non_zero() -> impl Strategy<Value = u8> {
1837 let upper = FIXED_PRECISION.clamp(1, MAX_FLOAT_PRECISION);
1838 prop_oneof![Just(upper), Just(FIXED_PRECISION.max(1)), 1u8..=upper,]
1839 }
1840
1841 fn raw_for_precision_strategy() -> impl Strategy<Value = (QuantityRaw, u8)> {
1842 precision_strategy().prop_flat_map(|precision| {
1843 let step_u128 = 10u128.pow(u32::from(FIXED_PRECISION.saturating_sub(precision)));
1844 #[cfg(feature = "high-precision")]
1845 let max_steps_u128 = QUANTITY_RAW_MAX / step_u128;
1846 #[cfg(not(feature = "high-precision"))]
1847 let max_steps_u128 = (QUANTITY_RAW_MAX as u128) / step_u128;
1848
1849 (0u128..=max_steps_u128).prop_map(move |steps_u128| {
1850 let raw_u128 = steps_u128 * step_u128;
1851 #[cfg(feature = "high-precision")]
1852 let raw = raw_u128;
1853 #[cfg(not(feature = "high-precision"))]
1854 let raw = raw_u128
1855 .try_into()
1856 .expect("raw value should fit in QuantityRaw");
1857 (raw, precision)
1858 })
1859 })
1860 }
1861
1862 const DECIMAL_MAX_MANTISSA: u128 = 79_228_162_514_264_337_593_543_950_335;
1863
1864 fn decimal_compatible(raw: QuantityRaw, precision: u8) -> bool {
1865 if precision > MAX_FLOAT_PRECISION {
1866 return false;
1867 }
1868 let precision_diff = u32::from(FIXED_PRECISION.saturating_sub(precision));
1869 let divisor = 10u128.pow(precision_diff);
1870 #[cfg(feature = "high-precision")]
1871 let rescaled_raw = raw / divisor;
1872 #[cfg(not(feature = "high-precision"))]
1873 let rescaled_raw = (raw as u128) / divisor;
1874 rescaled_raw <= DECIMAL_MAX_MANTISSA
1877 }
1878
1879 proptest! {
1880 #[rstest]
1882 fn prop_quantity_serde_round_trip(
1883 (raw, precision) in raw_for_precision_strategy()
1884 ) {
1885 prop_assume!(decimal_compatible(raw, precision));
1887
1888 let original = Quantity::from_raw(raw, precision);
1889
1890 let string_repr = original.to_string();
1892 let from_string: Quantity = string_repr.parse().unwrap();
1893 prop_assert_eq!(from_string.raw, original.raw);
1894 prop_assert_eq!(from_string.precision, original.precision);
1895
1896 let json = serde_json::to_string(&original).unwrap();
1898 let from_json: Quantity = serde_json::from_str(&json).unwrap();
1899 prop_assert_eq!(from_json.precision, original.precision);
1900 prop_assert_eq!(from_json.raw, original.raw);
1901 }
1902
1903 #[rstest]
1905 fn prop_quantity_arithmetic_associative(
1906 a in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
1907 b in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
1908 c in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
1909 precision in precision_strategy()
1910 ) {
1911 let q_a = Quantity::new(a, precision);
1912 let q_b = Quantity::new(b, precision);
1913 let q_c = Quantity::new(c, precision);
1914
1915 let ab_raw = q_a.raw.checked_add(q_b.raw);
1917 let bc_raw = q_b.raw.checked_add(q_c.raw);
1918
1919 if let (Some(ab_raw), Some(bc_raw)) = (ab_raw, bc_raw) {
1920 let ab_c_raw = ab_raw.checked_add(q_c.raw);
1921 let a_bc_raw = q_a.raw.checked_add(bc_raw);
1922
1923 if let (Some(ab_c_raw), Some(a_bc_raw)) = (ab_c_raw, a_bc_raw) {
1924 prop_assert_eq!(ab_c_raw, a_bc_raw, "Associativity failed in raw arithmetic");
1926 }
1927 }
1928 }
1929
1930 #[rstest]
1932 fn prop_quantity_addition_subtraction_inverse(
1933 base in quantity_value_strategy().prop_filter("Reasonable values", |&x| x < 1e6),
1934 delta in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
1935 precision in precision_strategy()
1936 ) {
1937 let q_base = Quantity::new(base, precision);
1938 let q_delta = Quantity::new(delta, precision);
1939
1940 if let Some(added_raw) = q_base.raw.checked_add(q_delta.raw)
1942 && let Some(result_raw) = added_raw.checked_sub(q_delta.raw) {
1943 prop_assert_eq!(result_raw, q_base.raw, "Inverse operation failed in raw arithmetic");
1945 }
1946 }
1947
1948 #[rstest]
1951 fn prop_quantity_checked_add_matches_spec(
1952 a in quantity_value_strategy(),
1953 b in quantity_value_strategy(),
1954 precision in precision_strategy()
1955 ) {
1956 let q_a = Quantity::new(a, precision);
1957 let q_b = Quantity::new(b, precision);
1958 let expected = q_a.raw
1959 .checked_add(q_b.raw)
1960 .filter(|r| *r <= QUANTITY_RAW_MAX)
1961 .filter(|_| q_a.raw != QUANTITY_UNDEF && q_b.raw != QUANTITY_UNDEF)
1962 .map(|raw| Quantity { raw, precision: q_a.precision.max(q_b.precision) });
1963 prop_assert_eq!(q_a.checked_add(q_b), expected);
1964 }
1965
1966 #[rstest]
1969 fn prop_quantity_checked_sub_matches_spec(
1970 a in quantity_value_strategy(),
1971 b in quantity_value_strategy(),
1972 precision in precision_strategy()
1973 ) {
1974 let q_a = Quantity::new(a, precision);
1975 let q_b = Quantity::new(b, precision);
1976 let expected = q_a.raw
1977 .checked_sub(q_b.raw)
1978 .filter(|_| q_a.raw != QUANTITY_UNDEF && q_b.raw != QUANTITY_UNDEF)
1979 .map(|raw| Quantity { raw, precision: q_a.precision.max(q_b.precision) });
1980 prop_assert_eq!(q_a.checked_sub(q_b), expected);
1981 }
1982
1983 #[rstest]
1985 fn prop_quantity_ordering_transitive(
1986 a in quantity_value_strategy(),
1987 b in quantity_value_strategy(),
1988 c in quantity_value_strategy(),
1989 precision in precision_strategy()
1990 ) {
1991 let q_a = Quantity::new(a, precision);
1992 let q_b = Quantity::new(b, precision);
1993 let q_c = Quantity::new(c, precision);
1994
1995 if q_a <= q_b && q_b <= q_c {
1997 prop_assert!(q_a <= q_c, "Transitivity failed: {} <= {} <= {} but {} > {}",
1998 q_a.as_f64(), q_b.as_f64(), q_c.as_f64(), q_a.as_f64(), q_c.as_f64());
1999 }
2000 }
2001
2002 #[rstest]
2004 fn prop_quantity_string_parsing_precision(
2005 integral in 0u32..1_000_000,
2006 fractional in 0u32..1_000_000,
2007 precision in precision_strategy_non_zero()
2008 ) {
2009 let pow = 10u128.pow(u32::from(precision));
2011 let fractional_mod = u128::from(fractional) % pow;
2012 let fractional_str = format!("{:0width$}", fractional_mod, width = precision as usize);
2013 let quantity_str = format!("{integral}.{fractional_str}");
2014
2015 let parsed: Quantity = quantity_str.parse().unwrap();
2016 prop_assert_eq!(parsed.precision, precision);
2017
2018 let round_trip = parsed.to_string();
2020 let expected_value = format!("{integral}.{fractional_str}");
2021 prop_assert_eq!(round_trip, expected_value);
2022 }
2023
2024 #[rstest]
2026 fn prop_quantity_precision_information_preservation(
2027 value in quantity_value_strategy().prop_filter("Reasonable values", |&x| x < 1e6),
2028 precision1 in precision_strategy_non_zero(),
2029 precision2 in precision_strategy_non_zero()
2030 ) {
2031 prop_assume!(precision1 != precision2);
2033
2034 let _q1 = Quantity::new(value, precision1);
2035 let _q2 = Quantity::new(value, precision2);
2036
2037 let min_precision = precision1.min(precision2);
2040
2041 let scale = 10.0_f64.powi(i32::from(min_precision));
2043 let rounded_value = (value * scale).round() / scale;
2044
2045 let q1_reduced = Quantity::new(rounded_value, min_precision);
2046 let q2_reduced = Quantity::new(rounded_value, min_precision);
2047
2048 prop_assert_eq!(q1_reduced.raw, q2_reduced.raw, "Precision reduction inconsistent");
2050 }
2051
2052 #[rstest]
2054 fn prop_quantity_arithmetic_bounds(
2055 a in quantity_value_strategy(),
2056 b in quantity_value_strategy(),
2057 precision in precision_strategy()
2058 ) {
2059 let q_a = Quantity::new(a, precision);
2060 let q_b = Quantity::new(b, precision);
2061
2062 let sum_f64 = q_a.as_f64() + q_b.as_f64();
2064 if sum_f64.is_finite() && (QUANTITY_MIN..=QUANTITY_MAX).contains(&sum_f64) {
2065 let sum = q_a + q_b;
2066 prop_assert!(sum.as_f64().is_finite());
2067 prop_assert!(!sum.is_undefined());
2068 }
2069
2070 let diff_f64 = q_a.as_f64() - q_b.as_f64();
2072 if diff_f64.is_finite() && (QUANTITY_MIN..=QUANTITY_MAX).contains(&diff_f64) {
2073 let diff = q_a - q_b;
2074 prop_assert!(diff.as_f64().is_finite());
2075 prop_assert!(!diff.is_undefined());
2076 }
2077 }
2078
2079 #[rstest]
2081 fn prop_quantity_multiplication_non_negative(
2082 a in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 0.0 && x < 10.0),
2083 b in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 0.0 && x < 10.0),
2084 precision in precision_strategy()
2085 ) {
2086 let q_a = Quantity::new(a, precision);
2087 let q_b = Quantity::new(b, precision);
2088
2089 let raw_product_check = q_a.raw.checked_mul(q_b.raw);
2091
2092 if let Some(raw_product) = raw_product_check {
2093 let scaled_raw = raw_product / (FIXED_SCALAR as QuantityRaw);
2095 if scaled_raw <= QUANTITY_RAW_MAX {
2096 let product = q_a * q_b;
2098 prop_assert!(product.as_f64() >= 0.0, "Quantity multiplication produced negative value: {}", product.as_f64());
2099 }
2100 }
2101 }
2102
2103 #[rstest]
2105 fn prop_quantity_zero_addition_identity(
2106 value in quantity_value_strategy(),
2107 precision in precision_strategy()
2108 ) {
2109 let q = Quantity::new(value, precision);
2110 let zero = Quantity::zero(precision);
2111
2112 prop_assert_eq!(q + zero, q);
2114 prop_assert_eq!(zero + q, q);
2115 }
2116 }
2117
2118 proptest! {
2119 #[rstest]
2121 fn prop_quantity_as_decimal_preserves_precision(
2122 (raw, precision) in raw_for_precision_strategy()
2123 ) {
2124 prop_assume!(decimal_compatible(raw, precision));
2125 let quantity = Quantity::from_raw(raw, precision);
2126 let decimal = quantity.as_decimal();
2127 prop_assert_eq!(decimal.scale(), u32::from(precision));
2128 }
2129
2130 #[rstest]
2132 fn prop_quantity_as_decimal_matches_display(
2133 (raw, precision) in raw_for_precision_strategy()
2134 ) {
2135 prop_assume!(decimal_compatible(raw, precision));
2136 let quantity = Quantity::from_raw(raw, precision);
2137 let display_str = format!("{quantity}");
2138 let decimal_str = quantity.as_decimal().to_string();
2139 prop_assert_eq!(display_str, decimal_str);
2140 }
2141
2142 #[rstest]
2144 fn prop_quantity_from_decimal_roundtrip(
2145 (raw, precision) in raw_for_precision_strategy()
2146 ) {
2147 prop_assume!(decimal_compatible(raw, precision));
2148 let original = Quantity::from_raw(raw, precision);
2149 let decimal = original.as_decimal();
2150 let reconstructed = Quantity::from_decimal(decimal).unwrap();
2151 prop_assert_eq!(original.raw, reconstructed.raw);
2152 prop_assert_eq!(original.precision, reconstructed.precision);
2153 }
2154
2155 #[rstest]
2157 fn prop_quantity_from_raw_round_trip(
2158 (raw, precision) in raw_for_precision_strategy()
2159 ) {
2160 let quantity = Quantity::from_raw(raw, precision);
2161 prop_assert_eq!(quantity.raw, raw);
2162 prop_assert_eq!(quantity.precision, precision);
2163 }
2164 }
2165}