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 { raw: 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 Self::from_str(qty_str.as_ref()).map_err(serde::de::Error::custom)
885 }
886}
887
888pub fn check_positive_quantity(value: Quantity, param: &str) -> CorrectnessResult<()> {
894 if !value.is_positive() {
895 return Err(CorrectnessError::NotPositive {
896 param: param.to_string(),
897 value: value.to_string(),
898 type_name: "`Quantity`",
899 });
900 }
901 Ok(())
902}
903
904#[cfg(test)]
905mod tests {
906 use std::str::FromStr;
907
908 use nautilus_core::{approx_eq, correctness::CorrectnessError};
909 use rstest::rstest;
910 use rust_decimal_macros::dec;
911
912 use super::*;
913
914 #[rstest]
915 fn test_max_quantity_round_trips_through_raw() {
916 let qty = Quantity::new(QUANTITY_MAX, 0);
919
920 assert_eq!(qty.raw, QUANTITY_RAW_MAX);
921 assert!(Quantity::from_raw_checked(qty.raw, 0).is_ok());
922 assert!(qty.checked_add(Quantity::zero(0)).is_some());
923 }
924
925 #[rstest]
926 fn test_check_quantity_positive() {
927 let qty = Quantity::new(0.0, 0);
928 let error = check_positive_quantity(qty, "qty").unwrap_err();
929
930 assert_eq!(
931 error,
932 CorrectnessError::NotPositive {
933 param: "qty".to_string(),
934 value: "0".to_string(),
935 type_name: "`Quantity`",
936 }
937 );
938 assert_eq!(
939 error.to_string(),
940 "invalid `Quantity` for 'qty' not positive, was 0"
941 );
942 }
943
944 #[rstest]
945 #[cfg(all(not(feature = "defi"), not(feature = "high-precision")))]
946 #[should_panic(expected = "`precision` exceeded maximum `FIXED_PRECISION` (9), was 17")]
947 fn test_invalid_precision_new() {
948 let _ = Quantity::new(1.0, 17);
950 }
951
952 #[rstest]
953 #[cfg(all(not(feature = "defi"), feature = "high-precision"))]
954 #[should_panic(expected = "`precision` exceeded maximum `FIXED_PRECISION` (16), was 17")]
955 fn test_invalid_precision_new() {
956 let _ = Quantity::new(1.0, 17);
958 }
959
960 #[rstest]
961 #[cfg(not(feature = "defi"))]
962 #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
963 fn test_invalid_precision_from_raw() {
964 let _ = Quantity::from_raw(1, FIXED_PRECISION + 1);
966 }
967
968 #[rstest]
969 #[cfg(not(feature = "defi"))]
970 #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
971 fn test_invalid_precision_zero() {
972 let _ = Quantity::zero(FIXED_PRECISION + 1);
974 }
975
976 #[rstest]
977 fn test_mixed_precision_add() {
978 let q1 = Quantity::new(1.0, 1);
979 let q2 = Quantity::new(1.0, 2);
980 let result = q1 + q2;
981 assert_eq!(result.precision, 2);
982 assert_eq!(result.as_f64(), 2.0);
983 }
984
985 #[rstest]
986 fn test_mixed_precision_sub() {
987 let q1 = Quantity::new(2.0, 1);
988 let q2 = Quantity::new(1.0, 2);
989 let result = q1 - q2;
990 assert_eq!(result.precision, 2);
991 assert_eq!(result.as_f64(), 1.0);
992 }
993
994 #[rstest]
995 fn test_mixed_precision_mul() {
996 let q1 = Quantity::new(2.0, 1);
997 let q2 = Quantity::new(3.0, 2);
998 let result = q1 * q2;
999 assert_eq!(result.precision, 2);
1000 assert_eq!(result.as_f64(), 6.0);
1001 }
1002
1003 #[rstest]
1004 fn test_new_non_zero_ok() {
1005 let qty = Quantity::non_zero_checked(123.456, 3).unwrap();
1006 assert_eq!(qty.raw, Quantity::new(123.456, 3).raw);
1007 assert!(qty.is_positive());
1008 }
1009
1010 #[rstest]
1011 fn test_new_non_zero_zero_input() {
1012 assert!(Quantity::non_zero_checked(0.0, 0).is_err());
1013 }
1014
1015 #[rstest]
1016 fn test_new_non_zero_rounds_to_zero() {
1017 assert!(Quantity::non_zero_checked(0.0004, 3).is_err());
1019 }
1020
1021 #[rstest]
1022 fn test_new_non_zero_negative() {
1023 assert!(Quantity::non_zero_checked(-1.0, 0).is_err());
1024 }
1025
1026 #[rstest]
1027 fn test_new_non_zero_exceeds_max() {
1028 assert!(Quantity::non_zero_checked(QUANTITY_MAX * 10.0, 0).is_err());
1029 }
1030
1031 #[rstest]
1032 fn test_new_non_zero_invalid_precision() {
1033 assert!(Quantity::non_zero_checked(1.0, FIXED_PRECISION + 1).is_err());
1034 }
1035
1036 #[rstest]
1037 fn test_new() {
1038 let value = 0.00812;
1039 let qty = Quantity::new(value, 8);
1040 assert_eq!(qty, qty);
1041 assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
1042 assert_eq!(qty.precision, 8);
1043 assert_eq!(qty, Quantity::from("0.00812000"));
1044 assert_eq!(qty.as_decimal(), dec!(0.00812000));
1045 assert_eq!(qty.to_string(), "0.00812000");
1046 assert!(!qty.is_zero());
1047 assert!(qty.is_positive());
1048 assert!(approx_eq!(f64, qty.as_f64(), 0.00812, epsilon = 0.000_001));
1049 }
1050
1051 #[rstest]
1052 fn test_check_quantity_positive_ok() {
1053 let qty = Quantity::new(10.0, 0);
1054 check_positive_quantity(qty, "qty").unwrap();
1055 }
1056
1057 #[rstest]
1058 fn test_negative_quantity_validation() {
1059 assert!(Quantity::new_checked(-1.0, FIXED_PRECISION).is_err());
1060 }
1061
1062 #[rstest]
1063 fn test_new_checked_returns_typed_error_with_stable_display() {
1064 let error = Quantity::new_checked(QUANTITY_MAX + 1.0, FIXED_PRECISION).unwrap_err();
1065
1066 assert!(matches!(error, CorrectnessError::OutOfRange { .. }));
1067 assert_eq!(
1068 error.to_string(),
1069 format!(
1070 "invalid f64 for 'value' not in range [{QUANTITY_MIN}, {QUANTITY_MAX}], was {}",
1071 QUANTITY_MAX + 1.0
1072 )
1073 );
1074 }
1075
1076 #[rstest]
1077 fn test_from_raw_checked_returns_typed_error_with_stable_display() {
1078 let error = Quantity::from_raw_checked(QUANTITY_UNDEF, 3).unwrap_err();
1079
1080 assert_eq!(
1081 error,
1082 CorrectnessError::PredicateViolation {
1083 message: "`precision` must be 0 when `raw` is QUANTITY_UNDEF".to_string(),
1084 }
1085 );
1086 assert_eq!(
1087 error.to_string(),
1088 "`precision` must be 0 when `raw` is QUANTITY_UNDEF"
1089 );
1090 }
1091
1092 #[rstest]
1093 fn test_undefined() {
1094 let qty = Quantity::from_raw(QUANTITY_UNDEF, 0);
1095 assert_eq!(qty.raw, QUANTITY_UNDEF);
1096 assert!(qty.is_undefined());
1097 }
1098
1099 #[rstest]
1100 fn test_zero() {
1101 let qty = Quantity::zero(8);
1102 assert_eq!(qty.raw, 0);
1103 assert_eq!(qty.precision, 8);
1104 assert!(qty.is_zero());
1105 assert!(!qty.is_positive());
1106 }
1107
1108 #[rstest]
1109 fn test_from_i32() {
1110 let value = 100_000i32;
1111 let qty = Quantity::from(value);
1112 assert_eq!(qty, qty);
1113 assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
1114 assert_eq!(qty.precision, 0);
1115 }
1116
1117 #[rstest]
1118 fn test_from_u32() {
1119 let value: u32 = 5000;
1120 let qty = Quantity::from(value);
1121 assert_eq!(qty.raw, Quantity::from(format!("{value}")).raw);
1122 assert_eq!(qty.precision, 0);
1123 }
1124
1125 #[rstest]
1126 fn test_from_i64() {
1127 let value = 100_000i64;
1128 let qty = Quantity::from(value);
1129 assert_eq!(qty, qty);
1130 assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
1131 assert_eq!(qty.precision, 0);
1132 }
1133
1134 #[rstest]
1135 fn test_from_u64() {
1136 let value = 100_000u64;
1137 let qty = Quantity::from(value);
1138 assert_eq!(qty, qty);
1139 assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
1140 assert_eq!(qty.precision, 0);
1141 }
1142
1143 #[rstest] fn test_with_maximum_value() {
1145 let qty = Quantity::new_checked(QUANTITY_MAX, 0);
1146 assert!(qty.is_ok());
1147 }
1148
1149 #[rstest]
1150 fn test_with_minimum_positive_value() {
1151 let value = 0.000_000_001;
1152 let qty = Quantity::new(value, 9);
1153 assert_eq!(qty.raw, Quantity::from("0.000000001").raw);
1154 assert_eq!(qty.as_decimal(), dec!(0.000000001));
1155 assert_eq!(qty.to_string(), "0.000000001");
1156 }
1157
1158 #[rstest]
1159 fn test_with_minimum_value() {
1160 let qty = Quantity::new(QUANTITY_MIN, 9);
1161 assert_eq!(qty.raw, 0);
1162 assert_eq!(qty.as_decimal(), dec!(0));
1163 assert_eq!(qty.to_string(), "0.000000000");
1164 }
1165
1166 #[rstest]
1167 fn test_is_zero() {
1168 let qty = Quantity::zero(8);
1169 assert_eq!(qty, qty);
1170 assert_eq!(qty.raw, 0);
1171 assert_eq!(qty.precision, 8);
1172 assert_eq!(qty, Quantity::from("0.00000000"));
1173 assert_eq!(qty.as_decimal(), dec!(0));
1174 assert_eq!(qty.to_string(), "0.00000000");
1175 assert!(qty.is_zero());
1176 }
1177
1178 #[rstest]
1179 fn test_precision() {
1180 let value = 1.001;
1181 let qty = Quantity::new(value, 2);
1182 assert_eq!(qty.to_string(), "1.00");
1183 }
1184
1185 #[rstest]
1186 fn test_new_from_str() {
1187 let qty = Quantity::new(0.008_120_00, 8);
1188 assert_eq!(qty, qty);
1189 assert_eq!(qty.precision, 8);
1190 assert_eq!(qty, Quantity::from("0.00812000"));
1191 assert_eq!(qty.to_string(), "0.00812000");
1192 }
1193
1194 #[rstest]
1195 #[case("0", 0)]
1196 #[case("1.1", 1)]
1197 #[case("1.123456789", 9)]
1198 fn test_from_str_valid_input(#[case] input: &str, #[case] expected_prec: u8) {
1199 let qty = Quantity::from(input);
1200 assert_eq!(qty.precision, expected_prec);
1201 assert_eq!(qty.as_decimal(), Decimal::from_str(input).unwrap());
1202 }
1203
1204 #[rstest]
1205 #[should_panic(expected = "ParseFloatError")]
1206 fn test_from_str_invalid_input() {
1207 let input = "invalid";
1208 let _ = Quantity::new(f64::from_str(input).unwrap(), 8);
1209 }
1210
1211 #[rstest]
1212 fn test_from_str_errors() {
1213 assert!(Quantity::from_str("invalid").is_err());
1214 assert!(Quantity::from_str("12.34.56").is_err());
1215 assert!(Quantity::from_str("").is_err());
1216 assert!(Quantity::from_str("-1").is_err()); assert!(Quantity::from_str("-0.001").is_err());
1218 }
1219
1220 #[rstest]
1221 #[case("1e7", 0, 10_000_000.0)]
1222 #[case("2.5e3", 0, 2_500.0)]
1223 #[case("1.234e-2", 5, 0.01234)]
1224 #[case("5E-3", 3, 0.005)]
1225 #[case("1.0e6", 0, 1_000_000.0)]
1226 fn test_from_str_scientific_notation(
1227 #[case] input: &str,
1228 #[case] expected_precision: u8,
1229 #[case] expected_value: f64,
1230 ) {
1231 let qty = Quantity::from_str(input).unwrap();
1232 assert_eq!(qty.precision, expected_precision);
1233 assert!(approx_eq!(
1234 f64,
1235 qty.as_f64(),
1236 expected_value,
1237 epsilon = 1e-10
1238 ));
1239 }
1240
1241 #[rstest]
1242 #[case("1_234.56", 2, 1234.56)]
1243 #[case("1000000", 0, 1_000_000.0)]
1244 #[case("99_999.999_99", 5, 99_999.999_99)]
1245 fn test_from_str_with_underscores(
1246 #[case] input: &str,
1247 #[case] expected_precision: u8,
1248 #[case] expected_value: f64,
1249 ) {
1250 let qty = Quantity::from_str(input).unwrap();
1251 assert_eq!(qty.precision, expected_precision);
1252 assert!(approx_eq!(
1253 f64,
1254 qty.as_f64(),
1255 expected_value,
1256 epsilon = 1e-10
1257 ));
1258 }
1259
1260 #[rstest]
1261 fn test_from_decimal_dp_preservation() {
1262 let decimal = dec!(123.456789);
1264 let qty = Quantity::from_decimal_dp(decimal, 6).unwrap();
1265 assert_eq!(qty.precision, 6);
1266 assert!(approx_eq!(f64, qty.as_f64(), 123.456_789, epsilon = 1e-10));
1267
1268 let expected_raw = 123_456_789_u64 * 10_u64.pow(u32::from(FIXED_PRECISION - 6));
1270 assert_eq!(qty.raw, QuantityRaw::from(expected_raw));
1271 }
1272
1273 #[rstest]
1274 fn test_from_decimal_dp_rounding() {
1275 let decimal = dec!(1.005);
1277 let qty = Quantity::from_decimal_dp(decimal, 2).unwrap();
1278 assert_eq!(qty.as_f64(), 1.0); let decimal = dec!(1.015);
1281 let qty = Quantity::from_decimal_dp(decimal, 2).unwrap();
1282 assert_eq!(qty.as_f64(), 1.02); }
1284
1285 #[rstest]
1286 fn test_from_decimal_infers_precision() {
1287 let decimal = dec!(123.456);
1289 let qty = Quantity::from_decimal(decimal).unwrap();
1290 assert_eq!(qty.precision, 3);
1291 assert!(approx_eq!(f64, qty.as_f64(), 123.456, epsilon = 1e-10));
1292
1293 let decimal = dec!(100);
1295 let qty = Quantity::from_decimal(decimal).unwrap();
1296 assert_eq!(qty.precision, 0);
1297 assert_eq!(qty.as_f64(), 100.0);
1298
1299 let decimal = dec!(1.23456789);
1301 let qty = Quantity::from_decimal(decimal).unwrap();
1302 assert_eq!(qty.precision, 8);
1303 assert!(approx_eq!(f64, qty.as_f64(), 1.234_567_89, epsilon = 1e-10));
1304 }
1305
1306 #[rstest]
1307 fn test_from_decimal_trailing_zeros() {
1308 let decimal = dec!(5.670);
1310 assert_eq!(decimal.scale(), 3); let qty = Quantity::from_decimal(decimal).unwrap();
1314 assert_eq!(qty.precision, 3);
1315 assert!(approx_eq!(f64, qty.as_f64(), 5.67, epsilon = 1e-10));
1316
1317 let normalized = decimal.normalize();
1319 assert_eq!(normalized.scale(), 2);
1320 let qty_normalized = Quantity::from_decimal(normalized).unwrap();
1321 assert_eq!(qty_normalized.precision, 2);
1322 }
1323
1324 #[rstest]
1325 #[case("1.00", 2)]
1326 #[case("1.0", 1)]
1327 #[case("1.000", 3)]
1328 #[case("100.00", 2)]
1329 #[case("0.10", 2)]
1330 #[case("0.100", 3)]
1331 fn test_from_str_preserves_trailing_zeros(#[case] input: &str, #[case] expected_precision: u8) {
1332 let qty = Quantity::from_str(input).unwrap();
1333 assert_eq!(qty.precision, expected_precision);
1334 }
1335
1336 #[rstest]
1337 fn test_from_decimal_excessive_precision_inference() {
1338 let decimal = dec!(1.1234567890123456789012345678);
1341
1342 if decimal.scale() > u32::from(FIXED_PRECISION) {
1344 assert!(Quantity::from_decimal(decimal).is_err());
1345 }
1346 }
1347
1348 #[rstest]
1349 fn test_from_decimal_negative_quantity_errors() {
1350 let decimal = dec!(-123.45);
1352 let result = Quantity::from_decimal(decimal);
1353 assert!(result.is_err());
1354
1355 let result = Quantity::from_decimal_dp(decimal, 2);
1357 assert!(result.is_err());
1358 }
1359
1360 #[rstest]
1361 fn test_from_decimal_dp_negative_returns_typed_error_with_stable_display() {
1362 let error = Quantity::from_decimal_dp(dec!(-1.5), 2).unwrap_err();
1363 assert_eq!(
1364 error,
1365 CorrectnessError::PredicateViolation {
1366 message: "Decimal value '-1.5' is negative, Quantity must be non-negative"
1367 .to_string(),
1368 }
1369 );
1370 assert_eq!(
1371 error.to_string(),
1372 "Decimal value '-1.5' is negative, Quantity must be non-negative",
1373 );
1374 }
1375
1376 #[rstest]
1377 fn test_add() {
1378 let a = 1.0;
1379 let b = 2.0;
1380 let quantity1 = Quantity::new(1.0, 0);
1381 let quantity2 = Quantity::new(2.0, 0);
1382 let quantity3 = quantity1 + quantity2;
1383 assert_eq!(quantity3.raw, Quantity::new(a + b, 0).raw);
1384 }
1385
1386 #[rstest]
1387 fn test_sub() {
1388 let a = 3.0;
1389 let b = 2.0;
1390 let quantity1 = Quantity::new(a, 0);
1391 let quantity2 = Quantity::new(b, 0);
1392 let quantity3 = quantity1 - quantity2;
1393 assert_eq!(quantity3.raw, Quantity::new(a - b, 0).raw);
1394 }
1395
1396 #[rstest]
1397 fn test_quantity_checked_add_within_bounds() {
1398 let a = Quantity::new(10.0, 2);
1399 let b = Quantity::new(5.0, 2);
1400 assert_eq!(a.checked_add(b), Some(Quantity::new(15.0, 2)));
1401 }
1402
1403 #[rstest]
1404 fn test_quantity_checked_add_above_max_returns_none() {
1405 let near_max = Quantity::from_raw(QUANTITY_RAW_MAX, 0);
1406 let one = Quantity::new(1.0, 0);
1407 assert_eq!(near_max.checked_add(one), None);
1408 }
1409
1410 #[rstest]
1411 fn test_quantity_checked_sub_within_bounds() {
1412 let a = Quantity::new(10.0, 2);
1413 let b = Quantity::new(3.0, 2);
1414 assert_eq!(a.checked_sub(b), Some(Quantity::new(7.0, 2)));
1415 }
1416
1417 #[rstest]
1418 fn test_quantity_checked_sub_underflow_returns_none() {
1419 let a = Quantity::new(3.0, 2);
1420 let b = Quantity::new(10.0, 2);
1421 assert_eq!(a.checked_sub(b), None);
1422 }
1423
1424 #[rstest]
1425 fn test_quantity_checked_sub_to_zero() {
1426 let a = Quantity::new(5.0, 2);
1427 assert_eq!(a.checked_sub(a), Some(Quantity::zero(2)));
1428 }
1429
1430 #[rstest]
1431 fn test_quantity_checked_arith_rejects_undef() {
1432 let undef = Quantity::from_raw(QUANTITY_UNDEF, 0);
1433 let one = Quantity::new(1.0, 0);
1434 assert_eq!(undef.checked_add(one), None);
1435 assert_eq!(one.checked_add(undef), None);
1436 assert_eq!(undef.checked_sub(one), None);
1437 assert_eq!(one.checked_sub(undef), None);
1438 }
1439
1440 #[rstest]
1441 fn test_quantity_checked_add_at_exact_max_returns_some() {
1442 let near_max = Quantity::from_raw(QUANTITY_RAW_MAX - 1, 0);
1443 let one_unit = Quantity::from_raw(1, 0);
1444 assert_eq!(
1445 near_max.checked_add(one_unit),
1446 Some(Quantity::from_raw(QUANTITY_RAW_MAX, 0)),
1447 );
1448 }
1449
1450 #[rstest]
1451 fn test_quantity_checked_arith_uses_max_precision() {
1452 let a = Quantity::new(10.5, 1);
1453 let b = Quantity::new(2.25, 2);
1454 let sum = a.checked_add(b).unwrap();
1455 assert_eq!(sum.precision, 2);
1456 assert_eq!(sum.as_f64(), 12.75);
1457
1458 let diff = a.checked_sub(b).unwrap();
1459 assert_eq!(diff.precision, 2);
1460 assert_eq!(diff.as_f64(), 8.25);
1461 }
1462
1463 #[rstest]
1464 fn test_mul() {
1465 let value = 2.0;
1466 let quantity1 = Quantity::new(value, 1);
1467 let quantity2 = Quantity::new(value, 1);
1468 let quantity3 = quantity1 * quantity2;
1469 assert_eq!(quantity3.raw, Quantity::new(value * value, 0).raw);
1470 }
1471
1472 #[rstest]
1473 fn test_comparisons() {
1474 assert_eq!(Quantity::new(1.0, 1), Quantity::new(1.0, 1));
1475 assert_eq!(Quantity::new(1.0, 1), Quantity::new(1.0, 2));
1476 assert_ne!(Quantity::new(1.1, 1), Quantity::new(1.0, 1));
1477 assert!(Quantity::new(1.0, 1) <= Quantity::new(1.0, 2));
1478 assert!(Quantity::new(1.1, 1) > Quantity::new(1.0, 1));
1479 assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 1));
1480 assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 2));
1481 assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 2));
1482 assert!(Quantity::new(0.9, 1) < Quantity::new(1.0, 1));
1483 assert!(Quantity::new(0.9, 1) <= Quantity::new(1.0, 2));
1484 assert!(Quantity::new(0.9, 1) <= Quantity::new(1.0, 1));
1485 }
1486
1487 #[rstest]
1488 fn test_debug() {
1489 let quantity = Quantity::from_str("44.12").unwrap();
1490 let result = format!("{quantity:?}");
1491 assert_eq!(result, "Quantity(44.12)");
1492 }
1493
1494 #[rstest]
1495 fn test_display() {
1496 let quantity = Quantity::from_str("44.12").unwrap();
1497 let result = format!("{quantity}");
1498 assert_eq!(result, "44.12");
1499 }
1500
1501 #[rstest]
1502 #[case(44.12, 2, "Quantity(44.12)", "44.12")] #[case(1234.567, 8, "Quantity(1234.56700000)", "1234.56700000")] #[cfg_attr(
1505 feature = "defi",
1506 case(
1507 1_000_000_000_000_000_000.0,
1508 18,
1509 "Quantity(1000000000000000000)",
1510 "1000000000000000000"
1511 )
1512 )] fn test_debug_display_precision_handling(
1514 #[case] value: f64,
1515 #[case] precision: u8,
1516 #[case] expected_debug: &str,
1517 #[case] expected_display: &str,
1518 ) {
1519 let quantity = if precision > MAX_FLOAT_PRECISION {
1520 Quantity::from_raw(value as QuantityRaw, precision)
1522 } else {
1523 Quantity::new(value, precision)
1524 };
1525
1526 assert_eq!(format!("{quantity:?}"), expected_debug);
1527 assert_eq!(format!("{quantity}"), expected_display);
1528 }
1529
1530 #[rstest]
1531 fn test_to_formatted_string() {
1532 let qty = Quantity::new(1234.5678, 4);
1533 let formatted = qty.to_formatted_string();
1534 assert_eq!(formatted, "1_234.5678");
1535 assert_eq!(qty.to_string(), "1234.5678");
1536 }
1537
1538 #[rstest]
1539 fn test_saturating_sub() {
1540 let q1 = Quantity::new(100.0, 2);
1541 let q2 = Quantity::new(50.0, 2);
1542 let q3 = Quantity::new(150.0, 2);
1543
1544 let result = q1.saturating_sub(q2);
1545 assert_eq!(result, Quantity::new(50.0, 2));
1546
1547 let result = q1.saturating_sub(q3);
1548 assert_eq!(result, Quantity::zero(2));
1549 assert_eq!(result.raw, 0);
1550 }
1551
1552 #[rstest]
1553 fn test_saturating_sub_overflow_bug() {
1554 use crate::types::fixed::FIXED_PRECISION;
1557 let precision = 3;
1558 let scale = QuantityRaw::from(10u64.pow(u32::from(FIXED_PRECISION - precision)));
1559
1560 let peak_qty = Quantity::from_raw(79 * scale, precision);
1562 let order_qty = Quantity::from_raw(80 * scale, precision);
1563
1564 let result = peak_qty.saturating_sub(order_qty);
1566 assert_eq!(result.raw, 0);
1567 assert_eq!(result, Quantity::zero(precision));
1568 }
1569
1570 #[rstest]
1571 fn test_hash() {
1572 use std::{
1573 collections::hash_map::DefaultHasher,
1574 hash::{Hash, Hasher},
1575 };
1576
1577 let q1 = Quantity::new(100.0, 1);
1578 let q2 = Quantity::new(100.0, 1);
1579 let q3 = Quantity::new(200.0, 1);
1580
1581 let mut s1 = DefaultHasher::new();
1582 let mut s2 = DefaultHasher::new();
1583 let mut s3 = DefaultHasher::new();
1584
1585 q1.hash(&mut s1);
1586 q2.hash(&mut s2);
1587 q3.hash(&mut s3);
1588
1589 assert_eq!(
1590 s1.finish(),
1591 s2.finish(),
1592 "Equal quantities must hash equally"
1593 );
1594 assert_ne!(
1595 s1.finish(),
1596 s3.finish(),
1597 "Different quantities must hash differently"
1598 );
1599 }
1600
1601 #[rstest]
1602 fn test_quantity_serde_json_round_trip() {
1603 let original = Quantity::new(123.456, 3);
1604 let json_str = serde_json::to_string(&original).unwrap();
1605 assert_eq!(json_str, "\"123.456\"");
1606
1607 let deserialized: Quantity = serde_json::from_str(&json_str).unwrap();
1608 assert_eq!(deserialized, original);
1609 assert_eq!(deserialized.precision, 3);
1610 }
1611
1612 #[rstest]
1613 fn test_quantity_serde_json_from_value_round_trip() {
1614 let original = Quantity::new(123.456, 3);
1615 let value = serde_json::to_value(original).unwrap();
1616 assert_eq!(value, serde_json::json!("123.456"));
1617
1618 let deserialized: Quantity = serde_json::from_value(value).unwrap();
1619 assert_eq!(deserialized, original);
1620 assert_eq!(deserialized.precision, 3);
1621 }
1622
1623 #[rstest]
1624 fn test_quantity_deserialize_invalid_string_returns_error() {
1625 let result = serde_json::from_str::<Quantity>("\"not-a-quantity\"");
1626 let error = result.unwrap_err();
1627 assert!(
1628 error.to_string().contains("Error parsing"),
1629 "unexpected message: {error}"
1630 );
1631 }
1632
1633 #[rstest]
1634 fn test_quantity_deserialize_negative_returns_error() {
1635 let result = serde_json::from_str::<Quantity>("\"-1.5\"");
1636 let error = result.unwrap_err();
1637 assert!(
1638 error.to_string().contains("negative"),
1639 "unexpected message: {error}"
1640 );
1641 }
1642
1643 #[rstest]
1644 fn test_from_mantissa_exponent_exact_precision() {
1645 let qty = Quantity::from_mantissa_exponent(12345, -2, 2);
1646 assert_eq!(qty.as_f64(), 123.45);
1647 }
1648
1649 #[rstest]
1650 fn test_from_mantissa_exponent_excess_rounds_down() {
1651 let qty = Quantity::from_mantissa_exponent(12345, -3, 2);
1654 assert_eq!(qty.as_f64(), 12.34);
1655 }
1656
1657 #[rstest]
1658 fn test_from_mantissa_exponent_excess_rounds_up() {
1659 let qty = Quantity::from_mantissa_exponent(12355, -3, 2);
1661 assert_eq!(qty.as_f64(), 12.36);
1662 }
1663
1664 #[rstest]
1665 fn test_from_mantissa_exponent_positive_exponent() {
1666 let qty = Quantity::from_mantissa_exponent(5, 2, 0);
1667 assert_eq!(qty.as_f64(), 500.0);
1668 }
1669
1670 #[rstest]
1671 fn test_from_mantissa_exponent_zero() {
1672 let qty = Quantity::from_mantissa_exponent(0, 2, 2);
1673 assert_eq!(qty.as_f64(), 0.0);
1674 }
1675
1676 #[rstest]
1677 fn test_from_mantissa_exponent_checked_exact_precision() {
1678 let qty = Quantity::from_mantissa_exponent_checked(12345, -2, 2).unwrap();
1679 assert_eq!(qty.as_decimal(), dec!(123.45));
1680 }
1681
1682 #[rstest]
1683 fn test_from_mantissa_exponent_checked_zero_with_large_exponent() {
1684 let qty = Quantity::from_mantissa_exponent_checked(0, 119, 2).unwrap();
1685 assert_eq!(qty.as_decimal(), dec!(0.00));
1686 }
1687
1688 #[rstest]
1689 fn test_from_mantissa_exponent_checked_invalid_precision() {
1690 #[cfg(feature = "defi")]
1691 let invalid_precision = crate::defi::WEI_PRECISION + 1;
1692 #[cfg(not(feature = "defi"))]
1693 let invalid_precision = FIXED_PRECISION + 1;
1694
1695 let error = Quantity::from_mantissa_exponent_checked(1, 0, invalid_precision).unwrap_err();
1696 assert!(error.to_string().contains("`precision` exceeded maximum"));
1697 }
1698
1699 #[rstest]
1700 fn test_from_mantissa_exponent_checked_overflow_returns_error() {
1701 let error = Quantity::from_mantissa_exponent_checked(u64::MAX, 100, 0).unwrap_err();
1702 assert!(
1703 error
1704 .to_string()
1705 .contains("Overflow in Quantity::from_mantissa_exponent")
1706 );
1707 }
1708
1709 #[rstest]
1710 #[should_panic(expected = "Quantity::from_mantissa_exponent")]
1711 fn test_from_mantissa_exponent_overflow_panics() {
1712 let _ = Quantity::from_mantissa_exponent(u64::MAX, 9, 0);
1713 }
1714
1715 #[rstest]
1716 #[should_panic(expected = "exceeds i128 range")]
1717 fn test_from_mantissa_exponent_large_exponent_panics() {
1718 let _ = Quantity::from_mantissa_exponent(1, 119, 0);
1719 }
1720
1721 #[rstest]
1722 fn test_from_mantissa_exponent_zero_with_large_exponent() {
1723 let qty = Quantity::from_mantissa_exponent(0, 119, 0);
1724 assert_eq!(qty.as_f64(), 0.0);
1725 }
1726
1727 #[rstest]
1728 fn test_from_mantissa_exponent_very_negative_exponent_rounds_to_zero() {
1729 let qty = Quantity::from_mantissa_exponent(12345, -120, 2);
1730 assert_eq!(qty.as_f64(), 0.0);
1731 }
1732
1733 #[rstest]
1734 fn test_f64_operations() {
1735 let q = Quantity::new(10.5, 2);
1736 assert_eq!(q + 1.0, 11.5);
1737 assert_eq!(q - 1.0, 9.5);
1738 assert_eq!(q * 2.0, 21.0);
1739 assert_eq!(q / 2.0, 5.25);
1740 }
1741
1742 #[rstest]
1743 fn test_decimal_arithmetic_operations() {
1744 let qty = Quantity::new(100.0, 2);
1745 assert_eq!(qty + dec!(50.25), dec!(150.25));
1746 assert_eq!(qty - dec!(30.50), dec!(69.50));
1747 assert_eq!(qty * dec!(1.5), dec!(150.00));
1748 assert_eq!(qty / dec!(4), dec!(25.00));
1749 }
1750
1751 #[rstest]
1755 #[cfg(feature = "defi")]
1756 #[case::sell_tx_rain_amount(
1757 U256::from_str_radix("42193532365637161405123", 10).unwrap(),
1758 18,
1759 "42193.532365637161405123"
1760 )]
1761 #[case::sell_tx_weth_amount(
1762 U256::from_str_radix("112633187203033110", 10).unwrap(),
1763 18,
1764 "0.112633187203033110"
1765 )]
1766 fn test_from_u256_real_swap_data(
1767 #[case] amount: U256,
1768 #[case] precision: u8,
1769 #[case] expected_str: &str,
1770 ) {
1771 let qty = Quantity::from_u256(amount, precision).unwrap();
1772 assert_eq!(qty.precision, precision);
1773 assert_eq!(qty.as_decimal().to_string(), expected_str);
1774 }
1775
1776 #[rstest]
1777 #[cfg(feature = "defi")]
1778 fn test_from_u256_overflow_returns_typed_error_with_stable_display() {
1779 let error = Quantity::from_u256(U256::MAX, 0).unwrap_err();
1780 match error {
1781 CorrectnessError::PredicateViolation { ref message } => {
1782 assert!(
1783 message.contains("Amount overflow during scaling to fixed precision"),
1784 "unexpected message: {message:?}",
1785 );
1786 }
1787 _ => panic!("expected PredicateViolation, was {error:?}"),
1788 }
1789 }
1790
1791 #[rstest]
1792 #[cfg(feature = "defi")]
1793 fn test_from_u256_invalid_precision_returns_typed_error() {
1794 let error = Quantity::from_u256(U256::from(1u8), 19).unwrap_err();
1795 match error {
1796 CorrectnessError::PredicateViolation { ref message } => {
1797 assert!(
1798 message.contains("WEI_PRECISION"),
1799 "unexpected message: {message:?}",
1800 );
1801 }
1802 _ => panic!("expected PredicateViolation, was {error:?}"),
1803 }
1804 }
1805
1806 #[rstest]
1807 #[cfg(feature = "defi")]
1808 fn test_from_u256_raw_above_max_returns_typed_error() {
1809 let raw = QUANTITY_RAW_MAX + 1;
1812 let error = Quantity::from_u256(U256::from(raw), FIXED_PRECISION).unwrap_err();
1813 match error {
1814 CorrectnessError::PredicateViolation { ref message } => {
1815 assert!(
1816 message.contains("QUANTITY_RAW_MAX"),
1817 "unexpected message: {message:?}",
1818 );
1819 }
1820 _ => panic!("expected PredicateViolation, was {error:?}"),
1821 }
1822 }
1823}
1824
1825#[cfg(test)]
1826mod property_tests {
1827 use proptest::prelude::*;
1828 use rstest::rstest;
1829
1830 use super::*;
1831
1832 fn quantity_value_strategy() -> impl Strategy<Value = f64> {
1834 prop_oneof![
1836 0.00001..1.0,
1838 1.0..100_000.0,
1840 100_000.0..1_000_000.0,
1842 Just(0.0),
1844 Just(QUANTITY_MAX / 2.0),
1846 ]
1847 }
1848
1849 fn precision_strategy() -> impl Strategy<Value = u8> {
1851 let upper = FIXED_PRECISION.min(MAX_FLOAT_PRECISION);
1852 prop_oneof![Just(0u8), 0u8..=upper, Just(FIXED_PRECISION),]
1853 }
1854
1855 fn precision_strategy_non_zero() -> impl Strategy<Value = u8> {
1856 let upper = FIXED_PRECISION.clamp(1, MAX_FLOAT_PRECISION);
1857 prop_oneof![Just(upper), Just(FIXED_PRECISION.max(1)), 1u8..=upper,]
1858 }
1859
1860 fn raw_for_precision_strategy() -> impl Strategy<Value = (QuantityRaw, u8)> {
1861 precision_strategy().prop_flat_map(|precision| {
1862 let step_u128 = 10u128.pow(u32::from(FIXED_PRECISION.saturating_sub(precision)));
1863 #[cfg(feature = "high-precision")]
1864 let max_steps_u128 = QUANTITY_RAW_MAX / step_u128;
1865 #[cfg(not(feature = "high-precision"))]
1866 let max_steps_u128 = (QUANTITY_RAW_MAX as u128) / step_u128;
1867
1868 (0u128..=max_steps_u128).prop_map(move |steps_u128| {
1869 let raw_u128 = steps_u128 * step_u128;
1870 #[cfg(feature = "high-precision")]
1871 let raw = raw_u128;
1872 #[cfg(not(feature = "high-precision"))]
1873 let raw = raw_u128
1874 .try_into()
1875 .expect("raw value should fit in QuantityRaw");
1876 (raw, precision)
1877 })
1878 })
1879 }
1880
1881 const DECIMAL_MAX_MANTISSA: u128 = 79_228_162_514_264_337_593_543_950_335;
1882
1883 fn decimal_compatible(raw: QuantityRaw, precision: u8) -> bool {
1884 if precision > MAX_FLOAT_PRECISION {
1885 return false;
1886 }
1887 let precision_diff = u32::from(FIXED_PRECISION.saturating_sub(precision));
1888 let divisor = 10u128.pow(precision_diff);
1889 #[cfg(feature = "high-precision")]
1890 let rescaled_raw = raw / divisor;
1891 #[cfg(not(feature = "high-precision"))]
1892 let rescaled_raw = (raw as u128) / divisor;
1893 rescaled_raw <= DECIMAL_MAX_MANTISSA
1896 }
1897
1898 proptest! {
1899 #[rstest]
1901 fn prop_quantity_serde_round_trip(
1902 (raw, precision) in raw_for_precision_strategy()
1903 ) {
1904 prop_assume!(decimal_compatible(raw, precision));
1906
1907 let original = Quantity::from_raw(raw, precision);
1908
1909 let string_repr = original.to_string();
1911 let from_string: Quantity = string_repr.parse().unwrap();
1912 prop_assert_eq!(from_string.raw, original.raw);
1913 prop_assert_eq!(from_string.precision, original.precision);
1914
1915 let json = serde_json::to_string(&original).unwrap();
1917 let from_json: Quantity = serde_json::from_str(&json).unwrap();
1918 prop_assert_eq!(from_json.precision, original.precision);
1919 prop_assert_eq!(from_json.raw, original.raw);
1920 }
1921
1922 #[rstest]
1924 fn prop_quantity_arithmetic_associative(
1925 a in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
1926 b in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
1927 c in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
1928 precision in precision_strategy()
1929 ) {
1930 let q_a = Quantity::new(a, precision);
1931 let q_b = Quantity::new(b, precision);
1932 let q_c = Quantity::new(c, precision);
1933
1934 let ab_raw = q_a.raw.checked_add(q_b.raw);
1936 let bc_raw = q_b.raw.checked_add(q_c.raw);
1937
1938 if let (Some(ab_raw), Some(bc_raw)) = (ab_raw, bc_raw) {
1939 let ab_c_raw = ab_raw.checked_add(q_c.raw);
1940 let a_bc_raw = q_a.raw.checked_add(bc_raw);
1941
1942 if let (Some(ab_c_raw), Some(a_bc_raw)) = (ab_c_raw, a_bc_raw) {
1943 prop_assert_eq!(ab_c_raw, a_bc_raw, "Associativity failed in raw arithmetic");
1945 }
1946 }
1947 }
1948
1949 #[rstest]
1951 fn prop_quantity_addition_subtraction_inverse(
1952 base in quantity_value_strategy().prop_filter("Reasonable values", |&x| x < 1e6),
1953 delta in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
1954 precision in precision_strategy()
1955 ) {
1956 let q_base = Quantity::new(base, precision);
1957 let q_delta = Quantity::new(delta, precision);
1958
1959 if let Some(added_raw) = q_base.raw.checked_add(q_delta.raw)
1961 && let Some(result_raw) = added_raw.checked_sub(q_delta.raw) {
1962 prop_assert_eq!(result_raw, q_base.raw, "Inverse operation failed in raw arithmetic");
1964 }
1965 }
1966
1967 #[rstest]
1970 fn prop_quantity_checked_add_matches_spec(
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 let expected = q_a.raw
1978 .checked_add(q_b.raw)
1979 .filter(|r| *r <= QUANTITY_RAW_MAX)
1980 .filter(|_| q_a.raw != QUANTITY_UNDEF && q_b.raw != QUANTITY_UNDEF)
1981 .map(|raw| Quantity { raw, precision: q_a.precision.max(q_b.precision) });
1982 prop_assert_eq!(q_a.checked_add(q_b), expected);
1983 }
1984
1985 #[rstest]
1988 fn prop_quantity_checked_sub_matches_spec(
1989 a in quantity_value_strategy(),
1990 b in quantity_value_strategy(),
1991 precision in precision_strategy()
1992 ) {
1993 let q_a = Quantity::new(a, precision);
1994 let q_b = Quantity::new(b, precision);
1995 let expected = q_a.raw
1996 .checked_sub(q_b.raw)
1997 .filter(|_| q_a.raw != QUANTITY_UNDEF && q_b.raw != QUANTITY_UNDEF)
1998 .map(|raw| Quantity { raw, precision: q_a.precision.max(q_b.precision) });
1999 prop_assert_eq!(q_a.checked_sub(q_b), expected);
2000 }
2001
2002 #[rstest]
2004 fn prop_quantity_ordering_transitive(
2005 a in quantity_value_strategy(),
2006 b in quantity_value_strategy(),
2007 c in quantity_value_strategy(),
2008 precision in precision_strategy()
2009 ) {
2010 let q_a = Quantity::new(a, precision);
2011 let q_b = Quantity::new(b, precision);
2012 let q_c = Quantity::new(c, precision);
2013
2014 if q_a <= q_b && q_b <= q_c {
2016 prop_assert!(q_a <= q_c, "Transitivity failed: {} <= {} <= {} but {} > {}",
2017 q_a.as_f64(), q_b.as_f64(), q_c.as_f64(), q_a.as_f64(), q_c.as_f64());
2018 }
2019 }
2020
2021 #[rstest]
2023 fn prop_quantity_string_parsing_precision(
2024 integral in 0u32..1_000_000,
2025 fractional in 0u32..1_000_000,
2026 precision in precision_strategy_non_zero()
2027 ) {
2028 let pow = 10u128.pow(u32::from(precision));
2030 let fractional_mod = u128::from(fractional) % pow;
2031 let fractional_str = format!("{:0width$}", fractional_mod, width = precision as usize);
2032 let quantity_str = format!("{integral}.{fractional_str}");
2033
2034 let parsed: Quantity = quantity_str.parse().unwrap();
2035 prop_assert_eq!(parsed.precision, precision);
2036
2037 let round_trip = parsed.to_string();
2039 let expected_value = format!("{integral}.{fractional_str}");
2040 prop_assert_eq!(round_trip, expected_value);
2041 }
2042
2043 #[rstest]
2045 fn prop_quantity_precision_information_preservation(
2046 value in quantity_value_strategy().prop_filter("Reasonable values", |&x| x < 1e6),
2047 precision1 in precision_strategy_non_zero(),
2048 precision2 in precision_strategy_non_zero()
2049 ) {
2050 prop_assume!(precision1 != precision2);
2052
2053 let _q1 = Quantity::new(value, precision1);
2054 let _q2 = Quantity::new(value, precision2);
2055
2056 let min_precision = precision1.min(precision2);
2059
2060 let scale = 10.0_f64.powi(i32::from(min_precision));
2062 let rounded_value = (value * scale).round() / scale;
2063
2064 let q1_reduced = Quantity::new(rounded_value, min_precision);
2065 let q2_reduced = Quantity::new(rounded_value, min_precision);
2066
2067 prop_assert_eq!(q1_reduced.raw, q2_reduced.raw, "Precision reduction inconsistent");
2069 }
2070
2071 #[rstest]
2073 fn prop_quantity_arithmetic_bounds(
2074 a in quantity_value_strategy(),
2075 b in quantity_value_strategy(),
2076 precision in precision_strategy()
2077 ) {
2078 let q_a = Quantity::new(a, precision);
2079 let q_b = Quantity::new(b, precision);
2080
2081 let sum_f64 = q_a.as_f64() + q_b.as_f64();
2083 if sum_f64.is_finite() && (QUANTITY_MIN..=QUANTITY_MAX).contains(&sum_f64) {
2084 let sum = q_a + q_b;
2085 prop_assert!(sum.as_f64().is_finite());
2086 prop_assert!(!sum.is_undefined());
2087 }
2088
2089 let diff_f64 = q_a.as_f64() - q_b.as_f64();
2091 if diff_f64.is_finite() && (QUANTITY_MIN..=QUANTITY_MAX).contains(&diff_f64) {
2092 let diff = q_a - q_b;
2093 prop_assert!(diff.as_f64().is_finite());
2094 prop_assert!(!diff.is_undefined());
2095 }
2096 }
2097
2098 #[rstest]
2100 fn prop_quantity_multiplication_non_negative(
2101 a in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 0.0 && x < 10.0),
2102 b in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 0.0 && x < 10.0),
2103 precision in precision_strategy()
2104 ) {
2105 let q_a = Quantity::new(a, precision);
2106 let q_b = Quantity::new(b, precision);
2107
2108 let raw_product_check = q_a.raw.checked_mul(q_b.raw);
2110
2111 if let Some(raw_product) = raw_product_check {
2112 let scaled_raw = raw_product / (FIXED_SCALAR as QuantityRaw);
2114 if scaled_raw <= QUANTITY_RAW_MAX {
2115 let product = q_a * q_b;
2117 prop_assert!(product.as_f64() >= 0.0, "Quantity multiplication produced negative value: {}", product.as_f64());
2118 }
2119 }
2120 }
2121
2122 #[rstest]
2124 fn prop_quantity_zero_addition_identity(
2125 value in quantity_value_strategy(),
2126 precision in precision_strategy()
2127 ) {
2128 let q = Quantity::new(value, precision);
2129 let zero = Quantity::zero(precision);
2130
2131 prop_assert_eq!(q + zero, q);
2133 prop_assert_eq!(zero + q, q);
2134 }
2135 }
2136
2137 proptest! {
2138 #[rstest]
2140 fn prop_quantity_as_decimal_preserves_precision(
2141 (raw, precision) in raw_for_precision_strategy()
2142 ) {
2143 prop_assume!(decimal_compatible(raw, precision));
2144 let quantity = Quantity::from_raw(raw, precision);
2145 let decimal = quantity.as_decimal();
2146 prop_assert_eq!(decimal.scale(), u32::from(precision));
2147 }
2148
2149 #[rstest]
2151 fn prop_quantity_as_decimal_matches_display(
2152 (raw, precision) in raw_for_precision_strategy()
2153 ) {
2154 prop_assume!(decimal_compatible(raw, precision));
2155 let quantity = Quantity::from_raw(raw, precision);
2156 let display_str = format!("{quantity}");
2157 let decimal_str = quantity.as_decimal().to_string();
2158 prop_assert_eq!(display_str, decimal_str);
2159 }
2160
2161 #[rstest]
2163 fn prop_quantity_from_decimal_roundtrip(
2164 (raw, precision) in raw_for_precision_strategy()
2165 ) {
2166 prop_assume!(decimal_compatible(raw, precision));
2167 let original = Quantity::from_raw(raw, precision);
2168 let decimal = original.as_decimal();
2169 let reconstructed = Quantity::from_decimal(decimal).unwrap();
2170 prop_assert_eq!(original.raw, reconstructed.raw);
2171 prop_assert_eq!(original.precision, reconstructed.precision);
2172 }
2173
2174 #[rstest]
2176 fn prop_quantity_from_raw_round_trip(
2177 (raw, precision) in raw_for_precision_strategy()
2178 ) {
2179 let quantity = Quantity::from_raw(raw, precision);
2180 prop_assert_eq!(quantity.raw, raw);
2181 prop_assert_eq!(quantity.precision, precision);
2182 }
2183 }
2184}