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