1use std::{
43 cmp::Ordering,
44 fmt::{Debug, Display},
45 hash::{Hash, Hasher},
46 ops::{Add, Deref, Div, Mul, Sub},
47 str::FromStr,
48};
49
50#[cfg(feature = "defi")]
51use alloy_primitives::U256;
52use nautilus_core::{
53 correctness::{FAILED, check_in_range_inclusive_f64, check_predicate_true},
54 formatting::Separable,
55};
56use rust_decimal::Decimal;
57use serde::{Deserialize, Deserializer, Serialize};
58
59use super::fixed::{
60 FIXED_PRECISION, FIXED_SCALAR, MAX_FLOAT_PRECISION, check_fixed_precision,
61 mantissa_exponent_to_fixed_i128,
62};
63#[cfg(not(feature = "high-precision"))]
64use super::fixed::{f64_to_fixed_u64, fixed_u64_to_f64};
65#[cfg(feature = "high-precision")]
66use super::fixed::{f64_to_fixed_u128, fixed_u128_to_f64};
67
68#[cfg(feature = "high-precision")]
73pub type QuantityRaw = u128;
74
75#[cfg(not(feature = "high-precision"))]
76pub type QuantityRaw = u64;
77
78#[unsafe(no_mangle)]
82#[allow(unsafe_code)]
83pub static QUANTITY_RAW_MAX: QuantityRaw = (QUANTITY_MAX * FIXED_SCALAR) as QuantityRaw;
84
85pub const QUANTITY_UNDEF: QuantityRaw = QuantityRaw::MAX;
87
88#[cfg(feature = "high-precision")]
93pub const QUANTITY_MAX: f64 = 34_028_236_692_093.0;
95
96#[cfg(not(feature = "high-precision"))]
97pub const QUANTITY_MAX: f64 = 18_446_744_073.0;
99
100pub const QUANTITY_MIN: f64 = 0.0;
104
105#[repr(C)]
116#[derive(Clone, Copy, Default, Eq)]
117#[cfg_attr(
118 feature = "python",
119 pyo3::pyclass(
120 module = "nautilus_trader.core.nautilus_pyo3.model",
121 frozen,
122 from_py_object
123 )
124)]
125#[cfg_attr(
126 feature = "python",
127 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
128)]
129pub struct Quantity {
130 pub raw: QuantityRaw,
132 pub precision: u8,
134}
135
136impl Quantity {
137 pub fn new_checked(value: f64, precision: u8) -> anyhow::Result<Self> {
149 check_in_range_inclusive_f64(value, QUANTITY_MIN, QUANTITY_MAX, "value")?;
150
151 #[cfg(feature = "defi")]
152 if precision > MAX_FLOAT_PRECISION {
153 anyhow::bail!(
155 "`precision` exceeded maximum float precision ({MAX_FLOAT_PRECISION}), use `Quantity::from_wei()` for wei values instead"
156 );
157 }
158
159 check_fixed_precision(precision)?;
160
161 #[cfg(feature = "high-precision")]
162 let raw = f64_to_fixed_u128(value, precision);
163 #[cfg(not(feature = "high-precision"))]
164 let raw = f64_to_fixed_u64(value, precision);
165
166 Ok(Self { raw, precision })
167 }
168
169 pub fn non_zero_checked(value: f64, precision: u8) -> anyhow::Result<Self> {
183 check_predicate_true(value != 0.0, "value was zero")?;
184 check_fixed_precision(precision)?;
185 let rounded_value =
186 (value * 10.0_f64.powi(precision as i32)).round() / 10.0_f64.powi(precision as i32);
187 check_predicate_true(
188 rounded_value != 0.0,
189 &format!("value {value} was zero after rounding to precision {precision}"),
190 )?;
191
192 Self::new_checked(value, precision)
193 }
194
195 pub fn new(value: f64, precision: u8) -> Self {
201 Self::new_checked(value, precision).expect(FAILED)
202 }
203
204 pub fn non_zero(value: f64, precision: u8) -> Self {
210 Self::non_zero_checked(value, precision).expect(FAILED)
211 }
212
213 pub fn from_raw(raw: QuantityRaw, precision: u8) -> Self {
220 assert!(
221 raw == QUANTITY_UNDEF || raw <= QUANTITY_RAW_MAX,
222 "`raw` value {raw} exceeds QUANTITY_RAW_MAX={QUANTITY_RAW_MAX} for Quantity"
223 );
224
225 if raw == QUANTITY_UNDEF {
226 assert!(
227 precision == 0,
228 "`precision` must be 0 when `raw` is QUANTITY_UNDEF"
229 );
230 }
231 check_fixed_precision(precision).expect(FAILED);
232
233 Self { raw, precision }
242 }
243
244 pub fn from_raw_checked(raw: QuantityRaw, precision: u8) -> anyhow::Result<Self> {
254 if raw == QUANTITY_UNDEF {
255 anyhow::ensure!(
256 precision == 0,
257 "`precision` must be 0 when `raw` is QUANTITY_UNDEF"
258 );
259 }
260 anyhow::ensure!(
261 raw == QUANTITY_UNDEF || raw <= QUANTITY_RAW_MAX,
262 "raw value {raw} exceeds QUANTITY_RAW_MAX={QUANTITY_RAW_MAX}"
263 );
264 check_fixed_precision(precision)?;
265
266 Ok(Self { raw, precision })
267 }
268
269 #[must_use]
274 pub fn saturating_sub(self, rhs: Self) -> Self {
275 let precision = self.precision.max(rhs.precision);
276 let raw = self.raw.saturating_sub(rhs.raw);
277 if raw == 0 && self.raw < rhs.raw {
278 log::warn!(
279 "Saturating Quantity subtraction: {self} - {rhs} < 0, clamped to 0 (precision={precision})"
280 );
281 }
282
283 Self { raw, precision }
284 }
285
286 #[must_use]
292 pub fn zero(precision: u8) -> Self {
293 check_fixed_precision(precision).expect(FAILED);
294 Self::new(0.0, precision)
295 }
296
297 #[must_use]
299 pub fn is_undefined(&self) -> bool {
300 self.raw == QUANTITY_UNDEF
301 }
302
303 #[must_use]
305 pub fn is_zero(&self) -> bool {
306 self.raw == 0
307 }
308
309 #[must_use]
311 pub fn is_positive(&self) -> bool {
312 self.raw != QUANTITY_UNDEF && self.raw > 0
313 }
314
315 #[cfg(feature = "high-precision")]
316 #[must_use]
322 pub fn as_f64(&self) -> f64 {
323 #[cfg(feature = "defi")]
324 assert!(
325 self.precision <= MAX_FLOAT_PRECISION,
326 "Invalid f64 conversion beyond `MAX_FLOAT_PRECISION` (16)"
327 );
328
329 fixed_u128_to_f64(self.raw)
330 }
331
332 #[cfg(not(feature = "high-precision"))]
333 #[must_use]
339 pub fn as_f64(&self) -> f64 {
340 #[cfg(feature = "defi")]
341 if self.precision > MAX_FLOAT_PRECISION {
342 panic!("Invalid f64 conversion beyond `MAX_FLOAT_PRECISION` (16)");
343 }
344
345 fixed_u64_to_f64(self.raw)
346 }
347
348 #[must_use]
350 pub fn as_decimal(&self) -> Decimal {
351 let precision_diff = FIXED_PRECISION.saturating_sub(self.precision);
353 let rescaled_raw = self.raw / QuantityRaw::pow(10, u32::from(precision_diff));
354
355 #[allow(clippy::useless_conversion)]
359 Decimal::from_i128_with_scale(rescaled_raw as i128, u32::from(self.precision))
360 }
361
362 #[must_use]
364 pub fn to_formatted_string(&self) -> String {
365 format!("{self}").separate_with_underscores()
366 }
367
368 pub fn from_decimal_dp(decimal: Decimal, precision: u8) -> anyhow::Result<Self> {
381 anyhow::ensure!(
382 decimal.mantissa() >= 0,
383 "Decimal value '{decimal}' is negative, Quantity must be non-negative"
384 );
385
386 let exponent = -(decimal.scale() as i8);
387 let raw_i128 = mantissa_exponent_to_fixed_i128(decimal.mantissa(), exponent, precision)?;
388
389 let raw: QuantityRaw = raw_i128.try_into().map_err(|_| {
390 anyhow::anyhow!("Decimal value exceeds QuantityRaw range [0, {QUANTITY_RAW_MAX}]")
391 })?;
392 anyhow::ensure!(
393 raw <= QUANTITY_RAW_MAX,
394 "Raw value {raw} exceeds QUANTITY_RAW_MAX={QUANTITY_RAW_MAX} for Quantity"
395 );
396
397 Ok(Self { raw, precision })
398 }
399
400 pub fn from_decimal(decimal: Decimal) -> anyhow::Result<Self> {
412 let precision = decimal.scale() as u8;
413 Self::from_decimal_dp(decimal, precision)
414 }
415
416 #[must_use]
425 pub fn from_mantissa_exponent(mantissa: u64, exponent: i8, precision: u8) -> Self {
426 check_fixed_precision(precision).expect(FAILED);
427
428 if mantissa == 0 {
429 return Self { raw: 0, precision };
430 }
431
432 let raw_i128 = mantissa_exponent_to_fixed_i128(mantissa as i128, exponent, precision)
433 .expect("Overflow in Quantity::from_mantissa_exponent");
434
435 let raw: QuantityRaw = raw_i128
436 .try_into()
437 .expect("Raw value exceeds QuantityRaw range in Quantity::from_mantissa_exponent");
438 assert!(
439 raw <= QUANTITY_RAW_MAX,
440 "`raw` value {raw} exceeded QUANTITY_RAW_MAX={QUANTITY_RAW_MAX} for Quantity"
441 );
442
443 Self { raw, precision }
444 }
445
446 #[cfg(feature = "defi")]
454 pub fn from_u256(amount: U256, precision: u8) -> anyhow::Result<Self> {
455 let scaled_amount = if precision < FIXED_PRECISION {
457 amount
458 .checked_mul(U256::from(10u128.pow((FIXED_PRECISION - precision) as u32)))
459 .ok_or_else(|| {
460 anyhow::anyhow!(
461 "Amount overflow during scaling to fixed precision: {} * 10^{}",
462 amount,
463 FIXED_PRECISION - precision
464 )
465 })?
466 } else {
467 amount
468 };
469
470 let raw = QuantityRaw::try_from(scaled_amount).map_err(|_| {
471 anyhow::anyhow!("U256 scaled amount {scaled_amount} exceeds QuantityRaw range")
472 })?;
473
474 Ok(Self::from_raw(raw, precision))
475 }
476}
477
478impl From<Quantity> for f64 {
479 fn from(qty: Quantity) -> Self {
480 qty.as_f64()
481 }
482}
483
484impl From<&Quantity> for f64 {
485 fn from(qty: &Quantity) -> Self {
486 qty.as_f64()
487 }
488}
489
490impl From<i32> for Quantity {
491 fn from(value: i32) -> Self {
497 assert!(
498 value >= 0,
499 "Cannot create Quantity from negative i32: {value}. Use u32 or check value is non-negative."
500 );
501 Self::new(value as f64, 0)
502 }
503}
504
505impl From<i64> for Quantity {
506 fn from(value: i64) -> Self {
512 assert!(
513 value >= 0,
514 "Cannot create Quantity from negative i64: {value}. Use u64 or check value is non-negative."
515 );
516 Self::new(value as f64, 0)
517 }
518}
519
520impl From<u32> for Quantity {
521 fn from(value: u32) -> Self {
522 Self::new(value as f64, 0)
523 }
524}
525
526impl From<u64> for Quantity {
527 fn from(value: u64) -> Self {
528 Self::new(value as f64, 0)
529 }
530}
531
532impl Hash for Quantity {
533 fn hash<H: Hasher>(&self, state: &mut H) {
534 self.raw.hash(state);
535 }
536}
537
538impl PartialEq for Quantity {
539 fn eq(&self, other: &Self) -> bool {
540 self.raw == other.raw
541 }
542}
543
544impl PartialOrd for Quantity {
545 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
546 Some(self.cmp(other))
547 }
548
549 fn lt(&self, other: &Self) -> bool {
550 self.raw.lt(&other.raw)
551 }
552
553 fn le(&self, other: &Self) -> bool {
554 self.raw.le(&other.raw)
555 }
556
557 fn gt(&self, other: &Self) -> bool {
558 self.raw.gt(&other.raw)
559 }
560
561 fn ge(&self, other: &Self) -> bool {
562 self.raw.ge(&other.raw)
563 }
564}
565
566impl Ord for Quantity {
567 fn cmp(&self, other: &Self) -> Ordering {
568 self.raw.cmp(&other.raw)
569 }
570}
571
572impl Deref for Quantity {
573 type Target = QuantityRaw;
574
575 fn deref(&self) -> &Self::Target {
576 &self.raw
577 }
578}
579
580impl Add for Quantity {
581 type Output = Self;
582 fn add(self, rhs: Self) -> Self::Output {
583 Self {
584 raw: self
585 .raw
586 .checked_add(rhs.raw)
587 .expect("Overflow occurred when adding `Quantity`"),
588 precision: self.precision.max(rhs.precision),
589 }
590 }
591}
592
593impl Sub for Quantity {
594 type Output = Self;
595 fn sub(self, rhs: Self) -> Self::Output {
596 Self {
597 raw: self
598 .raw
599 .checked_sub(rhs.raw)
600 .expect("Underflow occurred when subtracting `Quantity`"),
601 precision: self.precision.max(rhs.precision),
602 }
603 }
604}
605
606#[allow(
607 clippy::suspicious_arithmetic_impl,
608 reason = "Can use division to scale back"
609)]
610impl Mul for Quantity {
611 type Output = Self;
612 fn mul(self, rhs: Self) -> Self::Output {
613 let result_raw = self
614 .raw
615 .checked_mul(rhs.raw)
616 .expect("Overflow occurred when multiplying `Quantity`");
617
618 Self {
619 raw: result_raw / (FIXED_SCALAR as QuantityRaw),
620 precision: self.precision.max(rhs.precision),
621 }
622 }
623}
624
625impl Add<Decimal> for Quantity {
626 type Output = Decimal;
627 fn add(self, rhs: Decimal) -> Self::Output {
628 self.as_decimal() + rhs
629 }
630}
631
632impl Sub<Decimal> for Quantity {
633 type Output = Decimal;
634 fn sub(self, rhs: Decimal) -> Self::Output {
635 self.as_decimal() - rhs
636 }
637}
638
639impl Mul<Decimal> for Quantity {
640 type Output = Decimal;
641 fn mul(self, rhs: Decimal) -> Self::Output {
642 self.as_decimal() * rhs
643 }
644}
645
646impl Div<Decimal> for Quantity {
647 type Output = Decimal;
648 fn div(self, rhs: Decimal) -> Self::Output {
649 self.as_decimal() / rhs
650 }
651}
652
653impl Add<f64> for Quantity {
654 type Output = f64;
655 fn add(self, rhs: f64) -> Self::Output {
656 self.as_f64() + rhs
657 }
658}
659
660impl Sub<f64> for Quantity {
661 type Output = f64;
662 fn sub(self, rhs: f64) -> Self::Output {
663 self.as_f64() - rhs
664 }
665}
666
667impl Mul<f64> for Quantity {
668 type Output = f64;
669 fn mul(self, rhs: f64) -> Self::Output {
670 self.as_f64() * rhs
671 }
672}
673
674impl Div<f64> for Quantity {
675 type Output = f64;
676 fn div(self, rhs: f64) -> Self::Output {
677 self.as_f64() / rhs
678 }
679}
680
681impl From<Quantity> for QuantityRaw {
682 fn from(value: Quantity) -> Self {
683 value.raw
684 }
685}
686
687impl From<&Quantity> for QuantityRaw {
688 fn from(value: &Quantity) -> Self {
689 value.raw
690 }
691}
692
693impl From<Quantity> for Decimal {
694 fn from(value: Quantity) -> Self {
695 value.as_decimal()
696 }
697}
698
699impl From<&Quantity> for Decimal {
700 fn from(value: &Quantity) -> Self {
701 value.as_decimal()
702 }
703}
704
705impl FromStr for Quantity {
706 type Err = String;
707
708 fn from_str(value: &str) -> Result<Self, Self::Err> {
709 let clean_value = value.replace('_', "");
710
711 let decimal = if clean_value.contains('e') || clean_value.contains('E') {
712 Decimal::from_scientific(&clean_value)
713 .map_err(|e| format!("Error parsing `input` string '{value}' as Decimal: {e}"))?
714 } else {
715 Decimal::from_str(&clean_value)
716 .map_err(|e| format!("Error parsing `input` string '{value}' as Decimal: {e}"))?
717 };
718
719 let precision = decimal.scale() as u8;
721
722 Self::from_decimal_dp(decimal, precision).map_err(|e| e.to_string())
723 }
724}
725
726impl From<&str> for Quantity {
727 fn from(value: &str) -> Self {
728 Self::from_str(value).expect(FAILED)
729 }
730}
731
732impl From<String> for Quantity {
733 fn from(value: String) -> Self {
734 Self::from_str(&value).expect(FAILED)
735 }
736}
737
738impl From<&String> for Quantity {
739 fn from(value: &String) -> Self {
740 Self::from_str(value).expect(FAILED)
741 }
742}
743
744impl Debug for Quantity {
745 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
746 if self.precision > MAX_FLOAT_PRECISION {
747 write!(f, "{}({})", stringify!(Quantity), self.raw)
748 } else {
749 write!(f, "{}({})", stringify!(Quantity), self.as_decimal())
750 }
751 }
752}
753
754impl Display for Quantity {
755 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
756 if self.precision > MAX_FLOAT_PRECISION {
757 write!(f, "{}", self.raw)
758 } else {
759 write!(f, "{}", self.as_decimal())
760 }
761 }
762}
763
764impl Serialize for Quantity {
765 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
766 where
767 S: serde::Serializer,
768 {
769 serializer.serialize_str(&self.to_string())
770 }
771}
772
773impl<'de> Deserialize<'de> for Quantity {
774 fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
775 where
776 D: Deserializer<'de>,
777 {
778 let qty_str: &str = Deserialize::deserialize(_deserializer)?;
779 let qty: Self = qty_str.into();
780 Ok(qty)
781 }
782}
783
784pub fn check_positive_quantity(value: Quantity, param: &str) -> anyhow::Result<()> {
790 if !value.is_positive() {
791 anyhow::bail!("invalid `Quantity` for '{param}' not positive, was {value}")
792 }
793 Ok(())
794}
795
796#[cfg(test)]
797mod tests {
798 use std::str::FromStr;
799
800 use nautilus_core::approx_eq;
801 use rstest::rstest;
802 use rust_decimal_macros::dec;
803
804 use super::*;
805
806 #[rstest]
807 #[should_panic(expected = "invalid `Quantity` for 'qty' not positive, was 0")]
808 fn test_check_quantity_positive() {
809 let qty = Quantity::new(0.0, 0);
810 check_positive_quantity(qty, "qty").unwrap();
811 }
812
813 #[rstest]
814 #[cfg(all(not(feature = "defi"), not(feature = "high-precision")))]
815 #[should_panic(expected = "`precision` exceeded maximum `FIXED_PRECISION` (9), was 17")]
816 fn test_invalid_precision_new() {
817 let _ = Quantity::new(1.0, 17);
819 }
820
821 #[rstest]
822 #[cfg(all(not(feature = "defi"), feature = "high-precision"))]
823 #[should_panic(expected = "`precision` exceeded maximum `FIXED_PRECISION` (16), was 17")]
824 fn test_invalid_precision_new() {
825 let _ = Quantity::new(1.0, 17);
827 }
828
829 #[rstest]
830 #[cfg(not(feature = "defi"))]
831 #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
832 fn test_invalid_precision_from_raw() {
833 let _ = Quantity::from_raw(1, FIXED_PRECISION + 1);
835 }
836
837 #[rstest]
838 #[cfg(not(feature = "defi"))]
839 #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
840 fn test_invalid_precision_zero() {
841 let _ = Quantity::zero(FIXED_PRECISION + 1);
843 }
844
845 #[rstest]
846 fn test_mixed_precision_add() {
847 let q1 = Quantity::new(1.0, 1);
848 let q2 = Quantity::new(1.0, 2);
849 let result = q1 + q2;
850 assert_eq!(result.precision, 2);
851 assert_eq!(result.as_f64(), 2.0);
852 }
853
854 #[rstest]
855 fn test_mixed_precision_sub() {
856 let q1 = Quantity::new(2.0, 1);
857 let q2 = Quantity::new(1.0, 2);
858 let result = q1 - q2;
859 assert_eq!(result.precision, 2);
860 assert_eq!(result.as_f64(), 1.0);
861 }
862
863 #[rstest]
864 fn test_mixed_precision_mul() {
865 let q1 = Quantity::new(2.0, 1);
866 let q2 = Quantity::new(3.0, 2);
867 let result = q1 * q2;
868 assert_eq!(result.precision, 2);
869 assert_eq!(result.as_f64(), 6.0);
870 }
871
872 #[rstest]
873 fn test_new_non_zero_ok() {
874 let qty = Quantity::non_zero_checked(123.456, 3).unwrap();
875 assert_eq!(qty.raw, Quantity::new(123.456, 3).raw);
876 assert!(qty.is_positive());
877 }
878
879 #[rstest]
880 fn test_new_non_zero_zero_input() {
881 assert!(Quantity::non_zero_checked(0.0, 0).is_err());
882 }
883
884 #[rstest]
885 fn test_new_non_zero_rounds_to_zero() {
886 assert!(Quantity::non_zero_checked(0.0004, 3).is_err());
888 }
889
890 #[rstest]
891 fn test_new_non_zero_negative() {
892 assert!(Quantity::non_zero_checked(-1.0, 0).is_err());
893 }
894
895 #[rstest]
896 fn test_new_non_zero_exceeds_max() {
897 assert!(Quantity::non_zero_checked(QUANTITY_MAX * 10.0, 0).is_err());
898 }
899
900 #[rstest]
901 fn test_new_non_zero_invalid_precision() {
902 assert!(Quantity::non_zero_checked(1.0, FIXED_PRECISION + 1).is_err());
903 }
904
905 #[rstest]
906 fn test_new() {
907 let value = 0.00812;
908 let qty = Quantity::new(value, 8);
909 assert_eq!(qty, qty);
910 assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
911 assert_eq!(qty.precision, 8);
912 assert_eq!(qty, Quantity::from("0.00812000"));
913 assert_eq!(qty.as_decimal(), dec!(0.00812000));
914 assert_eq!(qty.to_string(), "0.00812000");
915 assert!(!qty.is_zero());
916 assert!(qty.is_positive());
917 assert!(approx_eq!(f64, qty.as_f64(), 0.00812, epsilon = 0.000_001));
918 }
919
920 #[rstest]
921 fn test_check_quantity_positive_ok() {
922 let qty = Quantity::new(10.0, 0);
923 check_positive_quantity(qty, "qty").unwrap();
924 }
925
926 #[rstest]
927 fn test_negative_quantity_validation() {
928 assert!(Quantity::new_checked(-1.0, FIXED_PRECISION).is_err());
929 }
930
931 #[rstest]
932 fn test_undefined() {
933 let qty = Quantity::from_raw(QUANTITY_UNDEF, 0);
934 assert_eq!(qty.raw, QUANTITY_UNDEF);
935 assert!(qty.is_undefined());
936 }
937
938 #[rstest]
939 fn test_zero() {
940 let qty = Quantity::zero(8);
941 assert_eq!(qty.raw, 0);
942 assert_eq!(qty.precision, 8);
943 assert!(qty.is_zero());
944 assert!(!qty.is_positive());
945 }
946
947 #[rstest]
948 fn test_from_i32() {
949 let value = 100_000i32;
950 let qty = Quantity::from(value);
951 assert_eq!(qty, qty);
952 assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
953 assert_eq!(qty.precision, 0);
954 }
955
956 #[rstest]
957 fn test_from_u32() {
958 let value: u32 = 5000;
959 let qty = Quantity::from(value);
960 assert_eq!(qty.raw, Quantity::from(format!("{value}")).raw);
961 assert_eq!(qty.precision, 0);
962 }
963
964 #[rstest]
965 fn test_from_i64() {
966 let value = 100_000i64;
967 let qty = Quantity::from(value);
968 assert_eq!(qty, qty);
969 assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
970 assert_eq!(qty.precision, 0);
971 }
972
973 #[rstest]
974 fn test_from_u64() {
975 let value = 100_000u64;
976 let qty = Quantity::from(value);
977 assert_eq!(qty, qty);
978 assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
979 assert_eq!(qty.precision, 0);
980 }
981
982 #[rstest] fn test_with_maximum_value() {
984 let qty = Quantity::new_checked(QUANTITY_MAX, 0);
985 assert!(qty.is_ok());
986 }
987
988 #[rstest]
989 fn test_with_minimum_positive_value() {
990 let value = 0.000_000_001;
991 let qty = Quantity::new(value, 9);
992 assert_eq!(qty.raw, Quantity::from("0.000000001").raw);
993 assert_eq!(qty.as_decimal(), dec!(0.000000001));
994 assert_eq!(qty.to_string(), "0.000000001");
995 }
996
997 #[rstest]
998 fn test_with_minimum_value() {
999 let qty = Quantity::new(QUANTITY_MIN, 9);
1000 assert_eq!(qty.raw, 0);
1001 assert_eq!(qty.as_decimal(), dec!(0));
1002 assert_eq!(qty.to_string(), "0.000000000");
1003 }
1004
1005 #[rstest]
1006 fn test_is_zero() {
1007 let qty = Quantity::zero(8);
1008 assert_eq!(qty, qty);
1009 assert_eq!(qty.raw, 0);
1010 assert_eq!(qty.precision, 8);
1011 assert_eq!(qty, Quantity::from("0.00000000"));
1012 assert_eq!(qty.as_decimal(), dec!(0));
1013 assert_eq!(qty.to_string(), "0.00000000");
1014 assert!(qty.is_zero());
1015 }
1016
1017 #[rstest]
1018 fn test_precision() {
1019 let value = 1.001;
1020 let qty = Quantity::new(value, 2);
1021 assert_eq!(qty.to_string(), "1.00");
1022 }
1023
1024 #[rstest]
1025 fn test_new_from_str() {
1026 let qty = Quantity::new(0.00812000, 8);
1027 assert_eq!(qty, qty);
1028 assert_eq!(qty.precision, 8);
1029 assert_eq!(qty, Quantity::from("0.00812000"));
1030 assert_eq!(qty.to_string(), "0.00812000");
1031 }
1032
1033 #[rstest]
1034 #[case("0", 0)]
1035 #[case("1.1", 1)]
1036 #[case("1.123456789", 9)]
1037 fn test_from_str_valid_input(#[case] input: &str, #[case] expected_prec: u8) {
1038 let qty = Quantity::from(input);
1039 assert_eq!(qty.precision, expected_prec);
1040 assert_eq!(qty.as_decimal(), Decimal::from_str(input).unwrap());
1041 }
1042
1043 #[rstest]
1044 #[should_panic]
1045 fn test_from_str_invalid_input() {
1046 let input = "invalid";
1047 Quantity::new(f64::from_str(input).unwrap(), 8);
1048 }
1049
1050 #[rstest]
1051 fn test_from_str_errors() {
1052 assert!(Quantity::from_str("invalid").is_err());
1053 assert!(Quantity::from_str("12.34.56").is_err());
1054 assert!(Quantity::from_str("").is_err());
1055 assert!(Quantity::from_str("-1").is_err()); assert!(Quantity::from_str("-0.001").is_err());
1057 }
1058
1059 #[rstest]
1060 #[case("1e7", 0, 10_000_000.0)]
1061 #[case("2.5e3", 0, 2_500.0)]
1062 #[case("1.234e-2", 5, 0.01234)]
1063 #[case("5E-3", 3, 0.005)]
1064 #[case("1.0e6", 0, 1_000_000.0)]
1065 fn test_from_str_scientific_notation(
1066 #[case] input: &str,
1067 #[case] expected_precision: u8,
1068 #[case] expected_value: f64,
1069 ) {
1070 let qty = Quantity::from_str(input).unwrap();
1071 assert_eq!(qty.precision, expected_precision);
1072 assert!(approx_eq!(
1073 f64,
1074 qty.as_f64(),
1075 expected_value,
1076 epsilon = 1e-10
1077 ));
1078 }
1079
1080 #[rstest]
1081 #[case("1_234.56", 2, 1234.56)]
1082 #[case("1_000_000", 0, 1_000_000.0)]
1083 #[case("99_999.999_99", 5, 99_999.999_99)]
1084 fn test_from_str_with_underscores(
1085 #[case] input: &str,
1086 #[case] expected_precision: u8,
1087 #[case] expected_value: f64,
1088 ) {
1089 let qty = Quantity::from_str(input).unwrap();
1090 assert_eq!(qty.precision, expected_precision);
1091 assert!(approx_eq!(
1092 f64,
1093 qty.as_f64(),
1094 expected_value,
1095 epsilon = 1e-10
1096 ));
1097 }
1098
1099 #[rstest]
1100 fn test_from_decimal_dp_preservation() {
1101 let decimal = dec!(123.456789);
1103 let qty = Quantity::from_decimal_dp(decimal, 6).unwrap();
1104 assert_eq!(qty.precision, 6);
1105 assert!(approx_eq!(f64, qty.as_f64(), 123.456789, epsilon = 1e-10));
1106
1107 let expected_raw = 123456789_u64 * 10_u64.pow((FIXED_PRECISION - 6) as u32);
1109 assert_eq!(qty.raw, expected_raw as QuantityRaw);
1110 }
1111
1112 #[rstest]
1113 fn test_from_decimal_dp_rounding() {
1114 let decimal = dec!(1.005);
1116 let qty = Quantity::from_decimal_dp(decimal, 2).unwrap();
1117 assert_eq!(qty.as_f64(), 1.0); let decimal = dec!(1.015);
1120 let qty = Quantity::from_decimal_dp(decimal, 2).unwrap();
1121 assert_eq!(qty.as_f64(), 1.02); }
1123
1124 #[rstest]
1125 fn test_from_decimal_infers_precision() {
1126 let decimal = dec!(123.456);
1128 let qty = Quantity::from_decimal(decimal).unwrap();
1129 assert_eq!(qty.precision, 3);
1130 assert!(approx_eq!(f64, qty.as_f64(), 123.456, epsilon = 1e-10));
1131
1132 let decimal = dec!(100);
1134 let qty = Quantity::from_decimal(decimal).unwrap();
1135 assert_eq!(qty.precision, 0);
1136 assert_eq!(qty.as_f64(), 100.0);
1137
1138 let decimal = dec!(1.23456789);
1140 let qty = Quantity::from_decimal(decimal).unwrap();
1141 assert_eq!(qty.precision, 8);
1142 assert!(approx_eq!(f64, qty.as_f64(), 1.23456789, epsilon = 1e-10));
1143 }
1144
1145 #[rstest]
1146 fn test_from_decimal_trailing_zeros() {
1147 let decimal = dec!(5.670);
1149 assert_eq!(decimal.scale(), 3); let qty = Quantity::from_decimal(decimal).unwrap();
1153 assert_eq!(qty.precision, 3);
1154 assert!(approx_eq!(f64, qty.as_f64(), 5.67, epsilon = 1e-10));
1155
1156 let normalized = decimal.normalize();
1158 assert_eq!(normalized.scale(), 2);
1159 let qty_normalized = Quantity::from_decimal(normalized).unwrap();
1160 assert_eq!(qty_normalized.precision, 2);
1161 }
1162
1163 #[rstest]
1164 #[case("1.00", 2)]
1165 #[case("1.0", 1)]
1166 #[case("1.000", 3)]
1167 #[case("100.00", 2)]
1168 #[case("0.10", 2)]
1169 #[case("0.100", 3)]
1170 fn test_from_str_preserves_trailing_zeros(#[case] input: &str, #[case] expected_precision: u8) {
1171 let qty = Quantity::from_str(input).unwrap();
1172 assert_eq!(qty.precision, expected_precision);
1173 }
1174
1175 #[rstest]
1176 fn test_from_decimal_excessive_precision_inference() {
1177 let decimal = dec!(1.1234567890123456789012345678);
1180
1181 if decimal.scale() > FIXED_PRECISION as u32 {
1183 assert!(Quantity::from_decimal(decimal).is_err());
1184 }
1185 }
1186
1187 #[rstest]
1188 fn test_from_decimal_negative_quantity_errors() {
1189 let decimal = dec!(-123.45);
1191 let result = Quantity::from_decimal(decimal);
1192 assert!(result.is_err());
1193
1194 let result = Quantity::from_decimal_dp(decimal, 2);
1196 assert!(result.is_err());
1197 }
1198
1199 #[rstest]
1200 fn test_add() {
1201 let a = 1.0;
1202 let b = 2.0;
1203 let quantity1 = Quantity::new(1.0, 0);
1204 let quantity2 = Quantity::new(2.0, 0);
1205 let quantity3 = quantity1 + quantity2;
1206 assert_eq!(quantity3.raw, Quantity::new(a + b, 0).raw);
1207 }
1208
1209 #[rstest]
1210 fn test_sub() {
1211 let a = 3.0;
1212 let b = 2.0;
1213 let quantity1 = Quantity::new(a, 0);
1214 let quantity2 = Quantity::new(b, 0);
1215 let quantity3 = quantity1 - quantity2;
1216 assert_eq!(quantity3.raw, Quantity::new(a - b, 0).raw);
1217 }
1218
1219 #[rstest]
1220 fn test_mul() {
1221 let value = 2.0;
1222 let quantity1 = Quantity::new(value, 1);
1223 let quantity2 = Quantity::new(value, 1);
1224 let quantity3 = quantity1 * quantity2;
1225 assert_eq!(quantity3.raw, Quantity::new(value * value, 0).raw);
1226 }
1227
1228 #[rstest]
1229 fn test_comparisons() {
1230 assert_eq!(Quantity::new(1.0, 1), Quantity::new(1.0, 1));
1231 assert_eq!(Quantity::new(1.0, 1), Quantity::new(1.0, 2));
1232 assert_ne!(Quantity::new(1.1, 1), Quantity::new(1.0, 1));
1233 assert!(Quantity::new(1.0, 1) <= Quantity::new(1.0, 2));
1234 assert!(Quantity::new(1.1, 1) > Quantity::new(1.0, 1));
1235 assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 1));
1236 assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 2));
1237 assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 2));
1238 assert!(Quantity::new(0.9, 1) < Quantity::new(1.0, 1));
1239 assert!(Quantity::new(0.9, 1) <= Quantity::new(1.0, 2));
1240 assert!(Quantity::new(0.9, 1) <= Quantity::new(1.0, 1));
1241 }
1242
1243 #[rstest]
1244 fn test_debug() {
1245 let quantity = Quantity::from_str("44.12").unwrap();
1246 let result = format!("{quantity:?}");
1247 assert_eq!(result, "Quantity(44.12)");
1248 }
1249
1250 #[rstest]
1251 fn test_display() {
1252 let quantity = Quantity::from_str("44.12").unwrap();
1253 let result = format!("{quantity}");
1254 assert_eq!(result, "44.12");
1255 }
1256
1257 #[rstest]
1258 #[case(44.12, 2, "Quantity(44.12)", "44.12")] #[case(1234.567, 8, "Quantity(1234.56700000)", "1234.56700000")] #[cfg_attr(
1261 feature = "defi",
1262 case(
1263 1_000_000_000_000_000_000.0,
1264 18,
1265 "Quantity(1000000000000000000)",
1266 "1000000000000000000"
1267 )
1268 )] fn test_debug_display_precision_handling(
1270 #[case] value: f64,
1271 #[case] precision: u8,
1272 #[case] expected_debug: &str,
1273 #[case] expected_display: &str,
1274 ) {
1275 let quantity = if precision > MAX_FLOAT_PRECISION {
1276 Quantity::from_raw(value as QuantityRaw, precision)
1278 } else {
1279 Quantity::new(value, precision)
1280 };
1281
1282 assert_eq!(format!("{quantity:?}"), expected_debug);
1283 assert_eq!(format!("{quantity}"), expected_display);
1284 }
1285
1286 #[rstest]
1287 fn test_to_formatted_string() {
1288 let qty = Quantity::new(1234.5678, 4);
1289 let formatted = qty.to_formatted_string();
1290 assert_eq!(formatted, "1_234.5678");
1291 assert_eq!(qty.to_string(), "1234.5678");
1292 }
1293
1294 #[rstest]
1295 fn test_saturating_sub() {
1296 let q1 = Quantity::new(100.0, 2);
1297 let q2 = Quantity::new(50.0, 2);
1298 let q3 = Quantity::new(150.0, 2);
1299
1300 let result = q1.saturating_sub(q2);
1301 assert_eq!(result, Quantity::new(50.0, 2));
1302
1303 let result = q1.saturating_sub(q3);
1304 assert_eq!(result, Quantity::zero(2));
1305 assert_eq!(result.raw, 0);
1306 }
1307
1308 #[rstest]
1309 fn test_saturating_sub_overflow_bug() {
1310 use crate::types::fixed::FIXED_PRECISION;
1313 let precision = 3;
1314 let scale = 10u64.pow(u32::from(FIXED_PRECISION - precision)) as QuantityRaw;
1315
1316 let peak_qty = Quantity::from_raw(79 * scale, precision);
1318 let order_qty = Quantity::from_raw(80 * scale, precision);
1319
1320 let result = peak_qty.saturating_sub(order_qty);
1322 assert_eq!(result.raw, 0);
1323 assert_eq!(result, Quantity::zero(precision));
1324 }
1325
1326 #[rstest]
1327 fn test_hash() {
1328 use std::{
1329 collections::hash_map::DefaultHasher,
1330 hash::{Hash, Hasher},
1331 };
1332
1333 let q1 = Quantity::new(100.0, 1);
1334 let q2 = Quantity::new(100.0, 1);
1335 let q3 = Quantity::new(200.0, 1);
1336
1337 let mut s1 = DefaultHasher::new();
1338 let mut s2 = DefaultHasher::new();
1339 let mut s3 = DefaultHasher::new();
1340
1341 q1.hash(&mut s1);
1342 q2.hash(&mut s2);
1343 q3.hash(&mut s3);
1344
1345 assert_eq!(
1346 s1.finish(),
1347 s2.finish(),
1348 "Equal quantities must hash equally"
1349 );
1350 assert_ne!(
1351 s1.finish(),
1352 s3.finish(),
1353 "Different quantities must hash differently"
1354 );
1355 }
1356
1357 #[rstest]
1358 fn test_quantity_serde_json_round_trip() {
1359 let original = Quantity::new(123.456, 3);
1360 let json_str = serde_json::to_string(&original).unwrap();
1361 assert_eq!(json_str, "\"123.456\"");
1362
1363 let deserialized: Quantity = serde_json::from_str(&json_str).unwrap();
1364 assert_eq!(deserialized, original);
1365 assert_eq!(deserialized.precision, 3);
1366 }
1367
1368 #[rstest]
1369 fn test_from_mantissa_exponent_exact_precision() {
1370 let qty = Quantity::from_mantissa_exponent(12345, -2, 2);
1371 assert_eq!(qty.as_f64(), 123.45);
1372 }
1373
1374 #[rstest]
1375 fn test_from_mantissa_exponent_excess_rounds_down() {
1376 let qty = Quantity::from_mantissa_exponent(12345, -3, 2);
1379 assert_eq!(qty.as_f64(), 12.34);
1380 }
1381
1382 #[rstest]
1383 fn test_from_mantissa_exponent_excess_rounds_up() {
1384 let qty = Quantity::from_mantissa_exponent(12355, -3, 2);
1386 assert_eq!(qty.as_f64(), 12.36);
1387 }
1388
1389 #[rstest]
1390 fn test_from_mantissa_exponent_positive_exponent() {
1391 let qty = Quantity::from_mantissa_exponent(5, 2, 0);
1392 assert_eq!(qty.as_f64(), 500.0);
1393 }
1394
1395 #[rstest]
1396 fn test_from_mantissa_exponent_zero() {
1397 let qty = Quantity::from_mantissa_exponent(0, 2, 2);
1398 assert_eq!(qty.as_f64(), 0.0);
1399 }
1400
1401 #[rstest]
1402 #[should_panic]
1403 fn test_from_mantissa_exponent_overflow_panics() {
1404 let _ = Quantity::from_mantissa_exponent(u64::MAX, 9, 0);
1405 }
1406
1407 #[rstest]
1408 #[should_panic(expected = "exceeds i128 range")]
1409 fn test_from_mantissa_exponent_large_exponent_panics() {
1410 let _ = Quantity::from_mantissa_exponent(1, 119, 0);
1411 }
1412
1413 #[rstest]
1414 fn test_from_mantissa_exponent_zero_with_large_exponent() {
1415 let qty = Quantity::from_mantissa_exponent(0, 119, 0);
1416 assert_eq!(qty.as_f64(), 0.0);
1417 }
1418
1419 #[rstest]
1420 fn test_from_mantissa_exponent_very_negative_exponent_rounds_to_zero() {
1421 let qty = Quantity::from_mantissa_exponent(12345, -120, 2);
1422 assert_eq!(qty.as_f64(), 0.0);
1423 }
1424
1425 #[rstest]
1426 fn test_f64_operations() {
1427 let q = Quantity::new(10.5, 2);
1428 assert_eq!(q + 1.0, 11.5);
1429 assert_eq!(q - 1.0, 9.5);
1430 assert_eq!(q * 2.0, 21.0);
1431 assert_eq!(q / 2.0, 5.25);
1432 }
1433
1434 #[rstest]
1435 fn test_decimal_arithmetic_operations() {
1436 let qty = Quantity::new(100.0, 2);
1437 assert_eq!(qty + dec!(50.25), dec!(150.25));
1438 assert_eq!(qty - dec!(30.50), dec!(69.50));
1439 assert_eq!(qty * dec!(1.5), dec!(150.00));
1440 assert_eq!(qty / dec!(4), dec!(25.00));
1441 }
1442
1443 #[rstest]
1447 #[cfg(feature = "defi")]
1448 #[case::sell_tx_rain_amount(
1449 U256::from_str_radix("42193532365637161405123", 10).unwrap(),
1450 18,
1451 "42193.532365637161405123"
1452 )]
1453 #[case::sell_tx_weth_amount(
1454 U256::from_str_radix("112633187203033110", 10).unwrap(),
1455 18,
1456 "0.112633187203033110"
1457 )]
1458 fn test_from_u256_real_swap_data(
1459 #[case] amount: U256,
1460 #[case] precision: u8,
1461 #[case] expected_str: &str,
1462 ) {
1463 let qty = Quantity::from_u256(amount, precision).unwrap();
1464 assert_eq!(qty.precision, precision);
1465 assert_eq!(qty.as_decimal().to_string(), expected_str);
1466 }
1467}
1468
1469#[cfg(test)]
1470mod property_tests {
1471 use proptest::prelude::*;
1472 use rstest::rstest;
1473
1474 use super::*;
1475
1476 fn quantity_value_strategy() -> impl Strategy<Value = f64> {
1478 prop_oneof![
1480 0.00001..1.0,
1482 1.0..100_000.0,
1484 100_000.0..1_000_000.0,
1486 Just(0.0),
1488 Just(QUANTITY_MAX / 2.0),
1490 ]
1491 }
1492
1493 fn precision_strategy() -> impl Strategy<Value = u8> {
1495 let upper = FIXED_PRECISION.min(MAX_FLOAT_PRECISION);
1496 prop_oneof![Just(0u8), 0u8..=upper, Just(FIXED_PRECISION),]
1497 }
1498
1499 fn precision_strategy_non_zero() -> impl Strategy<Value = u8> {
1500 let upper = FIXED_PRECISION.clamp(1, MAX_FLOAT_PRECISION);
1501 prop_oneof![Just(upper), Just(FIXED_PRECISION.max(1)), 1u8..=upper,]
1502 }
1503
1504 fn raw_for_precision_strategy() -> impl Strategy<Value = (QuantityRaw, u8)> {
1505 precision_strategy().prop_flat_map(|precision| {
1506 let step_u128 = 10u128.pow(u32::from(FIXED_PRECISION.saturating_sub(precision)));
1507 #[cfg(feature = "high-precision")]
1508 let max_steps_u128 = QUANTITY_RAW_MAX / step_u128;
1509 #[cfg(not(feature = "high-precision"))]
1510 let max_steps_u128 = (QUANTITY_RAW_MAX as u128) / step_u128;
1511
1512 (0u128..=max_steps_u128).prop_map(move |steps_u128| {
1513 let raw_u128 = steps_u128 * step_u128;
1514 #[cfg(feature = "high-precision")]
1515 let raw = raw_u128;
1516 #[cfg(not(feature = "high-precision"))]
1517 let raw = raw_u128
1518 .try_into()
1519 .expect("raw value should fit in QuantityRaw");
1520 (raw, precision)
1521 })
1522 })
1523 }
1524
1525 const DECIMAL_MAX_MANTISSA: u128 = 79_228_162_514_264_337_593_543_950_335;
1526
1527 fn decimal_compatible(raw: QuantityRaw, precision: u8) -> bool {
1528 if precision > MAX_FLOAT_PRECISION {
1529 return false;
1530 }
1531 let precision_diff = u32::from(FIXED_PRECISION.saturating_sub(precision));
1532 let divisor = 10u128.pow(precision_diff);
1533 #[cfg(feature = "high-precision")]
1534 let rescaled_raw = raw / divisor;
1535 #[cfg(not(feature = "high-precision"))]
1536 let rescaled_raw = (raw as u128) / divisor;
1537 rescaled_raw <= DECIMAL_MAX_MANTISSA
1540 }
1541
1542 proptest! {
1543 #[rstest]
1545 fn prop_quantity_serde_round_trip(
1546 (raw, precision) in raw_for_precision_strategy()
1547 ) {
1548 prop_assume!(decimal_compatible(raw, precision));
1550
1551 let original = Quantity::from_raw(raw, precision);
1552
1553 let string_repr = original.to_string();
1555 let from_string: Quantity = string_repr.parse().unwrap();
1556 prop_assert_eq!(from_string.raw, original.raw);
1557 prop_assert_eq!(from_string.precision, original.precision);
1558
1559 let json = serde_json::to_string(&original).unwrap();
1561 let from_json: Quantity = serde_json::from_str(&json).unwrap();
1562 prop_assert_eq!(from_json.precision, original.precision);
1563 prop_assert_eq!(from_json.raw, original.raw);
1564 }
1565
1566 #[rstest]
1568 fn prop_quantity_arithmetic_associative(
1569 a in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
1570 b in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
1571 c in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
1572 precision in precision_strategy()
1573 ) {
1574 let q_a = Quantity::new(a, precision);
1575 let q_b = Quantity::new(b, precision);
1576 let q_c = Quantity::new(c, precision);
1577
1578 let ab_raw = q_a.raw.checked_add(q_b.raw);
1580 let bc_raw = q_b.raw.checked_add(q_c.raw);
1581
1582 if let (Some(ab_raw), Some(bc_raw)) = (ab_raw, bc_raw) {
1583 let ab_c_raw = ab_raw.checked_add(q_c.raw);
1584 let a_bc_raw = q_a.raw.checked_add(bc_raw);
1585
1586 if let (Some(ab_c_raw), Some(a_bc_raw)) = (ab_c_raw, a_bc_raw) {
1587 prop_assert_eq!(ab_c_raw, a_bc_raw, "Associativity failed in raw arithmetic");
1589 }
1590 }
1591 }
1592
1593 #[rstest]
1595 fn prop_quantity_addition_subtraction_inverse(
1596 base in quantity_value_strategy().prop_filter("Reasonable values", |&x| x < 1e6),
1597 delta in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
1598 precision in precision_strategy()
1599 ) {
1600 let q_base = Quantity::new(base, precision);
1601 let q_delta = Quantity::new(delta, precision);
1602
1603 if let Some(added_raw) = q_base.raw.checked_add(q_delta.raw)
1605 && let Some(result_raw) = added_raw.checked_sub(q_delta.raw) {
1606 prop_assert_eq!(result_raw, q_base.raw, "Inverse operation failed in raw arithmetic");
1608 }
1609 }
1610
1611 #[rstest]
1613 fn prop_quantity_ordering_transitive(
1614 a in quantity_value_strategy(),
1615 b in quantity_value_strategy(),
1616 c in quantity_value_strategy(),
1617 precision in precision_strategy()
1618 ) {
1619 let q_a = Quantity::new(a, precision);
1620 let q_b = Quantity::new(b, precision);
1621 let q_c = Quantity::new(c, precision);
1622
1623 if q_a <= q_b && q_b <= q_c {
1625 prop_assert!(q_a <= q_c, "Transitivity failed: {} <= {} <= {} but {} > {}",
1626 q_a.as_f64(), q_b.as_f64(), q_c.as_f64(), q_a.as_f64(), q_c.as_f64());
1627 }
1628 }
1629
1630 #[rstest]
1632 fn prop_quantity_string_parsing_precision(
1633 integral in 0u32..1000000,
1634 fractional in 0u32..1000000,
1635 precision in precision_strategy_non_zero()
1636 ) {
1637 let pow = 10u128.pow(u32::from(precision));
1639 let fractional_mod = (fractional as u128) % pow;
1640 let fractional_str = format!("{:0width$}", fractional_mod, width = precision as usize);
1641 let quantity_str = format!("{integral}.{fractional_str}");
1642
1643 let parsed: Quantity = quantity_str.parse().unwrap();
1644 prop_assert_eq!(parsed.precision, precision);
1645
1646 let round_trip = parsed.to_string();
1648 let expected_value = format!("{integral}.{fractional_str}");
1649 prop_assert_eq!(round_trip, expected_value);
1650 }
1651
1652 #[rstest]
1654 fn prop_quantity_precision_information_preservation(
1655 value in quantity_value_strategy().prop_filter("Reasonable values", |&x| x < 1e6),
1656 precision1 in precision_strategy_non_zero(),
1657 precision2 in precision_strategy_non_zero()
1658 ) {
1659 prop_assume!(precision1 != precision2);
1661
1662 let _q1 = Quantity::new(value, precision1);
1663 let _q2 = Quantity::new(value, precision2);
1664
1665 let min_precision = precision1.min(precision2);
1668
1669 let scale = 10.0_f64.powi(min_precision as i32);
1671 let rounded_value = (value * scale).round() / scale;
1672
1673 let q1_reduced = Quantity::new(rounded_value, min_precision);
1674 let q2_reduced = Quantity::new(rounded_value, min_precision);
1675
1676 prop_assert_eq!(q1_reduced.raw, q2_reduced.raw, "Precision reduction inconsistent");
1678 }
1679
1680 #[rstest]
1682 fn prop_quantity_arithmetic_bounds(
1683 a in quantity_value_strategy(),
1684 b in quantity_value_strategy(),
1685 precision in precision_strategy()
1686 ) {
1687 let q_a = Quantity::new(a, precision);
1688 let q_b = Quantity::new(b, precision);
1689
1690 let sum_f64 = q_a.as_f64() + q_b.as_f64();
1692 if sum_f64.is_finite() && (QUANTITY_MIN..=QUANTITY_MAX).contains(&sum_f64) {
1693 let sum = q_a + q_b;
1694 prop_assert!(sum.as_f64().is_finite());
1695 prop_assert!(!sum.is_undefined());
1696 }
1697
1698 let diff_f64 = q_a.as_f64() - q_b.as_f64();
1700 if diff_f64.is_finite() && (QUANTITY_MIN..=QUANTITY_MAX).contains(&diff_f64) {
1701 let diff = q_a - q_b;
1702 prop_assert!(diff.as_f64().is_finite());
1703 prop_assert!(!diff.is_undefined());
1704 }
1705 }
1706
1707 #[rstest]
1709 fn prop_quantity_multiplication_non_negative(
1710 a in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 0.0 && x < 10.0),
1711 b in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 0.0 && x < 10.0),
1712 precision in precision_strategy()
1713 ) {
1714 let q_a = Quantity::new(a, precision);
1715 let q_b = Quantity::new(b, precision);
1716
1717 let raw_product_check = q_a.raw.checked_mul(q_b.raw);
1719
1720 if let Some(raw_product) = raw_product_check {
1721 let scaled_raw = raw_product / (FIXED_SCALAR as QuantityRaw);
1723 if scaled_raw <= QUANTITY_RAW_MAX {
1724 let product = q_a * q_b;
1726 prop_assert!(product.as_f64() >= 0.0, "Quantity multiplication produced negative value: {}", product.as_f64());
1727 }
1728 }
1729 }
1730
1731 #[rstest]
1733 fn prop_quantity_zero_addition_identity(
1734 value in quantity_value_strategy(),
1735 precision in precision_strategy()
1736 ) {
1737 let q = Quantity::new(value, precision);
1738 let zero = Quantity::zero(precision);
1739
1740 prop_assert_eq!(q + zero, q);
1742 prop_assert_eq!(zero + q, q);
1743 }
1744 }
1745
1746 proptest! {
1747 #[rstest]
1749 fn prop_quantity_as_decimal_preserves_precision(
1750 (raw, precision) in raw_for_precision_strategy()
1751 ) {
1752 prop_assume!(decimal_compatible(raw, precision));
1753 let quantity = Quantity::from_raw(raw, precision);
1754 let decimal = quantity.as_decimal();
1755 prop_assert_eq!(decimal.scale(), u32::from(precision));
1756 }
1757
1758 #[rstest]
1760 fn prop_quantity_as_decimal_matches_display(
1761 (raw, precision) in raw_for_precision_strategy()
1762 ) {
1763 prop_assume!(decimal_compatible(raw, precision));
1764 let quantity = Quantity::from_raw(raw, precision);
1765 let display_str = format!("{quantity}");
1766 let decimal_str = quantity.as_decimal().to_string();
1767 prop_assert_eq!(display_str, decimal_str);
1768 }
1769
1770 #[rstest]
1772 fn prop_quantity_from_decimal_roundtrip(
1773 (raw, precision) in raw_for_precision_strategy()
1774 ) {
1775 prop_assume!(decimal_compatible(raw, precision));
1776 let original = Quantity::from_raw(raw, precision);
1777 let decimal = original.as_decimal();
1778 let reconstructed = Quantity::from_decimal(decimal).unwrap();
1779 prop_assert_eq!(original.raw, reconstructed.raw);
1780 prop_assert_eq!(original.precision, reconstructed.precision);
1781 }
1782
1783 #[rstest]
1785 fn prop_quantity_from_raw_round_trip(
1786 (raw, precision) in raw_for_precision_strategy()
1787 ) {
1788 let quantity = Quantity::from_raw(raw, precision);
1789 prop_assert_eq!(quantity.raw, raw);
1790 prop_assert_eq!(quantity.precision, precision);
1791 }
1792 }
1793}