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