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