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]
395 pub fn as_f64(&self) -> f64 {
396 fixed_u64_to_f64(self.raw)
397 }
398
399 #[must_use]
401 pub fn as_decimal(&self) -> Decimal {
402 let precision_diff = FIXED_PRECISION.saturating_sub(self.precision);
404 let rescaled_raw = self.raw / QuantityRaw::pow(10, u32::from(precision_diff));
405
406 #[allow(
410 clippy::unnecessary_cast,
411 clippy::cast_lossless,
412 reason = "cast is real when QuantityRaw is u64, no-op when u128"
413 )]
414 scaled_raw_to_decimal(rescaled_raw as i128, self.precision)
415 }
416
417 #[must_use]
419 #[allow(
420 clippy::unnecessary_fallible_conversions,
421 reason = "try_from is infallible when QuantityRaw is u64, fallible when u128"
422 )]
423 pub(crate) fn raw_as_decimal(raw: QuantityRaw) -> Decimal {
424 let whole =
425 i128::try_from(raw / FIXED_SCALAR_RAW).expect("Whole raw quantity must fit in Decimal");
426 let fractional = i128::try_from(raw % FIXED_SCALAR_RAW)
427 .expect("Fractional raw quantity must fit in Decimal");
428
429 Decimal::from(whole) + Decimal::from_i128_with_scale(fractional, u32::from(FIXED_PRECISION))
430 }
431
432 #[must_use]
434 pub fn to_formatted_string(&self) -> String {
435 format!("{self}").separate_with_underscores()
436 }
437
438 pub fn from_decimal_dp(decimal: Decimal, precision: u8) -> CorrectnessResult<Self> {
451 if decimal.mantissa() < 0 {
452 return Err(CorrectnessError::PredicateViolation {
453 message: format!(
454 "Decimal value '{decimal}' is negative, Quantity must be non-negative"
455 ),
456 });
457 }
458
459 let exponent = -(decimal.scale() as i8);
460 let raw_i128 = mantissa_exponent_to_fixed_i128(decimal.mantissa(), exponent, precision)?;
461
462 let raw: QuantityRaw =
463 raw_i128
464 .try_into()
465 .map_err(|_| CorrectnessError::PredicateViolation {
466 message: format!(
467 "Decimal value exceeds QuantityRaw range [0, {QUANTITY_RAW_MAX}]"
468 ),
469 })?;
470
471 if raw > QUANTITY_RAW_MAX {
472 return Err(CorrectnessError::PredicateViolation {
473 message: format!(
474 "Raw value {raw} exceeds QUANTITY_RAW_MAX={QUANTITY_RAW_MAX} for Quantity"
475 ),
476 });
477 }
478
479 Ok(Self { raw, precision })
480 }
481
482 pub fn from_decimal(decimal: Decimal) -> CorrectnessResult<Self> {
494 let precision = decimal.scale() as u8;
495 Self::from_decimal_dp(decimal, precision)
496 }
497
498 #[must_use]
507 pub fn from_mantissa_exponent(mantissa: u64, exponent: i8, precision: u8) -> Self {
508 check_fixed_precision(precision).expect_display(FAILED);
509
510 if mantissa == 0 {
511 return Self { raw: 0, precision };
512 }
513
514 let raw_i128 = mantissa_exponent_to_fixed_i128(i128::from(mantissa), exponent, precision)
515 .expect("Overflow in Quantity::from_mantissa_exponent");
516
517 let raw: QuantityRaw = raw_i128
518 .try_into()
519 .expect("Raw value exceeds QuantityRaw range in Quantity::from_mantissa_exponent");
520 assert!(
521 raw <= QUANTITY_RAW_MAX,
522 "`raw` value {raw} exceeded QUANTITY_RAW_MAX={QUANTITY_RAW_MAX} for Quantity"
523 );
524
525 Self { raw, precision }
526 }
527
528 pub fn from_mantissa_exponent_checked(
535 mantissa: u64,
536 exponent: i8,
537 precision: u8,
538 ) -> CorrectnessResult<Self> {
539 let raw = mantissa_exponent_to_raw_checked::<QuantityRaw>(
540 i128::from(mantissa),
541 exponent,
542 precision,
543 "Quantity::from_mantissa_exponent",
544 "QuantityRaw",
545 "Quantity",
546 )?;
547
548 Self::from_raw_checked(raw, precision)
549 }
550
551 #[cfg(feature = "defi")]
559 pub fn from_u256(amount: U256, precision: u8) -> CorrectnessResult<Self> {
560 let scaled_amount = if precision < FIXED_PRECISION {
562 amount
563 .checked_mul(U256::from(
564 10u128.pow(u32::from(FIXED_PRECISION - precision)),
565 ))
566 .ok_or_else(|| CorrectnessError::PredicateViolation {
567 message: format!(
568 "Amount overflow during scaling to fixed precision: {} * 10^{}",
569 amount,
570 FIXED_PRECISION - precision
571 ),
572 })?
573 } else {
574 amount
575 };
576
577 let raw = QuantityRaw::try_from(scaled_amount).map_err(|_| {
578 CorrectnessError::PredicateViolation {
579 message: format!("U256 scaled amount {scaled_amount} exceeds QuantityRaw range"),
580 }
581 })?;
582
583 Self::from_raw_checked(raw, precision)
584 }
585}
586
587impl From<Quantity> for f64 {
588 fn from(qty: Quantity) -> Self {
589 qty.as_f64()
590 }
591}
592
593impl From<&Quantity> for f64 {
594 fn from(qty: &Quantity) -> Self {
595 qty.as_f64()
596 }
597}
598
599impl From<i32> for Quantity {
600 fn from(value: i32) -> Self {
606 assert!(
607 value >= 0,
608 "Cannot create Quantity from negative i32: {value}. Use u32 or check value is non-negative."
609 );
610 Self::from_mantissa_exponent(u64::from(value.cast_unsigned()), 0, 0)
611 }
612}
613
614impl From<i64> for Quantity {
615 fn from(value: i64) -> Self {
621 assert!(
622 value >= 0,
623 "Cannot create Quantity from negative i64: {value}. Use u64 or check value is non-negative."
624 );
625 Self::from_mantissa_exponent(value.cast_unsigned(), 0, 0)
626 }
627}
628
629impl From<u32> for Quantity {
630 fn from(value: u32) -> Self {
631 Self::from_mantissa_exponent(u64::from(value), 0, 0)
632 }
633}
634
635impl From<u64> for Quantity {
636 fn from(value: u64) -> Self {
637 Self::from_mantissa_exponent(value, 0, 0)
638 }
639}
640
641impl Hash for Quantity {
642 fn hash<H: Hasher>(&self, state: &mut H) {
643 self.raw.hash(state);
644 }
645}
646
647impl PartialEq for Quantity {
648 fn eq(&self, other: &Self) -> bool {
649 self.raw == other.raw
650 }
651}
652
653impl PartialOrd for Quantity {
654 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
655 Some(self.cmp(other))
656 }
657
658 fn lt(&self, other: &Self) -> bool {
659 self.raw.lt(&other.raw)
660 }
661
662 fn le(&self, other: &Self) -> bool {
663 self.raw.le(&other.raw)
664 }
665
666 fn gt(&self, other: &Self) -> bool {
667 self.raw.gt(&other.raw)
668 }
669
670 fn ge(&self, other: &Self) -> bool {
671 self.raw.ge(&other.raw)
672 }
673}
674
675impl Ord for Quantity {
676 fn cmp(&self, other: &Self) -> Ordering {
677 self.raw.cmp(&other.raw)
678 }
679}
680
681impl Deref for Quantity {
682 type Target = QuantityRaw;
683
684 fn deref(&self) -> &Self::Target {
685 &self.raw
686 }
687}
688
689impl Add for Quantity {
690 type Output = Self;
691 fn add(self, rhs: Self) -> Self::Output {
692 Self {
693 raw: self
694 .raw
695 .checked_add(rhs.raw)
696 .expect("Overflow occurred when adding `Quantity`"),
697 precision: self.precision.max(rhs.precision),
698 }
699 }
700}
701
702impl Sum for Quantity {
703 fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
704 iter.fold(Self::from(0), |acc, x| acc + x)
705 }
706}
707
708impl<'a> Sum<&'a Self> for Quantity {
709 fn sum<I: Iterator<Item = &'a Self>>(iter: I) -> Self {
710 iter.fold(Self::from(0), |acc, x| acc + *x)
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_sum_owned_quantities() {
1007 let quantities = [Quantity::new(1.25, 2), Quantity::new(2.75, 2)];
1008 let result: Quantity = quantities.into_iter().sum();
1009
1010 assert_eq!(result.as_decimal(), dec!(4.00));
1011 assert_eq!(result.precision, 2);
1012 }
1013
1014 #[rstest]
1015 fn test_sum_borrowed_quantities() {
1016 let quantities = [Quantity::new(0.125, 3), Quantity::new(0.375, 3)];
1017 let result: Quantity = quantities.iter().sum();
1018
1019 assert_eq!(result.as_decimal(), dec!(0.500));
1020 assert_eq!(result.precision, 3);
1021 }
1022
1023 #[rstest]
1024 fn test_sum_mixed_precision_quantities() {
1025 let quantities = [
1026 Quantity::new(1.2, 1),
1027 Quantity::new(3.45, 2),
1028 Quantity::new(0.006, 3),
1029 ];
1030 let result: Quantity = quantities.into_iter().sum();
1031
1032 assert_eq!(result.as_decimal(), dec!(4.656));
1033 assert_eq!(result.precision, 3);
1034 }
1035
1036 #[rstest]
1037 fn test_sum_empty_quantity_iterator() {
1038 let result: Quantity = std::iter::empty::<Quantity>().sum();
1039
1040 assert_eq!(result.as_decimal(), dec!(0));
1041 assert_eq!(result.precision, 0);
1042 }
1043
1044 #[rstest]
1045 fn test_mixed_precision_sub() {
1046 let q1 = Quantity::new(2.0, 1);
1047 let q2 = Quantity::new(1.0, 2);
1048 let result = q1 - q2;
1049 assert_eq!(result.precision, 2);
1050 assert_eq!(result.as_f64(), 1.0);
1051 }
1052
1053 #[rstest]
1054 fn test_mixed_precision_mul() {
1055 let q1 = Quantity::new(2.0, 1);
1056 let q2 = Quantity::new(3.0, 2);
1057 let result = q1 * q2;
1058 assert_eq!(result.precision, 2);
1059 assert_eq!(result.as_f64(), 6.0);
1060 }
1061
1062 #[rstest]
1063 fn test_new_non_zero_ok() {
1064 let qty = Quantity::non_zero_checked(123.456, 3).unwrap();
1065 assert_eq!(qty.raw, Quantity::new(123.456, 3).raw);
1066 assert!(qty.is_positive());
1067 }
1068
1069 #[rstest]
1070 fn test_new_non_zero_zero_input() {
1071 assert!(Quantity::non_zero_checked(0.0, 0).is_err());
1072 }
1073
1074 #[rstest]
1075 fn test_new_non_zero_rounds_to_zero() {
1076 assert!(Quantity::non_zero_checked(0.0004, 3).is_err());
1078 }
1079
1080 #[rstest]
1081 fn test_new_non_zero_negative() {
1082 assert!(Quantity::non_zero_checked(-1.0, 0).is_err());
1083 }
1084
1085 #[rstest]
1086 fn test_new_non_zero_exceeds_max() {
1087 assert!(Quantity::non_zero_checked(QUANTITY_MAX * 10.0, 0).is_err());
1088 }
1089
1090 #[rstest]
1091 fn test_new_non_zero_invalid_precision() {
1092 assert!(Quantity::non_zero_checked(1.0, FIXED_PRECISION + 1).is_err());
1093 }
1094
1095 #[rstest]
1096 fn test_new() {
1097 let value = 0.00812;
1098 let qty = Quantity::new(value, 8);
1099 assert_eq!(qty, qty);
1100 assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
1101 assert_eq!(qty.precision, 8);
1102 assert_eq!(qty, Quantity::from("0.00812000"));
1103 assert_eq!(qty.as_decimal(), dec!(0.00812000));
1104 assert_eq!(qty.to_string(), "0.00812000");
1105 assert!(!qty.is_zero());
1106 assert!(qty.is_positive());
1107 assert!(approx_eq!(f64, qty.as_f64(), 0.00812, epsilon = 0.000_001));
1108 }
1109
1110 #[rstest]
1111 fn test_check_quantity_positive_ok() {
1112 let qty = Quantity::new(10.0, 0);
1113 check_positive_quantity(qty, "qty").unwrap();
1114 }
1115
1116 #[rstest]
1117 fn test_negative_quantity_validation() {
1118 assert!(Quantity::new_checked(-1.0, FIXED_PRECISION).is_err());
1119 }
1120
1121 #[rstest]
1122 fn test_new_checked_returns_typed_error_with_stable_display() {
1123 let error = Quantity::new_checked(QUANTITY_MAX + 1.0, FIXED_PRECISION).unwrap_err();
1124
1125 assert!(matches!(error, CorrectnessError::OutOfRange { .. }));
1126 assert_eq!(
1127 error.to_string(),
1128 format!(
1129 "invalid f64 for 'value' not in range [{QUANTITY_MIN}, {QUANTITY_MAX}], was {}",
1130 QUANTITY_MAX + 1.0
1131 )
1132 );
1133 }
1134
1135 #[rstest]
1136 fn test_from_raw_checked_returns_typed_error_with_stable_display() {
1137 let error = Quantity::from_raw_checked(QUANTITY_UNDEF, 3).unwrap_err();
1138
1139 assert_eq!(
1140 error,
1141 CorrectnessError::PredicateViolation {
1142 message: "`precision` must be 0 when `raw` is QUANTITY_UNDEF".to_string(),
1143 }
1144 );
1145 assert_eq!(
1146 error.to_string(),
1147 "`precision` must be 0 when `raw` is QUANTITY_UNDEF"
1148 );
1149 }
1150
1151 #[rstest]
1152 fn test_undefined() {
1153 let qty = Quantity::from_raw(QUANTITY_UNDEF, 0);
1154 assert_eq!(qty.raw, QUANTITY_UNDEF);
1155 assert!(qty.is_undefined());
1156 }
1157
1158 #[rstest]
1159 fn test_zero() {
1160 let qty = Quantity::zero(8);
1161 assert_eq!(qty.raw, 0);
1162 assert_eq!(qty.precision, 8);
1163 assert!(qty.is_zero());
1164 assert!(!qty.is_positive());
1165 }
1166
1167 #[rstest]
1168 fn test_from_i32_exact() {
1169 let values = [0, 1, i32::MAX];
1170 let quantities = values.map(Quantity::from);
1171 let expected =
1172 values.map(|value| (QuantityRaw::try_from(value).unwrap() * FIXED_SCALAR_RAW, 0));
1173
1174 assert_eq!(
1175 quantities.map(|quantity| (quantity.raw, quantity.precision)),
1176 expected
1177 );
1178 }
1179
1180 #[rstest]
1181 fn test_from_i64_exact() {
1182 let max = quantity_max_i64();
1183 let values = [0, 1, max];
1184 let quantities = values.map(Quantity::from);
1185 let expected =
1186 values.map(|value| (QuantityRaw::try_from(value).unwrap() * FIXED_SCALAR_RAW, 0));
1187
1188 assert_eq!(
1189 quantities.map(|quantity| (quantity.raw, quantity.precision)),
1190 expected
1191 );
1192 }
1193
1194 #[rstest]
1195 fn test_from_u32_exact() {
1196 let values = [0, 1, u32::MAX];
1197 let quantities = values.map(Quantity::from);
1198 let expected = values.map(|value| (QuantityRaw::from(value) * FIXED_SCALAR_RAW, 0));
1199
1200 assert_eq!(
1201 quantities.map(|quantity| (quantity.raw, quantity.precision)),
1202 expected
1203 );
1204 }
1205
1206 #[rstest]
1207 fn test_from_u64_exact() {
1208 let max = quantity_max_u64();
1209 let values = [0, 1, max];
1210 let quantities = values.map(Quantity::from);
1211 let expected = values.map(|value| (QuantityRaw::from(value) * FIXED_SCALAR_RAW, 0));
1212
1213 assert_eq!(
1214 quantities.map(|quantity| (quantity.raw, quantity.precision)),
1215 expected
1216 );
1217 }
1218
1219 #[rstest]
1220 #[should_panic(
1221 expected = "Cannot create Quantity from negative i32: -1. Use u32 or check value is non-negative."
1222 )]
1223 fn test_from_i32_negative_panics() {
1224 let _ = Quantity::from(-1_i32);
1225 }
1226
1227 #[rstest]
1228 #[should_panic(
1229 expected = "Cannot create Quantity from negative i64: -1. Use u64 or check value is non-negative."
1230 )]
1231 fn test_from_i64_negative_panics() {
1232 let _ = Quantity::from(-1_i64);
1233 }
1234
1235 #[rstest]
1236 #[cfg_attr(
1237 feature = "high-precision",
1238 should_panic(expected = "exceeded QUANTITY_RAW_MAX")
1239 )]
1240 #[cfg_attr(
1241 not(feature = "high-precision"),
1242 should_panic(expected = "Raw value exceeds QuantityRaw range")
1243 )]
1244 fn test_from_i64_overflow_panics() {
1245 let max = quantity_max_i64();
1246
1247 let _ = Quantity::from(max + 1);
1248 }
1249
1250 #[rstest]
1251 #[cfg_attr(
1252 feature = "high-precision",
1253 should_panic(expected = "exceeded QUANTITY_RAW_MAX")
1254 )]
1255 #[cfg_attr(
1256 not(feature = "high-precision"),
1257 should_panic(expected = "Raw value exceeds QuantityRaw range")
1258 )]
1259 fn test_from_u64_overflow_panics() {
1260 let max = quantity_max_u64();
1261
1262 let _ = Quantity::from(max + 1);
1263 }
1264
1265 fn quantity_max_i64() -> i64 {
1266 i64::try_from(QUANTITY_RAW_MAX / FIXED_SCALAR_RAW).unwrap()
1267 }
1268
1269 #[allow(
1270 clippy::useless_conversion,
1271 reason = "try_from is a no-op when QuantityRaw is u64, and narrows when u128 (high-precision)"
1272 )]
1273 fn quantity_max_u64() -> u64 {
1274 u64::try_from(QUANTITY_RAW_MAX / FIXED_SCALAR_RAW).unwrap()
1275 }
1276
1277 #[rstest] fn test_with_maximum_value() {
1279 let qty = Quantity::new_checked(QUANTITY_MAX, 0);
1280 assert!(qty.is_ok());
1281 }
1282
1283 #[rstest]
1284 fn test_with_minimum_positive_value() {
1285 let value = 0.000_000_001;
1286 let qty = Quantity::new(value, 9);
1287 assert_eq!(qty.raw, Quantity::from("0.000000001").raw);
1288 assert_eq!(qty.as_decimal(), dec!(0.000000001));
1289 assert_eq!(qty.to_string(), "0.000000001");
1290 }
1291
1292 #[rstest]
1293 fn test_with_minimum_value() {
1294 let qty = Quantity::new(QUANTITY_MIN, 9);
1295 assert_eq!(qty.raw, 0);
1296 assert_eq!(qty.as_decimal(), dec!(0));
1297 assert_eq!(qty.to_string(), "0.000000000");
1298 }
1299
1300 #[rstest]
1301 fn test_is_zero() {
1302 let qty = Quantity::zero(8);
1303 assert_eq!(qty, qty);
1304 assert_eq!(qty.raw, 0);
1305 assert_eq!(qty.precision, 8);
1306 assert_eq!(qty, Quantity::from("0.00000000"));
1307 assert_eq!(qty.as_decimal(), dec!(0));
1308 assert_eq!(qty.to_string(), "0.00000000");
1309 assert!(qty.is_zero());
1310 }
1311
1312 #[rstest]
1313 fn test_precision() {
1314 let value = 1.001;
1315 let qty = Quantity::new(value, 2);
1316 assert_eq!(qty.to_string(), "1.00");
1317 }
1318
1319 #[rstest]
1320 fn test_new_from_str() {
1321 let qty = Quantity::new(0.008_120_00, 8);
1322 assert_eq!(qty, qty);
1323 assert_eq!(qty.precision, 8);
1324 assert_eq!(qty, Quantity::from("0.00812000"));
1325 assert_eq!(qty.to_string(), "0.00812000");
1326 }
1327
1328 #[rstest]
1329 #[case("0", 0)]
1330 #[case("1.1", 1)]
1331 #[case("1.123456789", 9)]
1332 fn test_from_str_valid_input(#[case] input: &str, #[case] expected_prec: u8) {
1333 let qty = Quantity::from(input);
1334 assert_eq!(qty.precision, expected_prec);
1335 assert_eq!(qty.as_decimal(), Decimal::from_str(input).unwrap());
1336 }
1337
1338 #[rstest]
1339 #[should_panic(expected = "ParseFloatError")]
1340 fn test_from_str_invalid_input() {
1341 let input = "invalid";
1342 let _ = Quantity::new(f64::from_str(input).unwrap(), 8);
1343 }
1344
1345 #[rstest]
1346 fn test_from_str_errors() {
1347 assert!(Quantity::from_str("invalid").is_err());
1348 assert!(Quantity::from_str("12.34.56").is_err());
1349 assert!(Quantity::from_str("").is_err());
1350 assert!(Quantity::from_str("-1").is_err()); assert!(Quantity::from_str("-0.001").is_err());
1352 }
1353
1354 #[rstest]
1355 #[case("1e7", 0, 10_000_000.0)]
1356 #[case("2.5e3", 0, 2_500.0)]
1357 #[case("1.234e-2", 5, 0.01234)]
1358 #[case("5E-3", 3, 0.005)]
1359 #[case("1.0e6", 0, 1_000_000.0)]
1360 fn test_from_str_scientific_notation(
1361 #[case] input: &str,
1362 #[case] expected_precision: u8,
1363 #[case] expected_value: f64,
1364 ) {
1365 let qty = Quantity::from_str(input).unwrap();
1366 assert_eq!(qty.precision, expected_precision);
1367 assert!(approx_eq!(
1368 f64,
1369 qty.as_f64(),
1370 expected_value,
1371 epsilon = 1e-10
1372 ));
1373 }
1374
1375 #[rstest]
1376 #[case("1_234.56", 2, 1234.56)]
1377 #[case("1000000", 0, 1_000_000.0)]
1378 #[case("99_999.999_99", 5, 99_999.999_99)]
1379 fn test_from_str_with_underscores(
1380 #[case] input: &str,
1381 #[case] expected_precision: u8,
1382 #[case] expected_value: f64,
1383 ) {
1384 let qty = Quantity::from_str(input).unwrap();
1385 assert_eq!(qty.precision, expected_precision);
1386 assert!(approx_eq!(
1387 f64,
1388 qty.as_f64(),
1389 expected_value,
1390 epsilon = 1e-10
1391 ));
1392 }
1393
1394 #[rstest]
1395 fn test_from_decimal_dp_preservation() {
1396 let decimal = dec!(123.456789);
1398 let qty = Quantity::from_decimal_dp(decimal, 6).unwrap();
1399 assert_eq!(qty.precision, 6);
1400 assert!(approx_eq!(f64, qty.as_f64(), 123.456_789, epsilon = 1e-10));
1401
1402 let expected_raw = 123_456_789_u64 * 10_u64.pow(u32::from(FIXED_PRECISION - 6));
1404 assert_eq!(qty.raw, QuantityRaw::from(expected_raw));
1405 }
1406
1407 #[rstest]
1408 fn test_from_decimal_dp_rounding() {
1409 let decimal = dec!(1.005);
1411 let qty = Quantity::from_decimal_dp(decimal, 2).unwrap();
1412 assert_eq!(qty.as_f64(), 1.0); let decimal = dec!(1.015);
1415 let qty = Quantity::from_decimal_dp(decimal, 2).unwrap();
1416 assert_eq!(qty.as_f64(), 1.02); }
1418
1419 #[rstest]
1420 fn test_from_decimal_infers_precision() {
1421 let decimal = dec!(123.456);
1423 let qty = Quantity::from_decimal(decimal).unwrap();
1424 assert_eq!(qty.precision, 3);
1425 assert!(approx_eq!(f64, qty.as_f64(), 123.456, epsilon = 1e-10));
1426
1427 let decimal = dec!(100);
1429 let qty = Quantity::from_decimal(decimal).unwrap();
1430 assert_eq!(qty.precision, 0);
1431 assert_eq!(qty.as_f64(), 100.0);
1432
1433 let decimal = dec!(1.23456789);
1435 let qty = Quantity::from_decimal(decimal).unwrap();
1436 assert_eq!(qty.precision, 8);
1437 assert!(approx_eq!(f64, qty.as_f64(), 1.234_567_89, epsilon = 1e-10));
1438 }
1439
1440 #[rstest]
1441 fn test_from_decimal_trailing_zeros() {
1442 let decimal = dec!(5.670);
1444 assert_eq!(decimal.scale(), 3); let qty = Quantity::from_decimal(decimal).unwrap();
1448 assert_eq!(qty.precision, 3);
1449 assert!(approx_eq!(f64, qty.as_f64(), 5.67, epsilon = 1e-10));
1450
1451 let normalized = decimal.normalize();
1453 assert_eq!(normalized.scale(), 2);
1454 let qty_normalized = Quantity::from_decimal(normalized).unwrap();
1455 assert_eq!(qty_normalized.precision, 2);
1456 }
1457
1458 #[rstest]
1459 #[case("1.00", 2)]
1460 #[case("1.0", 1)]
1461 #[case("1.000", 3)]
1462 #[case("100.00", 2)]
1463 #[case("0.10", 2)]
1464 #[case("0.100", 3)]
1465 fn test_from_str_preserves_trailing_zeros(#[case] input: &str, #[case] expected_precision: u8) {
1466 let qty = Quantity::from_str(input).unwrap();
1467 assert_eq!(qty.precision, expected_precision);
1468 }
1469
1470 #[rstest]
1471 fn test_from_decimal_excessive_precision_inference() {
1472 let decimal = dec!(1.1234567890123456789012345678);
1475
1476 if decimal.scale() > u32::from(FIXED_PRECISION) {
1478 assert!(Quantity::from_decimal(decimal).is_err());
1479 }
1480 }
1481
1482 #[rstest]
1483 fn test_from_decimal_negative_quantity_errors() {
1484 let decimal = dec!(-123.45);
1486 let result = Quantity::from_decimal(decimal);
1487 assert!(result.is_err());
1488
1489 let result = Quantity::from_decimal_dp(decimal, 2);
1491 assert!(result.is_err());
1492 }
1493
1494 #[rstest]
1495 fn test_from_decimal_dp_negative_returns_typed_error_with_stable_display() {
1496 let error = Quantity::from_decimal_dp(dec!(-1.5), 2).unwrap_err();
1497 assert_eq!(
1498 error,
1499 CorrectnessError::PredicateViolation {
1500 message: "Decimal value '-1.5' is negative, Quantity must be non-negative"
1501 .to_string(),
1502 }
1503 );
1504 assert_eq!(
1505 error.to_string(),
1506 "Decimal value '-1.5' is negative, Quantity must be non-negative",
1507 );
1508 }
1509
1510 #[cfg(not(feature = "high-precision"))]
1511 #[rstest]
1512 fn test_from_decimal_dp_rejects_quantity_raw_overflow() {
1513 let error = Quantity::from_decimal_dp(dec!(20_000_000_000), 0).unwrap_err();
1514
1515 assert_eq!(
1516 error,
1517 CorrectnessError::PredicateViolation {
1518 message: format!("Decimal value exceeds QuantityRaw range [0, {QUANTITY_RAW_MAX}]"),
1519 }
1520 );
1521 }
1522
1523 #[cfg(feature = "high-precision")]
1524 #[rstest]
1525 fn test_from_decimal_dp_rejects_value_above_quantity_max() {
1526 let value = 34_028_236_692_094_u64;
1527 let raw = u128::from(value) * FIXED_SCALAR_RAW;
1528 let error = Quantity::from_decimal_dp(Decimal::from(value), 0).unwrap_err();
1529
1530 assert_eq!(
1531 error,
1532 CorrectnessError::PredicateViolation {
1533 message: format!(
1534 "Raw value {raw} exceeds QUANTITY_RAW_MAX={QUANTITY_RAW_MAX} for Quantity"
1535 ),
1536 }
1537 );
1538 }
1539
1540 #[rstest]
1541 fn test_add() {
1542 let a = 1.0;
1543 let b = 2.0;
1544 let quantity1 = Quantity::new(1.0, 0);
1545 let quantity2 = Quantity::new(2.0, 0);
1546 let quantity3 = quantity1 + quantity2;
1547 assert_eq!(quantity3.raw, Quantity::new(a + b, 0).raw);
1548 }
1549
1550 #[rstest]
1551 fn test_sub() {
1552 let a = 3.0;
1553 let b = 2.0;
1554 let quantity1 = Quantity::new(a, 0);
1555 let quantity2 = Quantity::new(b, 0);
1556 let quantity3 = quantity1 - quantity2;
1557 assert_eq!(quantity3.raw, Quantity::new(a - b, 0).raw);
1558 }
1559
1560 #[rstest]
1561 fn test_quantity_checked_add_within_bounds() {
1562 let a = Quantity::new(10.0, 2);
1563 let b = Quantity::new(5.0, 2);
1564 assert_eq!(a.checked_add(b), Some(Quantity::new(15.0, 2)));
1565 }
1566
1567 #[rstest]
1568 fn test_quantity_checked_add_above_max_returns_none() {
1569 let near_max = Quantity::from_raw(QUANTITY_RAW_MAX, 0);
1570 let one = Quantity::new(1.0, 0);
1571 assert_eq!(near_max.checked_add(one), None);
1572 }
1573
1574 #[rstest]
1575 fn test_quantity_checked_sub_within_bounds() {
1576 let a = Quantity::new(10.0, 2);
1577 let b = Quantity::new(3.0, 2);
1578 assert_eq!(a.checked_sub(b), Some(Quantity::new(7.0, 2)));
1579 }
1580
1581 #[rstest]
1582 fn test_quantity_checked_sub_underflow_returns_none() {
1583 let a = Quantity::new(3.0, 2);
1584 let b = Quantity::new(10.0, 2);
1585 assert_eq!(a.checked_sub(b), None);
1586 }
1587
1588 #[rstest]
1589 fn test_quantity_checked_sub_to_zero() {
1590 let a = Quantity::new(5.0, 2);
1591 assert_eq!(a.checked_sub(a), Some(Quantity::zero(2)));
1592 }
1593
1594 #[rstest]
1595 fn test_quantity_checked_arith_rejects_undef() {
1596 let undef = Quantity::from_raw(QUANTITY_UNDEF, 0);
1597 let one = Quantity::new(1.0, 0);
1598 assert_eq!(undef.checked_add(one), None);
1599 assert_eq!(one.checked_add(undef), None);
1600 assert_eq!(undef.checked_sub(one), None);
1601 assert_eq!(one.checked_sub(undef), None);
1602 }
1603
1604 #[rstest]
1605 fn test_quantity_checked_add_at_exact_max_returns_some() {
1606 let near_max = Quantity::from_raw(QUANTITY_RAW_MAX - 1, 0);
1607 let one_unit = Quantity::from_raw(1, 0);
1608 assert_eq!(
1609 near_max.checked_add(one_unit),
1610 Some(Quantity::from_raw(QUANTITY_RAW_MAX, 0)),
1611 );
1612 }
1613
1614 #[rstest]
1615 fn test_quantity_checked_arith_uses_max_precision() {
1616 let a = Quantity::new(10.5, 1);
1617 let b = Quantity::new(2.25, 2);
1618 let sum = a.checked_add(b).unwrap();
1619 assert_eq!(sum.precision, 2);
1620 assert_eq!(sum.as_f64(), 12.75);
1621
1622 let diff = a.checked_sub(b).unwrap();
1623 assert_eq!(diff.precision, 2);
1624 assert_eq!(diff.as_f64(), 8.25);
1625 }
1626
1627 #[rstest]
1628 fn test_mul() {
1629 let value = 2.0;
1630 let quantity1 = Quantity::new(value, 1);
1631 let quantity2 = Quantity::new(value, 1);
1632 let quantity3 = quantity1 * quantity2;
1633 assert_eq!(quantity3.raw, Quantity::new(value * value, 0).raw);
1634 }
1635
1636 #[rstest]
1637 fn test_mul_avoids_intermediate_raw_overflow() {
1638 let scalar = FIXED_SCALAR_RAW;
1639 #[cfg(feature = "high-precision")]
1640 let (lhs_raw, rhs_raw, expected_raw) =
1641 (100_000 * scalar, 100 * scalar, 10_000_000 * scalar);
1642 #[cfg(not(feature = "high-precision"))]
1643 let (lhs_raw, rhs_raw, expected_raw) = (
1644 9_000_000_000 * scalar,
1645 2 * scalar + 1,
1646 18_000_000_009 * scalar,
1647 );
1648 let lhs = Quantity::from_raw(lhs_raw, FIXED_PRECISION);
1649 let rhs = Quantity::from_raw(rhs_raw, FIXED_PRECISION);
1650 let result = lhs * rhs;
1651
1652 assert_eq!(lhs_raw.checked_mul(rhs_raw), None);
1653 assert_eq!(result.raw, expected_raw);
1654 assert_eq!(result.precision, FIXED_PRECISION);
1655 }
1656
1657 #[rstest]
1658 #[should_panic(expected = "Overflow occurred when multiplying `Quantity`")]
1659 fn test_mul_panics_when_scaled_result_exceeds_quantity_max() {
1660 let lhs = Quantity::from_raw(QUANTITY_RAW_MAX, FIXED_PRECISION);
1661 let rhs = Quantity::from(2);
1662
1663 let _ = lhs * rhs;
1664 }
1665
1666 #[rstest]
1667 fn test_comparisons() {
1668 assert_eq!(Quantity::new(1.0, 1), Quantity::new(1.0, 1));
1669 assert_eq!(Quantity::new(1.0, 1), Quantity::new(1.0, 2));
1670 assert_ne!(Quantity::new(1.1, 1), Quantity::new(1.0, 1));
1671 assert!(Quantity::new(1.0, 1) <= Quantity::new(1.0, 2));
1672 assert!(Quantity::new(1.1, 1) > Quantity::new(1.0, 1));
1673 assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 1));
1674 assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 2));
1675 assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 2));
1676 assert!(Quantity::new(0.9, 1) < Quantity::new(1.0, 1));
1677 assert!(Quantity::new(0.9, 1) <= Quantity::new(1.0, 2));
1678 assert!(Quantity::new(0.9, 1) <= Quantity::new(1.0, 1));
1679 }
1680
1681 #[rstest]
1682 fn test_debug() {
1683 let quantity = Quantity::from_str("44.12").unwrap();
1684 let result = format!("{quantity:?}");
1685 assert_eq!(result, "Quantity(44.12)");
1686 }
1687
1688 #[rstest]
1689 fn test_display() {
1690 let quantity = Quantity::from_str("44.12").unwrap();
1691 let result = format!("{quantity}");
1692 assert_eq!(result, "44.12");
1693 }
1694
1695 #[rstest]
1696 #[case(44.12, 2, "Quantity(44.12)", "44.12")] #[case(1234.567, 8, "Quantity(1234.56700000)", "1234.56700000")] #[cfg_attr(
1699 feature = "defi",
1700 case(
1701 1_000_000_000_000_000_000.0,
1702 18,
1703 "Quantity(1000000000000000000)",
1704 "1000000000000000000"
1705 )
1706 )] fn test_debug_display_precision_handling(
1708 #[case] value: f64,
1709 #[case] precision: u8,
1710 #[case] expected_debug: &str,
1711 #[case] expected_display: &str,
1712 ) {
1713 let quantity = if precision > MAX_FLOAT_PRECISION {
1714 Quantity::from_raw(value as QuantityRaw, precision)
1716 } else {
1717 Quantity::new(value, precision)
1718 };
1719
1720 assert_eq!(format!("{quantity:?}"), expected_debug);
1721 assert_eq!(format!("{quantity}"), expected_display);
1722 }
1723
1724 #[rstest]
1725 fn test_to_formatted_string() {
1726 let qty = Quantity::new(1234.5678, 4);
1727 let formatted = qty.to_formatted_string();
1728 assert_eq!(formatted, "1_234.5678");
1729 assert_eq!(qty.to_string(), "1234.5678");
1730 }
1731
1732 #[rstest]
1733 fn test_saturating_sub() {
1734 let q1 = Quantity::new(100.0, 2);
1735 let q2 = Quantity::new(50.0, 2);
1736 let q3 = Quantity::new(150.0, 2);
1737
1738 let result = q1.saturating_sub(q2);
1739 assert_eq!(result, Quantity::new(50.0, 2));
1740
1741 let result = q1.saturating_sub(q3);
1742 assert_eq!(result, Quantity::zero(2));
1743 assert_eq!(result.raw, 0);
1744 }
1745
1746 #[rstest]
1747 fn test_saturating_sub_overflow_bug() {
1748 use crate::types::fixed::FIXED_PRECISION;
1751 let precision = 3;
1752 let scale = QuantityRaw::from(10u64.pow(u32::from(FIXED_PRECISION - precision)));
1753
1754 let peak_qty = Quantity::from_raw(79 * scale, precision);
1756 let order_qty = Quantity::from_raw(80 * scale, precision);
1757
1758 let result = peak_qty.saturating_sub(order_qty);
1760 assert_eq!(result.raw, 0);
1761 assert_eq!(result, Quantity::zero(precision));
1762 }
1763
1764 #[rstest]
1765 fn test_hash() {
1766 use std::{
1767 collections::hash_map::DefaultHasher,
1768 hash::{Hash, Hasher},
1769 };
1770
1771 let q1 = Quantity::new(100.0, 1);
1772 let q2 = Quantity::new(100.0, 1);
1773 let q3 = Quantity::new(200.0, 1);
1774
1775 let mut s1 = DefaultHasher::new();
1776 let mut s2 = DefaultHasher::new();
1777 let mut s3 = DefaultHasher::new();
1778
1779 q1.hash(&mut s1);
1780 q2.hash(&mut s2);
1781 q3.hash(&mut s3);
1782
1783 assert_eq!(
1784 s1.finish(),
1785 s2.finish(),
1786 "Equal quantities must hash equally"
1787 );
1788 assert_ne!(
1789 s1.finish(),
1790 s3.finish(),
1791 "Different quantities must hash differently"
1792 );
1793 }
1794
1795 #[rstest]
1796 fn test_quantity_serde_json_round_trip() {
1797 let original = Quantity::new(123.456, 3);
1798 let json_str = serde_json::to_string(&original).unwrap();
1799 assert_eq!(json_str, "\"123.456\"");
1800
1801 let deserialized: Quantity = serde_json::from_str(&json_str).unwrap();
1802 assert_eq!(deserialized, original);
1803 assert_eq!(deserialized.precision, 3);
1804 }
1805
1806 #[rstest]
1807 fn test_quantity_serde_json_from_value_round_trip() {
1808 let original = Quantity::new(123.456, 3);
1809 let value = serde_json::to_value(original).unwrap();
1810 assert_eq!(value, serde_json::json!("123.456"));
1811
1812 let deserialized: Quantity = serde_json::from_value(value).unwrap();
1813 assert_eq!(deserialized, original);
1814 assert_eq!(deserialized.precision, 3);
1815 }
1816
1817 #[rstest]
1818 fn test_quantity_deserialize_invalid_string_returns_error() {
1819 let result = serde_json::from_str::<Quantity>("\"not-a-quantity\"");
1820 let error = result.unwrap_err();
1821 assert!(
1822 error.to_string().contains("Error parsing"),
1823 "unexpected message: {error}"
1824 );
1825 }
1826
1827 #[rstest]
1828 fn test_quantity_deserialize_negative_returns_error() {
1829 let result = serde_json::from_str::<Quantity>("\"-1.5\"");
1830 let error = result.unwrap_err();
1831 assert!(
1832 error.to_string().contains("negative"),
1833 "unexpected message: {error}"
1834 );
1835 }
1836
1837 #[rstest]
1838 fn test_from_mantissa_exponent_exact_precision() {
1839 let qty = Quantity::from_mantissa_exponent(12345, -2, 2);
1840 assert_eq!(qty.as_f64(), 123.45);
1841 }
1842
1843 #[rstest]
1844 fn test_from_mantissa_exponent_excess_rounds_down() {
1845 let qty = Quantity::from_mantissa_exponent(12345, -3, 2);
1848 assert_eq!(qty.as_f64(), 12.34);
1849 }
1850
1851 #[rstest]
1852 fn test_from_mantissa_exponent_excess_rounds_up() {
1853 let qty = Quantity::from_mantissa_exponent(12355, -3, 2);
1855 assert_eq!(qty.as_f64(), 12.36);
1856 }
1857
1858 #[rstest]
1859 fn test_from_mantissa_exponent_positive_exponent() {
1860 let qty = Quantity::from_mantissa_exponent(5, 2, 0);
1861 assert_eq!(qty.as_f64(), 500.0);
1862 }
1863
1864 #[rstest]
1865 fn test_from_mantissa_exponent_zero() {
1866 let qty = Quantity::from_mantissa_exponent(0, 2, 2);
1867 assert_eq!(qty.as_f64(), 0.0);
1868 }
1869
1870 #[cfg(feature = "high-precision")]
1871 #[rstest]
1872 #[case(QUANTITY_RAW_MAX, dec!(34028236692093))]
1873 #[case(80_000_000_000_000_000_000_000_000_000, dec!(8000000000000))]
1874 fn test_as_decimal_above_decimal_mantissa(#[case] raw: QuantityRaw, #[case] expected: Decimal) {
1875 let qty = Quantity::from_raw(raw, 16);
1878
1879 assert_eq!(qty.as_decimal(), expected);
1880 }
1881
1882 #[rstest]
1883 fn test_from_mantissa_exponent_checked_exact_precision() {
1884 let qty = Quantity::from_mantissa_exponent_checked(12345, -2, 2).unwrap();
1885 assert_eq!(qty.as_decimal(), dec!(123.45));
1886 }
1887
1888 #[rstest]
1889 fn test_from_mantissa_exponent_checked_zero_with_large_exponent() {
1890 let qty = Quantity::from_mantissa_exponent_checked(0, 119, 2).unwrap();
1891 assert_eq!(qty.as_decimal(), dec!(0.00));
1892 }
1893
1894 #[rstest]
1895 fn test_from_mantissa_exponent_checked_invalid_precision() {
1896 #[cfg(feature = "defi")]
1897 let invalid_precision = crate::defi::WEI_PRECISION + 1;
1898 #[cfg(not(feature = "defi"))]
1899 let invalid_precision = FIXED_PRECISION + 1;
1900
1901 let error = Quantity::from_mantissa_exponent_checked(1, 0, invalid_precision).unwrap_err();
1902 assert!(error.to_string().contains("`precision` exceeded maximum"));
1903 }
1904
1905 #[rstest]
1906 fn test_from_mantissa_exponent_checked_overflow_returns_error() {
1907 let error = Quantity::from_mantissa_exponent_checked(u64::MAX, 100, 0).unwrap_err();
1908 assert!(
1909 error
1910 .to_string()
1911 .contains("Overflow in Quantity::from_mantissa_exponent")
1912 );
1913 }
1914
1915 #[rstest]
1916 #[should_panic(expected = "Quantity::from_mantissa_exponent")]
1917 fn test_from_mantissa_exponent_overflow_panics() {
1918 let _ = Quantity::from_mantissa_exponent(u64::MAX, 9, 0);
1919 }
1920
1921 #[rstest]
1922 #[should_panic(expected = "exceeds i128 range")]
1923 fn test_from_mantissa_exponent_large_exponent_panics() {
1924 let _ = Quantity::from_mantissa_exponent(1, 119, 0);
1925 }
1926
1927 #[rstest]
1928 fn test_from_mantissa_exponent_zero_with_large_exponent() {
1929 let qty = Quantity::from_mantissa_exponent(0, 119, 0);
1930 assert_eq!(qty.as_f64(), 0.0);
1931 }
1932
1933 #[rstest]
1934 fn test_from_mantissa_exponent_very_negative_exponent_rounds_to_zero() {
1935 let qty = Quantity::from_mantissa_exponent(12345, -120, 2);
1936 assert_eq!(qty.as_f64(), 0.0);
1937 }
1938
1939 #[rstest]
1940 fn test_f64_operations() {
1941 let q = Quantity::new(10.5, 2);
1942 assert_eq!(q + 1.0, 11.5);
1943 assert_eq!(q - 1.0, 9.5);
1944 assert_eq!(q * 2.0, 21.0);
1945 assert_eq!(q / 2.0, 5.25);
1946 }
1947
1948 #[rstest]
1949 fn test_decimal_arithmetic_operations() {
1950 let qty = Quantity::new(100.0, 2);
1951 assert_eq!(qty + dec!(50.25), dec!(150.25));
1952 assert_eq!(qty - dec!(30.50), dec!(69.50));
1953 assert_eq!(qty * dec!(1.5), dec!(150.00));
1954 assert_eq!(qty / dec!(4), dec!(25.00));
1955 }
1956
1957 #[rstest]
1961 #[cfg(feature = "defi")]
1962 #[case::sell_tx_rain_amount(
1963 U256::from_str_radix("42193532365637161405123", 10).unwrap(),
1964 18,
1965 "42193.532365637161405123"
1966 )]
1967 #[case::sell_tx_weth_amount(
1968 U256::from_str_radix("112633187203033110", 10).unwrap(),
1969 18,
1970 "0.112633187203033110"
1971 )]
1972 fn test_from_u256_real_swap_data(
1973 #[case] amount: U256,
1974 #[case] precision: u8,
1975 #[case] expected_str: &str,
1976 ) {
1977 let qty = Quantity::from_u256(amount, precision).unwrap();
1978 assert_eq!(qty.precision, precision);
1979 assert_eq!(qty.as_decimal().to_string(), expected_str);
1980 }
1981
1982 #[rstest]
1983 #[cfg(feature = "defi")]
1984 fn test_from_u256_overflow_returns_typed_error_with_stable_display() {
1985 let error = Quantity::from_u256(U256::MAX, 0).unwrap_err();
1986 match error {
1987 CorrectnessError::PredicateViolation { ref message } => {
1988 assert!(
1989 message.contains("Amount overflow during scaling to fixed precision"),
1990 "unexpected message: {message:?}",
1991 );
1992 }
1993 _ => panic!("expected PredicateViolation, was {error:?}"),
1994 }
1995 }
1996
1997 #[rstest]
1998 #[cfg(feature = "defi")]
1999 fn test_from_u256_rejects_amount_above_quantity_raw_range() {
2000 let amount = U256::from(u128::MAX) + U256::from(1_u8);
2001 let error = Quantity::from_u256(amount, 18).unwrap_err();
2002
2003 assert_eq!(
2004 error,
2005 CorrectnessError::PredicateViolation {
2006 message: format!("U256 scaled amount {amount} exceeds QuantityRaw range"),
2007 }
2008 );
2009 }
2010
2011 #[rstest]
2012 #[cfg(feature = "defi")]
2013 fn test_from_u256_invalid_precision_returns_typed_error() {
2014 let error = Quantity::from_u256(U256::from(1u8), 19).unwrap_err();
2015 match error {
2016 CorrectnessError::PredicateViolation { ref message } => {
2017 assert!(
2018 message.contains("WEI_PRECISION"),
2019 "unexpected message: {message:?}",
2020 );
2021 }
2022 _ => panic!("expected PredicateViolation, was {error:?}"),
2023 }
2024 }
2025
2026 #[rstest]
2027 #[cfg(feature = "defi")]
2028 fn test_from_u256_raw_above_max_returns_typed_error() {
2029 let raw = QUANTITY_RAW_MAX + 1;
2032 let error = Quantity::from_u256(U256::from(raw), FIXED_PRECISION).unwrap_err();
2033 match error {
2034 CorrectnessError::PredicateViolation { ref message } => {
2035 assert!(
2036 message.contains("QUANTITY_RAW_MAX"),
2037 "unexpected message: {message:?}",
2038 );
2039 }
2040 _ => panic!("expected PredicateViolation, was {error:?}"),
2041 }
2042 }
2043}
2044
2045#[cfg(test)]
2046mod property_tests {
2047 use proptest::prelude::*;
2048 use rstest::rstest;
2049
2050 use super::*;
2051
2052 fn quantity_value_strategy() -> impl Strategy<Value = f64> {
2054 prop_oneof![
2056 0.00001..1.0,
2058 1.0..100_000.0,
2060 100_000.0..1_000_000.0,
2062 Just(0.0),
2064 Just(QUANTITY_MAX / 2.0),
2066 ]
2067 }
2068
2069 fn precision_strategy() -> impl Strategy<Value = u8> {
2071 let upper = FIXED_PRECISION.min(MAX_FLOAT_PRECISION);
2072 prop_oneof![Just(0u8), 0u8..=upper, Just(FIXED_PRECISION),]
2073 }
2074
2075 fn precision_strategy_non_zero() -> impl Strategy<Value = u8> {
2076 let upper = FIXED_PRECISION.clamp(1, MAX_FLOAT_PRECISION);
2077 prop_oneof![Just(upper), Just(FIXED_PRECISION.max(1)), 1u8..=upper,]
2078 }
2079
2080 fn raw_for_precision_strategy() -> impl Strategy<Value = (QuantityRaw, u8)> {
2081 precision_strategy().prop_flat_map(|precision| {
2082 let step_u128 = 10u128.pow(u32::from(FIXED_PRECISION.saturating_sub(precision)));
2083 #[cfg(feature = "high-precision")]
2084 let max_steps_u128 = QUANTITY_RAW_MAX / step_u128;
2085 #[cfg(not(feature = "high-precision"))]
2086 let max_steps_u128 = u128::from(QUANTITY_RAW_MAX) / step_u128;
2087
2088 (0u128..=max_steps_u128).prop_map(move |steps_u128| {
2089 let raw_u128 = steps_u128 * step_u128;
2090 #[cfg(feature = "high-precision")]
2091 let raw = raw_u128;
2092 #[cfg(not(feature = "high-precision"))]
2093 let raw = raw_u128
2094 .try_into()
2095 .expect("raw value should fit in QuantityRaw");
2096 (raw, precision)
2097 })
2098 })
2099 }
2100
2101 const DECIMAL_MAX_MANTISSA: u128 = 79_228_162_514_264_337_593_543_950_335;
2102
2103 fn decimal_compatible(raw: QuantityRaw, precision: u8) -> bool {
2104 if precision > MAX_FLOAT_PRECISION {
2105 return false;
2106 }
2107 let precision_diff = u32::from(FIXED_PRECISION.saturating_sub(precision));
2108 let divisor = 10u128.pow(precision_diff);
2109 #[cfg(feature = "high-precision")]
2110 let rescaled_raw = raw / divisor;
2111 #[cfg(not(feature = "high-precision"))]
2112 let rescaled_raw = u128::from(raw) / divisor;
2113 rescaled_raw <= DECIMAL_MAX_MANTISSA
2116 }
2117
2118 proptest! {
2119 #[rstest]
2121 fn prop_quantity_serde_round_trip(
2122 (raw, precision) in raw_for_precision_strategy()
2123 ) {
2124 prop_assume!(decimal_compatible(raw, precision));
2126
2127 let original = Quantity::from_raw(raw, precision);
2128
2129 let string_repr = original.to_string();
2131 let from_string: Quantity = string_repr.parse().unwrap();
2132 prop_assert_eq!(from_string.raw, original.raw);
2133 prop_assert_eq!(from_string.precision, original.precision);
2134
2135 let json = serde_json::to_string(&original).unwrap();
2137 let from_json: Quantity = serde_json::from_str(&json).unwrap();
2138 prop_assert_eq!(from_json.precision, original.precision);
2139 prop_assert_eq!(from_json.raw, original.raw);
2140 }
2141
2142 #[rstest]
2144 fn prop_quantity_arithmetic_associative(
2145 a in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
2146 b in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
2147 c in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
2148 precision in precision_strategy()
2149 ) {
2150 let q_a = Quantity::new(a, precision);
2151 let q_b = Quantity::new(b, precision);
2152 let q_c = Quantity::new(c, precision);
2153
2154 let expected = q_a
2155 .raw
2156 .checked_add(q_b.raw)
2157 .and_then(|sum| sum.checked_add(q_c.raw))
2158 .filter(|sum| *sum <= QUANTITY_RAW_MAX);
2159
2160 if let Some(expected) = expected {
2161 let left = (q_a + q_b) + q_c;
2162 let right = q_a + (q_b + q_c);
2163 prop_assert_eq!(left.raw, expected);
2164 prop_assert_eq!(right.raw, expected);
2165 }
2166 }
2167
2168 #[rstest]
2170 fn prop_quantity_addition_subtraction_inverse(
2171 base in quantity_value_strategy().prop_filter("Reasonable values", |&x| x < 1e6),
2172 delta in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
2173 precision in precision_strategy()
2174 ) {
2175 let q_base = Quantity::new(base, precision);
2176 let q_delta = Quantity::new(delta, precision);
2177
2178 let expected = q_base
2179 .raw
2180 .checked_add(q_delta.raw)
2181 .filter(|sum| *sum <= QUANTITY_RAW_MAX);
2182
2183 if expected.is_some() {
2184 prop_assert_eq!((q_base + q_delta) - q_delta, q_base);
2185 }
2186 }
2187
2188 #[rstest]
2191 fn prop_quantity_checked_add_matches_spec(
2192 a in quantity_value_strategy(),
2193 b 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 expected = q_a.raw
2199 .checked_add(q_b.raw)
2200 .filter(|r| *r <= QUANTITY_RAW_MAX)
2201 .filter(|_| q_a.raw != QUANTITY_UNDEF && q_b.raw != QUANTITY_UNDEF)
2202 .map(|raw| Quantity { raw, precision: q_a.precision.max(q_b.precision) });
2203 prop_assert_eq!(q_a.checked_add(q_b), expected);
2204 }
2205
2206 #[rstest]
2209 fn prop_quantity_checked_sub_matches_spec(
2210 a in quantity_value_strategy(),
2211 b in quantity_value_strategy(),
2212 precision in precision_strategy()
2213 ) {
2214 let q_a = Quantity::new(a, precision);
2215 let q_b = Quantity::new(b, precision);
2216 let expected = q_a.raw
2217 .checked_sub(q_b.raw)
2218 .filter(|_| q_a.raw != QUANTITY_UNDEF && q_b.raw != QUANTITY_UNDEF)
2219 .map(|raw| Quantity { raw, precision: q_a.precision.max(q_b.precision) });
2220 prop_assert_eq!(q_a.checked_sub(q_b), expected);
2221 }
2222
2223 #[rstest]
2225 fn prop_quantity_ordering_transitive(
2226 a in quantity_value_strategy(),
2227 b in quantity_value_strategy(),
2228 c in quantity_value_strategy(),
2229 precision in precision_strategy()
2230 ) {
2231 let q_a = Quantity::new(a, precision);
2232 let q_b = Quantity::new(b, precision);
2233 let q_c = Quantity::new(c, precision);
2234
2235 if q_a <= q_b && q_b <= q_c {
2237 prop_assert!(q_a <= q_c, "Transitivity failed: {} <= {} <= {} but {} > {}",
2238 q_a.as_f64(), q_b.as_f64(), q_c.as_f64(), q_a.as_f64(), q_c.as_f64());
2239 }
2240 }
2241
2242 #[rstest]
2244 fn prop_quantity_string_parsing_precision(
2245 integral in 0u32..1_000_000,
2246 fractional in 0u32..1_000_000,
2247 precision in precision_strategy_non_zero()
2248 ) {
2249 let pow = 10u128.pow(u32::from(precision));
2251 let fractional_mod = u128::from(fractional) % pow;
2252 let fractional_str = format!("{:0width$}", fractional_mod, width = precision as usize);
2253 let quantity_str = format!("{integral}.{fractional_str}");
2254
2255 let parsed: Quantity = quantity_str.parse().unwrap();
2256 prop_assert_eq!(parsed.precision, precision);
2257
2258 let round_trip = parsed.to_string();
2260 let expected_value = format!("{integral}.{fractional_str}");
2261 prop_assert_eq!(round_trip, expected_value);
2262 }
2263
2264 #[rstest]
2266 fn prop_quantity_arithmetic_bounds(
2267 a in quantity_value_strategy(),
2268 b in quantity_value_strategy(),
2269 precision in precision_strategy()
2270 ) {
2271 let q_a = Quantity::new(a, precision);
2272 let q_b = Quantity::new(b, precision);
2273
2274 let sum_f64 = q_a.as_f64() + q_b.as_f64();
2276 if sum_f64.is_finite() && (QUANTITY_MIN..=QUANTITY_MAX).contains(&sum_f64) {
2277 let sum = q_a + q_b;
2278 prop_assert!(sum.as_f64().is_finite());
2279 prop_assert!(!sum.is_undefined());
2280 }
2281
2282 let diff_f64 = q_a.as_f64() - q_b.as_f64();
2284 if diff_f64.is_finite() && (QUANTITY_MIN..=QUANTITY_MAX).contains(&diff_f64) {
2285 let diff = q_a - q_b;
2286 prop_assert!(diff.as_f64().is_finite());
2287 prop_assert!(!diff.is_undefined());
2288 }
2289 }
2290
2291 #[rstest]
2293 fn prop_quantity_multiplication_non_negative(
2294 a in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 0.0 && x < 10.0),
2295 b in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 0.0 && x < 10.0),
2296 precision in precision_strategy()
2297 ) {
2298 let q_a = Quantity::new(a, precision);
2299 let q_b = Quantity::new(b, precision);
2300
2301 let raw_product_check = q_a.raw.checked_mul(q_b.raw);
2303
2304 if let Some(raw_product) = raw_product_check {
2305 let scaled_raw = raw_product / FIXED_SCALAR_RAW;
2307 if scaled_raw <= QUANTITY_RAW_MAX {
2308 let product = q_a * q_b;
2310 prop_assert!(product.as_f64() >= 0.0, "Quantity multiplication produced negative value: {}", product.as_f64());
2311 }
2312 }
2313 }
2314
2315 #[rstest]
2317 fn prop_quantity_zero_addition_identity(
2318 value in quantity_value_strategy(),
2319 precision in precision_strategy()
2320 ) {
2321 let q = Quantity::new(value, precision);
2322 let zero = Quantity::zero(precision);
2323
2324 prop_assert_eq!(q + zero, q);
2326 prop_assert_eq!(zero + q, q);
2327 }
2328 }
2329
2330 proptest! {
2331 #[rstest]
2333 fn prop_quantity_as_decimal_preserves_precision(
2334 (raw, precision) in raw_for_precision_strategy()
2335 ) {
2336 prop_assume!(decimal_compatible(raw, precision));
2337 let quantity = Quantity::from_raw(raw, precision);
2338 let decimal = quantity.as_decimal();
2339 prop_assert_eq!(decimal.scale(), u32::from(precision));
2340 }
2341
2342 #[rstest]
2344 fn prop_quantity_as_decimal_matches_display(
2345 (raw, precision) in raw_for_precision_strategy()
2346 ) {
2347 prop_assume!(decimal_compatible(raw, precision));
2348 let quantity = Quantity::from_raw(raw, precision);
2349 let display_str = format!("{quantity}");
2350 let decimal_str = quantity.as_decimal().to_string();
2351 prop_assert_eq!(display_str, decimal_str);
2352 }
2353
2354 #[rstest]
2356 fn prop_quantity_from_decimal_roundtrip(
2357 (raw, precision) in raw_for_precision_strategy()
2358 ) {
2359 prop_assume!(decimal_compatible(raw, precision));
2360 let original = Quantity::from_raw(raw, precision);
2361 let decimal = original.as_decimal();
2362 let reconstructed = Quantity::from_decimal(decimal).unwrap();
2363 prop_assert_eq!(original.raw, reconstructed.raw);
2364 prop_assert_eq!(original.precision, reconstructed.precision);
2365 }
2366
2367 #[rstest]
2369 fn prop_quantity_from_raw_round_trip(
2370 (raw, precision) in raw_for_precision_strategy()
2371 ) {
2372 let quantity = Quantity::from_raw(raw, precision);
2373 prop_assert_eq!(quantity.raw, raw);
2374 prop_assert_eq!(quantity.precision, precision);
2375 }
2376 }
2377}