1use std::{
19 cmp::Ordering,
20 fmt::{Debug, Display},
21 hash::{Hash, Hasher},
22 ops::{Add, AddAssign, Deref, Mul, MulAssign, Sub, SubAssign},
23 str::FromStr,
24};
25
26#[cfg(feature = "defi")]
27use alloy_primitives::U256;
28use nautilus_core::{
29 correctness::{FAILED, check_in_range_inclusive_f64, check_predicate_true},
30 formatting::Separable,
31};
32use rust_decimal::{Decimal, prelude::ToPrimitive};
33use serde::{Deserialize, Deserializer, Serialize};
34
35use super::fixed::{FIXED_PRECISION, FIXED_SCALAR, MAX_FLOAT_PRECISION, check_fixed_precision};
36#[cfg(not(feature = "high-precision"))]
37use super::fixed::{f64_to_fixed_u64, fixed_u64_to_f64};
38#[cfg(feature = "high-precision")]
39use super::fixed::{f64_to_fixed_u128, fixed_u128_to_f64};
40
41#[cfg(feature = "high-precision")]
46pub type QuantityRaw = u128;
47
48#[cfg(not(feature = "high-precision"))]
49pub type QuantityRaw = u64;
50
51#[unsafe(no_mangle)]
55#[allow(unsafe_code)]
56pub static QUANTITY_RAW_MAX: QuantityRaw = (QUANTITY_MAX * FIXED_SCALAR) as QuantityRaw;
57
58pub const QUANTITY_UNDEF: QuantityRaw = QuantityRaw::MAX;
60
61#[cfg(feature = "high-precision")]
66pub const QUANTITY_MAX: f64 = 34_028_236_692_093.0;
68
69#[cfg(not(feature = "high-precision"))]
70pub const QUANTITY_MAX: f64 = 18_446_744_073.0;
72
73pub const QUANTITY_MIN: f64 = 0.0;
77
78#[repr(C)]
89#[derive(Clone, Copy, Default, Eq)]
90#[cfg_attr(
91 feature = "python",
92 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", frozen)
93)]
94pub struct Quantity {
95 pub raw: QuantityRaw,
97 pub precision: u8,
99}
100
101impl Quantity {
102 pub fn new_checked(value: f64, precision: u8) -> anyhow::Result<Self> {
114 check_in_range_inclusive_f64(value, QUANTITY_MIN, QUANTITY_MAX, "value")?;
115
116 #[cfg(feature = "defi")]
117 if precision > MAX_FLOAT_PRECISION {
118 anyhow::bail!(
120 "`precision` exceeded maximum float precision ({MAX_FLOAT_PRECISION}), use `Quantity::from_wei()` for wei values instead"
121 );
122 }
123
124 check_fixed_precision(precision)?;
125
126 #[cfg(feature = "high-precision")]
127 let raw = f64_to_fixed_u128(value, precision);
128 #[cfg(not(feature = "high-precision"))]
129 let raw = f64_to_fixed_u64(value, precision);
130
131 Ok(Self { raw, precision })
132 }
133
134 pub fn non_zero_checked(value: f64, precision: u8) -> anyhow::Result<Self> {
148 check_predicate_true(value != 0.0, "value was zero")?;
149 check_fixed_precision(precision)?;
150 let rounded_value =
151 (value * 10.0_f64.powi(precision as i32)).round() / 10.0_f64.powi(precision as i32);
152 check_predicate_true(
153 rounded_value != 0.0,
154 &format!("value {value} was zero after rounding to precision {precision}"),
155 )?;
156
157 Self::new_checked(value, precision)
158 }
159
160 pub fn new(value: f64, precision: u8) -> Self {
166 Self::new_checked(value, precision).expect(FAILED)
167 }
168
169 pub fn non_zero(value: f64, precision: u8) -> Self {
175 Self::non_zero_checked(value, precision).expect(FAILED)
176 }
177
178 pub fn from_raw(raw: QuantityRaw, precision: u8) -> Self {
184 Self::from_raw_checked(raw, precision).expect(FAILED)
194 }
195
196 pub fn from_raw_checked(raw: QuantityRaw, precision: u8) -> anyhow::Result<Self> {
206 if raw == QUANTITY_UNDEF {
207 anyhow::ensure!(
208 precision == 0,
209 "`precision` must be 0 when `raw` is QUANTITY_UNDEF"
210 );
211 }
212 anyhow::ensure!(
213 raw == QUANTITY_UNDEF || raw <= QUANTITY_RAW_MAX,
214 "raw value {raw} exceeds QUANTITY_RAW_MAX={QUANTITY_RAW_MAX}"
215 );
216 check_fixed_precision(precision)?;
217
218 Ok(Self { raw, precision })
219 }
220
221 #[must_use]
230 pub fn saturating_sub(self, rhs: Self) -> Self {
231 let precision = match self.precision {
232 0 => rhs.precision,
233 _ => self.precision,
234 };
235 assert!(
236 self.precision >= rhs.precision,
237 "Precision mismatch: cannot subtract precision {} from precision {} (precision loss)",
238 rhs.precision,
239 self.precision,
240 );
241
242 let raw = self.raw.saturating_sub(rhs.raw);
243 if raw == 0 && self.raw < rhs.raw {
244 log::warn!(
245 "Saturating Quantity subtraction: {self} - {rhs} < 0, clamped to 0 (precision={precision})"
246 );
247 }
248
249 Self { raw, precision }
250 }
251
252 #[must_use]
258 pub fn zero(precision: u8) -> Self {
259 check_fixed_precision(precision).expect(FAILED);
260 Self::new(0.0, precision)
261 }
262
263 #[must_use]
265 pub fn is_undefined(&self) -> bool {
266 self.raw == QUANTITY_UNDEF
267 }
268
269 #[must_use]
271 pub fn is_zero(&self) -> bool {
272 self.raw == 0
273 }
274
275 #[must_use]
277 pub fn is_positive(&self) -> bool {
278 self.raw != QUANTITY_UNDEF && self.raw > 0
279 }
280
281 #[cfg(feature = "high-precision")]
282 #[must_use]
288 pub fn as_f64(&self) -> f64 {
289 #[cfg(feature = "defi")]
290 if self.precision > MAX_FLOAT_PRECISION {
291 panic!("Invalid f64 conversion beyond `MAX_FLOAT_PRECISION` (16)");
292 }
293
294 fixed_u128_to_f64(self.raw)
295 }
296
297 #[cfg(not(feature = "high-precision"))]
298 #[must_use]
304 pub fn as_f64(&self) -> f64 {
305 #[cfg(feature = "defi")]
306 if self.precision > MAX_FLOAT_PRECISION {
307 panic!("Invalid f64 conversion beyond `MAX_FLOAT_PRECISION` (16)");
308 }
309
310 fixed_u64_to_f64(self.raw)
311 }
312
313 #[must_use]
315 pub fn as_decimal(&self) -> Decimal {
316 let precision_diff = FIXED_PRECISION.saturating_sub(self.precision);
318 let rescaled_raw = self.raw / QuantityRaw::pow(10, u32::from(precision_diff));
319
320 #[allow(clippy::useless_conversion)]
324 Decimal::from_i128_with_scale(rescaled_raw as i128, u32::from(self.precision))
325 }
326
327 #[must_use]
329 pub fn to_formatted_string(&self) -> String {
330 format!("{self}").separate_with_underscores()
331 }
332
333 pub fn from_decimal_dp(decimal: Decimal, precision: u8) -> anyhow::Result<Self> {
346 check_fixed_precision(precision)?;
347
348 let scale_factor = Decimal::from(10_i64.pow(precision as u32));
350 let scaled = decimal * scale_factor;
351 let rounded = scaled.round();
352
353 #[cfg(feature = "high-precision")]
354 let raw_at_precision: QuantityRaw = rounded.to_u128().ok_or_else(|| {
355 anyhow::anyhow!("Decimal value '{decimal}' cannot be converted to u128")
356 })?;
357 #[cfg(not(feature = "high-precision"))]
358 let raw_at_precision: QuantityRaw = rounded.to_u64().ok_or_else(|| {
359 anyhow::anyhow!("Decimal value '{decimal}' cannot be converted to u64")
360 })?;
361
362 let scale_up = 10_u64.pow((FIXED_PRECISION - precision) as u32) as QuantityRaw;
363 let raw = raw_at_precision
364 .checked_mul(scale_up)
365 .ok_or_else(|| anyhow::anyhow!("Overflow when scaling to fixed precision"))?;
366
367 check_predicate_true(
368 raw <= QUANTITY_RAW_MAX,
369 &format!("raw value outside valid range, was {raw}"),
370 )?;
371
372 Ok(Self { raw, precision })
373 }
374
375 pub fn from_decimal(decimal: Decimal) -> anyhow::Result<Self> {
387 let precision = decimal.scale() as u8;
388 Self::from_decimal_dp(decimal, precision)
389 }
390
391 #[cfg(feature = "defi")]
399 pub fn from_u256(amount: U256, precision: u8) -> anyhow::Result<Self> {
400 let scaled_amount = if precision < FIXED_PRECISION {
402 amount
403 .checked_mul(U256::from(10u128.pow((FIXED_PRECISION - precision) as u32)))
404 .ok_or_else(|| {
405 anyhow::anyhow!(
406 "Amount overflow during scaling to fixed precision: {} * 10^{}",
407 amount,
408 FIXED_PRECISION - precision
409 )
410 })?
411 } else {
412 amount
413 };
414
415 let raw = QuantityRaw::try_from(scaled_amount).map_err(|_| {
416 anyhow::anyhow!("U256 scaled amount {scaled_amount} exceeds QuantityRaw range")
417 })?;
418
419 Ok(Self::from_raw(raw, precision))
420 }
421}
422
423impl From<Quantity> for f64 {
424 fn from(qty: Quantity) -> Self {
425 qty.as_f64()
426 }
427}
428
429impl From<&Quantity> for f64 {
430 fn from(qty: &Quantity) -> Self {
431 qty.as_f64()
432 }
433}
434
435impl From<i32> for Quantity {
436 fn from(value: i32) -> Self {
442 assert!(
443 value >= 0,
444 "Cannot create Quantity from negative i32: {value}. Use u32 or check value is non-negative."
445 );
446 Self::new(value as f64, 0)
447 }
448}
449
450impl From<i64> for Quantity {
451 fn from(value: i64) -> Self {
457 assert!(
458 value >= 0,
459 "Cannot create Quantity from negative i64: {value}. Use u64 or check value is non-negative."
460 );
461 Self::new(value as f64, 0)
462 }
463}
464
465impl From<u32> for Quantity {
466 fn from(value: u32) -> Self {
467 Self::new(value as f64, 0)
468 }
469}
470
471impl From<u64> for Quantity {
472 fn from(value: u64) -> Self {
473 Self::new(value as f64, 0)
474 }
475}
476
477impl Hash for Quantity {
478 fn hash<H: Hasher>(&self, state: &mut H) {
479 self.raw.hash(state);
480 }
481}
482
483impl PartialEq for Quantity {
484 fn eq(&self, other: &Self) -> bool {
485 self.raw == other.raw
486 }
487}
488
489impl PartialOrd for Quantity {
490 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
491 Some(self.cmp(other))
492 }
493
494 fn lt(&self, other: &Self) -> bool {
495 self.raw.lt(&other.raw)
496 }
497
498 fn le(&self, other: &Self) -> bool {
499 self.raw.le(&other.raw)
500 }
501
502 fn gt(&self, other: &Self) -> bool {
503 self.raw.gt(&other.raw)
504 }
505
506 fn ge(&self, other: &Self) -> bool {
507 self.raw.ge(&other.raw)
508 }
509}
510
511impl Ord for Quantity {
512 fn cmp(&self, other: &Self) -> Ordering {
513 self.raw.cmp(&other.raw)
514 }
515}
516
517impl Deref for Quantity {
518 type Target = QuantityRaw;
519
520 fn deref(&self) -> &Self::Target {
521 &self.raw
522 }
523}
524
525impl Add for Quantity {
526 type Output = Self;
527 fn add(self, rhs: Self) -> Self::Output {
528 let precision = match self.precision {
529 0 => rhs.precision,
530 _ => self.precision,
531 };
532 assert!(
533 self.precision >= rhs.precision,
534 "Precision mismatch: cannot add precision {} to precision {} (precision loss)",
535 rhs.precision,
536 self.precision,
537 );
538 Self {
539 raw: self
540 .raw
541 .checked_add(rhs.raw)
542 .expect("Overflow occurred when adding `Quantity`"),
543 precision,
544 }
545 }
546}
547
548impl Sub for Quantity {
549 type Output = Self;
550 fn sub(self, rhs: Self) -> Self::Output {
551 let precision = match self.precision {
552 0 => rhs.precision,
553 _ => self.precision,
554 };
555 assert!(
556 self.precision >= rhs.precision,
557 "Precision mismatch: cannot subtract precision {} from precision {} (precision loss)",
558 rhs.precision,
559 self.precision,
560 );
561 Self {
562 raw: self
563 .raw
564 .checked_sub(rhs.raw)
565 .expect("Underflow occurred when subtracting `Quantity`"),
566 precision,
567 }
568 }
569}
570
571#[allow(
572 clippy::suspicious_arithmetic_impl,
573 reason = "Can use division to scale back"
574)]
575impl Mul for Quantity {
576 type Output = Self;
577 fn mul(self, rhs: Self) -> Self::Output {
578 let precision = match self.precision {
579 0 => rhs.precision,
580 _ => self.precision,
581 };
582 assert!(
583 self.precision >= rhs.precision,
584 "Precision mismatch: cannot multiply precision {} with precision {} (precision loss)",
585 rhs.precision,
586 self.precision,
587 );
588
589 let result_raw = self
590 .raw
591 .checked_mul(rhs.raw)
592 .expect("Overflow occurred when multiplying `Quantity`");
593
594 Self {
595 raw: result_raw / (FIXED_SCALAR as QuantityRaw),
596 precision,
597 }
598 }
599}
600
601impl Mul<f64> for Quantity {
602 type Output = f64;
603 fn mul(self, rhs: f64) -> Self::Output {
604 self.as_f64() * rhs
605 }
606}
607
608impl From<Quantity> for QuantityRaw {
609 fn from(value: Quantity) -> Self {
610 value.raw
611 }
612}
613
614impl From<&Quantity> for QuantityRaw {
615 fn from(value: &Quantity) -> Self {
616 value.raw
617 }
618}
619
620impl From<Quantity> for Decimal {
621 fn from(value: Quantity) -> Self {
622 value.as_decimal()
623 }
624}
625
626impl From<&Quantity> for Decimal {
627 fn from(value: &Quantity) -> Self {
628 value.as_decimal()
629 }
630}
631
632impl FromStr for Quantity {
633 type Err = String;
634
635 fn from_str(value: &str) -> Result<Self, Self::Err> {
636 let clean_value = value.replace('_', "");
637
638 let decimal = if clean_value.contains('e') || clean_value.contains('E') {
639 Decimal::from_scientific(&clean_value)
640 .map_err(|e| format!("Error parsing `input` string '{value}' as Decimal: {e}"))?
641 } else {
642 Decimal::from_str(&clean_value)
643 .map_err(|e| format!("Error parsing `input` string '{value}' as Decimal: {e}"))?
644 };
645
646 let precision = decimal.scale() as u8;
648
649 Self::from_decimal_dp(decimal, precision).map_err(|e| e.to_string())
650 }
651}
652
653impl From<&str> for Quantity {
655 fn from(value: &str) -> Self {
656 Self::from_str(value).expect("Valid string input for `Quantity`")
657 }
658}
659
660impl From<String> for Quantity {
661 fn from(value: String) -> Self {
662 Self::from_str(&value).expect("Valid string input for `Quantity`")
663 }
664}
665
666impl From<&String> for Quantity {
667 fn from(value: &String) -> Self {
668 Self::from_str(value).expect("Valid string input for `Quantity`")
669 }
670}
671
672impl<T: Into<QuantityRaw>> AddAssign<T> for Quantity {
673 fn add_assign(&mut self, other: T) {
674 self.raw = self
675 .raw
676 .checked_add(other.into())
677 .expect("Overflow occurred when adding `Quantity`");
678 }
679}
680
681impl<T: Into<QuantityRaw>> SubAssign<T> for Quantity {
682 fn sub_assign(&mut self, other: T) {
683 self.raw = self
684 .raw
685 .checked_sub(other.into())
686 .expect("Underflow occurred when subtracting `Quantity`");
687 }
688}
689
690impl<T: Into<QuantityRaw>> MulAssign<T> for Quantity {
691 fn mul_assign(&mut self, other: T) {
692 self.raw = self
693 .raw
694 .checked_mul(other.into())
695 .expect("Overflow occurred when multiplying `Quantity`");
696 }
697}
698
699impl Debug for Quantity {
700 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
701 if self.precision > MAX_FLOAT_PRECISION {
702 write!(f, "{}({})", stringify!(Quantity), self.raw)
703 } else {
704 write!(f, "{}({})", stringify!(Quantity), self.as_decimal())
705 }
706 }
707}
708
709impl Display for Quantity {
710 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
711 if self.precision > MAX_FLOAT_PRECISION {
712 write!(f, "{}", self.raw)
713 } else {
714 write!(f, "{}", self.as_decimal())
715 }
716 }
717}
718
719impl Serialize for Quantity {
720 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
721 where
722 S: serde::Serializer,
723 {
724 serializer.serialize_str(&self.to_string())
725 }
726}
727
728impl<'de> Deserialize<'de> for Quantity {
729 fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
730 where
731 D: Deserializer<'de>,
732 {
733 let qty_str: &str = Deserialize::deserialize(_deserializer)?;
734 let qty: Self = qty_str.into();
735 Ok(qty)
736 }
737}
738
739pub fn check_positive_quantity(value: Quantity, param: &str) -> anyhow::Result<()> {
745 if !value.is_positive() {
746 anyhow::bail!("invalid `Quantity` for '{param}' not positive, was {value}")
747 }
748 Ok(())
749}
750
751#[cfg(test)]
752mod tests {
753 use std::str::FromStr;
754
755 use nautilus_core::approx_eq;
756 use rstest::rstest;
757 use rust_decimal_macros::dec;
758
759 use super::*;
760
761 #[rstest]
762 #[should_panic(expected = "invalid `Quantity` for 'qty' not positive, was 0")]
763 fn test_check_quantity_positive() {
764 let qty = Quantity::new(0.0, 0);
765 check_positive_quantity(qty, "qty").unwrap();
766 }
767
768 #[rstest]
769 #[cfg(all(not(feature = "defi"), not(feature = "high-precision")))]
770 #[should_panic(expected = "`precision` exceeded maximum `FIXED_PRECISION` (9), was 17")]
771 fn test_invalid_precision_new() {
772 let _ = Quantity::new(1.0, 17);
774 }
775
776 #[rstest]
777 #[cfg(all(not(feature = "defi"), feature = "high-precision"))]
778 #[should_panic(expected = "`precision` exceeded maximum `FIXED_PRECISION` (16), was 17")]
779 fn test_invalid_precision_new() {
780 let _ = Quantity::new(1.0, 17);
782 }
783
784 #[rstest]
785 #[cfg(not(feature = "defi"))]
786 #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
787 fn test_invalid_precision_from_raw() {
788 let _ = Quantity::from_raw(1, FIXED_PRECISION + 1);
790 }
791
792 #[rstest]
793 #[cfg(not(feature = "defi"))]
794 #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
795 fn test_invalid_precision_zero() {
796 let _ = Quantity::zero(FIXED_PRECISION + 1);
798 }
799
800 #[rstest]
801 #[should_panic(
802 expected = "Precision mismatch: cannot add precision 2 to precision 1 (precision loss)"
803 )]
804 fn test_precision_mismatch_add() {
805 let q1 = Quantity::new(1.0, 1);
806 let q2 = Quantity::new(1.0, 2);
807 let _ = q1 + q2;
808 }
809
810 #[rstest]
811 #[should_panic(
812 expected = "Precision mismatch: cannot subtract precision 2 from precision 1 (precision loss)"
813 )]
814 fn test_precision_mismatch_sub() {
815 let q1 = Quantity::new(1.0, 1);
816 let q2 = Quantity::new(1.0, 2);
817 let _ = q1 - q2;
818 }
819
820 #[rstest]
821 #[should_panic(
822 expected = "Precision mismatch: cannot multiply precision 2 with precision 1 (precision loss)"
823 )]
824 fn test_precision_mismatch_mul() {
825 let q1 = Quantity::new(2.0, 1);
826 let q2 = Quantity::new(3.0, 2);
827 let _ = q1 * q2;
828 }
829
830 #[rstest]
831 fn test_new_non_zero_ok() {
832 let qty = Quantity::non_zero_checked(123.456, 3).unwrap();
833 assert_eq!(qty.raw, Quantity::new(123.456, 3).raw);
834 assert!(qty.is_positive());
835 }
836
837 #[rstest]
838 fn test_new_non_zero_zero_input() {
839 assert!(Quantity::non_zero_checked(0.0, 0).is_err());
840 }
841
842 #[rstest]
843 fn test_new_non_zero_rounds_to_zero() {
844 assert!(Quantity::non_zero_checked(0.0004, 3).is_err());
846 }
847
848 #[rstest]
849 fn test_new_non_zero_negative() {
850 assert!(Quantity::non_zero_checked(-1.0, 0).is_err());
851 }
852
853 #[rstest]
854 fn test_new_non_zero_exceeds_max() {
855 assert!(Quantity::non_zero_checked(QUANTITY_MAX * 10.0, 0).is_err());
856 }
857
858 #[rstest]
859 fn test_new_non_zero_invalid_precision() {
860 assert!(Quantity::non_zero_checked(1.0, FIXED_PRECISION + 1).is_err());
861 }
862
863 #[rstest]
864 fn test_new() {
865 let value = 0.00812;
866 let qty = Quantity::new(value, 8);
867 assert_eq!(qty, qty);
868 assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
869 assert_eq!(qty.precision, 8);
870 assert_eq!(qty, Quantity::from("0.00812000"));
871 assert_eq!(qty.as_decimal(), dec!(0.00812000));
872 assert_eq!(qty.to_string(), "0.00812000");
873 assert!(!qty.is_zero());
874 assert!(qty.is_positive());
875 assert!(approx_eq!(f64, qty.as_f64(), 0.00812, epsilon = 0.000_001));
876 }
877
878 #[rstest]
879 fn test_check_quantity_positive_ok() {
880 let qty = Quantity::new(10.0, 0);
881 check_positive_quantity(qty, "qty").unwrap();
882 }
883
884 #[rstest]
885 fn test_negative_quantity_validation() {
886 assert!(Quantity::new_checked(-1.0, FIXED_PRECISION).is_err());
887 }
888
889 #[rstest]
890 fn test_undefined() {
891 let qty = Quantity::from_raw(QUANTITY_UNDEF, 0);
892 assert_eq!(qty.raw, QUANTITY_UNDEF);
893 assert!(qty.is_undefined());
894 }
895
896 #[rstest]
897 fn test_zero() {
898 let qty = Quantity::zero(8);
899 assert_eq!(qty.raw, 0);
900 assert_eq!(qty.precision, 8);
901 assert!(qty.is_zero());
902 assert!(!qty.is_positive());
903 }
904
905 #[rstest]
906 fn test_from_i32() {
907 let value = 100_000i32;
908 let qty = Quantity::from(value);
909 assert_eq!(qty, qty);
910 assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
911 assert_eq!(qty.precision, 0);
912 }
913
914 #[rstest]
915 fn test_from_u32() {
916 let value: u32 = 5000;
917 let qty = Quantity::from(value);
918 assert_eq!(qty.raw, Quantity::from(format!("{value}")).raw);
919 assert_eq!(qty.precision, 0);
920 }
921
922 #[rstest]
923 fn test_from_i64() {
924 let value = 100_000i64;
925 let qty = Quantity::from(value);
926 assert_eq!(qty, qty);
927 assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
928 assert_eq!(qty.precision, 0);
929 }
930
931 #[rstest]
932 fn test_from_u64() {
933 let value = 100_000u64;
934 let qty = Quantity::from(value);
935 assert_eq!(qty, qty);
936 assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
937 assert_eq!(qty.precision, 0);
938 }
939
940 #[rstest] fn test_with_maximum_value() {
942 let qty = Quantity::new_checked(QUANTITY_MAX, 0);
943 assert!(qty.is_ok());
944 }
945
946 #[rstest]
947 fn test_with_minimum_positive_value() {
948 let value = 0.000_000_001;
949 let qty = Quantity::new(value, 9);
950 assert_eq!(qty.raw, Quantity::from("0.000000001").raw);
951 assert_eq!(qty.as_decimal(), dec!(0.000000001));
952 assert_eq!(qty.to_string(), "0.000000001");
953 }
954
955 #[rstest]
956 fn test_with_minimum_value() {
957 let qty = Quantity::new(QUANTITY_MIN, 9);
958 assert_eq!(qty.raw, 0);
959 assert_eq!(qty.as_decimal(), dec!(0));
960 assert_eq!(qty.to_string(), "0.000000000");
961 }
962
963 #[rstest]
964 fn test_is_zero() {
965 let qty = Quantity::zero(8);
966 assert_eq!(qty, qty);
967 assert_eq!(qty.raw, 0);
968 assert_eq!(qty.precision, 8);
969 assert_eq!(qty, Quantity::from("0.00000000"));
970 assert_eq!(qty.as_decimal(), dec!(0));
971 assert_eq!(qty.to_string(), "0.00000000");
972 assert!(qty.is_zero());
973 }
974
975 #[rstest]
976 fn test_precision() {
977 let value = 1.001;
978 let qty = Quantity::new(value, 2);
979 assert_eq!(qty.to_string(), "1.00");
980 }
981
982 #[rstest]
983 fn test_new_from_str() {
984 let qty = Quantity::new(0.00812000, 8);
985 assert_eq!(qty, qty);
986 assert_eq!(qty.precision, 8);
987 assert_eq!(qty, Quantity::from("0.00812000"));
988 assert_eq!(qty.to_string(), "0.00812000");
989 }
990
991 #[rstest]
992 #[case("0", 0)]
993 #[case("1.1", 1)]
994 #[case("1.123456789", 9)]
995 fn test_from_str_valid_input(#[case] input: &str, #[case] expected_prec: u8) {
996 let qty = Quantity::from(input);
997 assert_eq!(qty.precision, expected_prec);
998 assert_eq!(qty.as_decimal(), Decimal::from_str(input).unwrap());
999 }
1000
1001 #[rstest]
1002 #[should_panic]
1003 fn test_from_str_invalid_input() {
1004 let input = "invalid";
1005 Quantity::new(f64::from_str(input).unwrap(), 8);
1006 }
1007
1008 #[rstest]
1009 fn test_from_str_errors() {
1010 assert!(Quantity::from_str("invalid").is_err());
1011 assert!(Quantity::from_str("12.34.56").is_err());
1012 assert!(Quantity::from_str("").is_err());
1013 assert!(Quantity::from_str("-1").is_err()); assert!(Quantity::from_str("-0.001").is_err());
1015 }
1016
1017 #[rstest]
1018 #[case("1e7", 0, 10_000_000.0)]
1019 #[case("2.5e3", 0, 2_500.0)]
1020 #[case("1.234e-2", 5, 0.01234)]
1021 #[case("5E-3", 3, 0.005)]
1022 #[case("1.0e6", 0, 1_000_000.0)]
1023 fn test_from_str_scientific_notation(
1024 #[case] input: &str,
1025 #[case] expected_precision: u8,
1026 #[case] expected_value: f64,
1027 ) {
1028 let qty = Quantity::from_str(input).unwrap();
1029 assert_eq!(qty.precision, expected_precision);
1030 assert!(approx_eq!(
1031 f64,
1032 qty.as_f64(),
1033 expected_value,
1034 epsilon = 1e-10
1035 ));
1036 }
1037
1038 #[rstest]
1039 #[case("1_234.56", 2, 1234.56)]
1040 #[case("1_000_000", 0, 1_000_000.0)]
1041 #[case("99_999.999_99", 5, 99_999.999_99)]
1042 fn test_from_str_with_underscores(
1043 #[case] input: &str,
1044 #[case] expected_precision: u8,
1045 #[case] expected_value: f64,
1046 ) {
1047 let qty = Quantity::from_str(input).unwrap();
1048 assert_eq!(qty.precision, expected_precision);
1049 assert!(approx_eq!(
1050 f64,
1051 qty.as_f64(),
1052 expected_value,
1053 epsilon = 1e-10
1054 ));
1055 }
1056
1057 #[rstest]
1058 fn test_from_decimal_dp_preservation() {
1059 let decimal = dec!(123.456789);
1061 let qty = Quantity::from_decimal_dp(decimal, 6).unwrap();
1062 assert_eq!(qty.precision, 6);
1063 assert!(approx_eq!(f64, qty.as_f64(), 123.456789, epsilon = 1e-10));
1064
1065 let expected_raw = 123456789_u64 * 10_u64.pow((FIXED_PRECISION - 6) as u32);
1067 assert_eq!(qty.raw, expected_raw as QuantityRaw);
1068 }
1069
1070 #[rstest]
1071 fn test_from_decimal_dp_rounding() {
1072 let decimal = dec!(1.005);
1074 let qty = Quantity::from_decimal_dp(decimal, 2).unwrap();
1075 assert_eq!(qty.as_f64(), 1.0); let decimal = dec!(1.015);
1078 let qty = Quantity::from_decimal_dp(decimal, 2).unwrap();
1079 assert_eq!(qty.as_f64(), 1.02); }
1081
1082 #[rstest]
1083 fn test_from_decimal_infers_precision() {
1084 let decimal = dec!(123.456);
1086 let qty = Quantity::from_decimal(decimal).unwrap();
1087 assert_eq!(qty.precision, 3);
1088 assert!(approx_eq!(f64, qty.as_f64(), 123.456, epsilon = 1e-10));
1089
1090 let decimal = dec!(100);
1092 let qty = Quantity::from_decimal(decimal).unwrap();
1093 assert_eq!(qty.precision, 0);
1094 assert_eq!(qty.as_f64(), 100.0);
1095
1096 let decimal = dec!(1.23456789);
1098 let qty = Quantity::from_decimal(decimal).unwrap();
1099 assert_eq!(qty.precision, 8);
1100 assert!(approx_eq!(f64, qty.as_f64(), 1.23456789, epsilon = 1e-10));
1101 }
1102
1103 #[rstest]
1104 fn test_from_decimal_trailing_zeros() {
1105 let decimal = dec!(5.670);
1107 assert_eq!(decimal.scale(), 3); let qty = Quantity::from_decimal(decimal).unwrap();
1111 assert_eq!(qty.precision, 3);
1112 assert!(approx_eq!(f64, qty.as_f64(), 5.67, epsilon = 1e-10));
1113
1114 let normalized = decimal.normalize();
1116 assert_eq!(normalized.scale(), 2);
1117 let qty_normalized = Quantity::from_decimal(normalized).unwrap();
1118 assert_eq!(qty_normalized.precision, 2);
1119 }
1120
1121 #[rstest]
1122 #[case("1.00", 2)]
1123 #[case("1.0", 1)]
1124 #[case("1.000", 3)]
1125 #[case("100.00", 2)]
1126 #[case("0.10", 2)]
1127 #[case("0.100", 3)]
1128 fn test_from_str_preserves_trailing_zeros(#[case] input: &str, #[case] expected_precision: u8) {
1129 let qty = Quantity::from_str(input).unwrap();
1130 assert_eq!(qty.precision, expected_precision);
1131 }
1132
1133 #[rstest]
1134 fn test_from_decimal_excessive_precision_inference() {
1135 let decimal = dec!(1.1234567890123456789012345678);
1138
1139 if decimal.scale() > FIXED_PRECISION as u32 {
1141 assert!(Quantity::from_decimal(decimal).is_err());
1142 }
1143 }
1144
1145 #[rstest]
1146 fn test_from_decimal_negative_quantity_errors() {
1147 let decimal = dec!(-123.45);
1149 let result = Quantity::from_decimal(decimal);
1150 assert!(result.is_err());
1151
1152 let result = Quantity::from_decimal_dp(decimal, 2);
1154 assert!(result.is_err());
1155 }
1156
1157 #[rstest]
1158 fn test_add() {
1159 let a = 1.0;
1160 let b = 2.0;
1161 let quantity1 = Quantity::new(1.0, 0);
1162 let quantity2 = Quantity::new(2.0, 0);
1163 let quantity3 = quantity1 + quantity2;
1164 assert_eq!(quantity3.raw, Quantity::new(a + b, 0).raw);
1165 }
1166
1167 #[rstest]
1168 fn test_sub() {
1169 let a = 3.0;
1170 let b = 2.0;
1171 let quantity1 = Quantity::new(a, 0);
1172 let quantity2 = Quantity::new(b, 0);
1173 let quantity3 = quantity1 - quantity2;
1174 assert_eq!(quantity3.raw, Quantity::new(a - b, 0).raw);
1175 }
1176
1177 #[rstest]
1178 fn test_add_assign() {
1179 let a = 1.0;
1180 let b = 2.0;
1181 let mut quantity1 = Quantity::new(a, 0);
1182 let quantity2 = Quantity::new(b, 0);
1183 quantity1 += quantity2;
1184 assert_eq!(quantity1.raw, Quantity::new(a + b, 0).raw);
1185 }
1186
1187 #[rstest]
1188 fn test_sub_assign() {
1189 let a = 3.0;
1190 let b = 2.0;
1191 let mut quantity1 = Quantity::new(a, 0);
1192 let quantity2 = Quantity::new(b, 0);
1193 quantity1 -= quantity2;
1194 assert_eq!(quantity1.raw, Quantity::new(a - b, 0).raw);
1195 }
1196
1197 #[rstest]
1198 fn test_mul() {
1199 let value = 2.0;
1200 let quantity1 = Quantity::new(value, 1);
1201 let quantity2 = Quantity::new(value, 1);
1202 let quantity3 = quantity1 * quantity2;
1203 assert_eq!(quantity3.raw, Quantity::new(value * value, 0).raw);
1204 }
1205
1206 #[rstest]
1207 fn test_mul_assign() {
1208 let mut quantity = Quantity::new(2.0, 0);
1209 quantity *= 3u64; assert_eq!(quantity.raw, Quantity::new(6.0, 0).raw);
1211
1212 let mut fraction = Quantity::new(1.5, 2);
1213 fraction *= 2u64; assert_eq!(fraction.raw, Quantity::new(3.0, 2).raw);
1215 }
1216
1217 #[rstest]
1218 fn test_comparisons() {
1219 assert_eq!(Quantity::new(1.0, 1), Quantity::new(1.0, 1));
1220 assert_eq!(Quantity::new(1.0, 1), Quantity::new(1.0, 2));
1221 assert_ne!(Quantity::new(1.1, 1), Quantity::new(1.0, 1));
1222 assert!(Quantity::new(1.0, 1) <= Quantity::new(1.0, 2));
1223 assert!(Quantity::new(1.1, 1) > Quantity::new(1.0, 1));
1224 assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 1));
1225 assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 2));
1226 assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 2));
1227 assert!(Quantity::new(0.9, 1) < Quantity::new(1.0, 1));
1228 assert!(Quantity::new(0.9, 1) <= Quantity::new(1.0, 2));
1229 assert!(Quantity::new(0.9, 1) <= Quantity::new(1.0, 1));
1230 }
1231
1232 #[rstest]
1233 fn test_debug() {
1234 let quantity = Quantity::from_str("44.12").unwrap();
1235 let result = format!("{quantity:?}");
1236 assert_eq!(result, "Quantity(44.12)");
1237 }
1238
1239 #[rstest]
1240 fn test_display() {
1241 let quantity = Quantity::from_str("44.12").unwrap();
1242 let result = format!("{quantity}");
1243 assert_eq!(result, "44.12");
1244 }
1245
1246 #[rstest]
1247 #[case(44.12, 2, "Quantity(44.12)", "44.12")] #[case(1234.567, 8, "Quantity(1234.56700000)", "1234.56700000")] #[cfg_attr(
1250 feature = "defi",
1251 case(
1252 1_000_000_000_000_000_000.0,
1253 18,
1254 "Quantity(1000000000000000000)",
1255 "1000000000000000000"
1256 )
1257 )] fn test_debug_display_precision_handling(
1259 #[case] value: f64,
1260 #[case] precision: u8,
1261 #[case] expected_debug: &str,
1262 #[case] expected_display: &str,
1263 ) {
1264 let quantity = if precision > MAX_FLOAT_PRECISION {
1265 Quantity::from_raw(value as QuantityRaw, precision)
1267 } else {
1268 Quantity::new(value, precision)
1269 };
1270
1271 assert_eq!(format!("{quantity:?}"), expected_debug);
1272 assert_eq!(format!("{quantity}"), expected_display);
1273 }
1274
1275 #[rstest]
1276 fn test_to_formatted_string() {
1277 let qty = Quantity::new(1234.5678, 4);
1278 let formatted = qty.to_formatted_string();
1279 assert_eq!(formatted, "1_234.5678");
1280 assert_eq!(qty.to_string(), "1234.5678");
1281 }
1282
1283 #[rstest]
1284 fn test_saturating_sub() {
1285 let q1 = Quantity::new(100.0, 2);
1286 let q2 = Quantity::new(50.0, 2);
1287 let q3 = Quantity::new(150.0, 2);
1288
1289 let result = q1.saturating_sub(q2);
1290 assert_eq!(result, Quantity::new(50.0, 2));
1291
1292 let result = q1.saturating_sub(q3);
1293 assert_eq!(result, Quantity::zero(2));
1294 assert_eq!(result.raw, 0);
1295 }
1296
1297 #[rstest]
1298 fn test_saturating_sub_overflow_bug() {
1299 use crate::types::fixed::FIXED_PRECISION;
1302 let precision = 3;
1303 let scale = 10u64.pow(u32::from(FIXED_PRECISION - precision)) as QuantityRaw;
1304
1305 let peak_qty = Quantity::from_raw(79 * scale, precision);
1307 let order_qty = Quantity::from_raw(80 * scale, precision);
1308
1309 let result = peak_qty.saturating_sub(order_qty);
1311 assert_eq!(result.raw, 0);
1312 assert_eq!(result, Quantity::zero(precision));
1313 }
1314
1315 #[rstest]
1316 fn test_hash() {
1317 use std::{
1318 collections::hash_map::DefaultHasher,
1319 hash::{Hash, Hasher},
1320 };
1321
1322 let q1 = Quantity::new(100.0, 1);
1323 let q2 = Quantity::new(100.0, 1);
1324 let q3 = Quantity::new(200.0, 1);
1325
1326 let mut s1 = DefaultHasher::new();
1327 let mut s2 = DefaultHasher::new();
1328 let mut s3 = DefaultHasher::new();
1329
1330 q1.hash(&mut s1);
1331 q2.hash(&mut s2);
1332 q3.hash(&mut s3);
1333
1334 assert_eq!(
1335 s1.finish(),
1336 s2.finish(),
1337 "Equal quantities must hash equally"
1338 );
1339 assert_ne!(
1340 s1.finish(),
1341 s3.finish(),
1342 "Different quantities must hash differently"
1343 );
1344 }
1345
1346 #[rstest]
1347 fn test_quantity_serde_json_round_trip() {
1348 let original = Quantity::new(123.456, 3);
1349 let json_str = serde_json::to_string(&original).unwrap();
1350 assert_eq!(json_str, "\"123.456\"");
1351
1352 let deserialized: Quantity = serde_json::from_str(&json_str).unwrap();
1353 assert_eq!(deserialized, original);
1354 assert_eq!(deserialized.precision, 3);
1355 }
1356
1357 #[rstest]
1361 #[cfg(feature = "defi")]
1362 #[case::sell_tx_rain_amount(
1363 U256::from_str_radix("42193532365637161405123", 10).unwrap(),
1364 18,
1365 "42193.532365637161405123"
1366 )]
1367 #[case::sell_tx_weth_amount(
1368 U256::from_str_radix("112633187203033110", 10).unwrap(),
1369 18,
1370 "0.112633187203033110"
1371 )]
1372 fn test_from_u256_real_swap_data(
1373 #[case] amount: U256,
1374 #[case] precision: u8,
1375 #[case] expected_str: &str,
1376 ) {
1377 let qty = Quantity::from_u256(amount, precision).unwrap();
1378 assert_eq!(qty.precision, precision);
1379 assert_eq!(qty.as_decimal().to_string(), expected_str);
1380 }
1381}
1382
1383#[cfg(test)]
1387mod property_tests {
1388 use proptest::prelude::*;
1389 use rstest::rstest;
1390
1391 use super::*;
1392
1393 fn quantity_value_strategy() -> impl Strategy<Value = f64> {
1395 prop_oneof![
1397 0.00001..1.0,
1399 1.0..100_000.0,
1401 100_000.0..1_000_000.0,
1403 Just(0.0),
1405 Just(QUANTITY_MAX / 2.0),
1407 ]
1408 }
1409
1410 fn precision_strategy() -> impl Strategy<Value = u8> {
1412 let upper = FIXED_PRECISION.min(MAX_FLOAT_PRECISION);
1413 prop_oneof![Just(0u8), 0u8..=upper, Just(FIXED_PRECISION),]
1414 }
1415
1416 fn precision_strategy_non_zero() -> impl Strategy<Value = u8> {
1417 let upper = FIXED_PRECISION.clamp(1, MAX_FLOAT_PRECISION);
1418 prop_oneof![Just(upper), Just(FIXED_PRECISION.max(1)), 1u8..=upper,]
1419 }
1420
1421 fn raw_for_precision_strategy() -> impl Strategy<Value = (QuantityRaw, u8)> {
1422 precision_strategy().prop_flat_map(|precision| {
1423 let step_u128 = 10u128.pow(u32::from(FIXED_PRECISION.saturating_sub(precision)));
1424 #[cfg(feature = "high-precision")]
1425 let max_steps_u128 = QUANTITY_RAW_MAX / step_u128;
1426 #[cfg(not(feature = "high-precision"))]
1427 let max_steps_u128 = (QUANTITY_RAW_MAX as u128) / step_u128;
1428
1429 (0u128..=max_steps_u128).prop_map(move |steps_u128| {
1430 let raw_u128 = steps_u128 * step_u128;
1431 #[cfg(feature = "high-precision")]
1432 let raw = raw_u128;
1433 #[cfg(not(feature = "high-precision"))]
1434 let raw = raw_u128
1435 .try_into()
1436 .expect("raw value should fit in QuantityRaw");
1437 (raw, precision)
1438 })
1439 })
1440 }
1441
1442 const DECIMAL_MAX_MANTISSA: u128 = 79_228_162_514_264_337_593_543_950_335;
1443
1444 fn decimal_compatible(raw: QuantityRaw, precision: u8) -> bool {
1445 if precision > MAX_FLOAT_PRECISION {
1446 return false;
1447 }
1448 let precision_diff = u32::from(FIXED_PRECISION.saturating_sub(precision));
1449 let divisor = 10u128.pow(precision_diff);
1450 #[cfg(feature = "high-precision")]
1451 let rescaled_raw = raw / divisor;
1452 #[cfg(not(feature = "high-precision"))]
1453 let rescaled_raw = (raw as u128) / divisor;
1454 rescaled_raw <= DECIMAL_MAX_MANTISSA
1457 }
1458
1459 proptest! {
1460 #[rstest]
1462 fn prop_quantity_serde_round_trip(
1463 (raw, precision) in raw_for_precision_strategy()
1464 ) {
1465 prop_assume!(decimal_compatible(raw, precision));
1467
1468 let original = Quantity::from_raw(raw, precision);
1469
1470 let string_repr = original.to_string();
1472 let from_string: Quantity = string_repr.parse().unwrap();
1473 prop_assert_eq!(from_string.raw, original.raw);
1474 prop_assert_eq!(from_string.precision, original.precision);
1475
1476 let json = serde_json::to_string(&original).unwrap();
1478 let from_json: Quantity = serde_json::from_str(&json).unwrap();
1479 prop_assert_eq!(from_json.precision, original.precision);
1480 prop_assert_eq!(from_json.raw, original.raw);
1481 }
1482
1483 #[rstest]
1485 fn prop_quantity_arithmetic_associative(
1486 a in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
1487 b in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
1488 c in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
1489 precision in precision_strategy()
1490 ) {
1491 let q_a = Quantity::new(a, precision);
1492 let q_b = Quantity::new(b, precision);
1493 let q_c = Quantity::new(c, precision);
1494
1495 let ab_raw = q_a.raw.checked_add(q_b.raw);
1497 let bc_raw = q_b.raw.checked_add(q_c.raw);
1498
1499 if let (Some(ab_raw), Some(bc_raw)) = (ab_raw, bc_raw) {
1500 let ab_c_raw = ab_raw.checked_add(q_c.raw);
1501 let a_bc_raw = q_a.raw.checked_add(bc_raw);
1502
1503 if let (Some(ab_c_raw), Some(a_bc_raw)) = (ab_c_raw, a_bc_raw) {
1504 prop_assert_eq!(ab_c_raw, a_bc_raw, "Associativity failed in raw arithmetic");
1506 }
1507 }
1508 }
1509
1510 #[rstest]
1512 fn prop_quantity_addition_subtraction_inverse(
1513 base in quantity_value_strategy().prop_filter("Reasonable values", |&x| x < 1e6),
1514 delta in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
1515 precision in precision_strategy()
1516 ) {
1517 let q_base = Quantity::new(base, precision);
1518 let q_delta = Quantity::new(delta, precision);
1519
1520 if let Some(added_raw) = q_base.raw.checked_add(q_delta.raw)
1522 && let Some(result_raw) = added_raw.checked_sub(q_delta.raw) {
1523 prop_assert_eq!(result_raw, q_base.raw, "Inverse operation failed in raw arithmetic");
1525 }
1526 }
1527
1528 #[rstest]
1530 fn prop_quantity_ordering_transitive(
1531 a in quantity_value_strategy(),
1532 b in quantity_value_strategy(),
1533 c in quantity_value_strategy(),
1534 precision in precision_strategy()
1535 ) {
1536 let q_a = Quantity::new(a, precision);
1537 let q_b = Quantity::new(b, precision);
1538 let q_c = Quantity::new(c, precision);
1539
1540 if q_a <= q_b && q_b <= q_c {
1542 prop_assert!(q_a <= q_c, "Transitivity failed: {} <= {} <= {} but {} > {}",
1543 q_a.as_f64(), q_b.as_f64(), q_c.as_f64(), q_a.as_f64(), q_c.as_f64());
1544 }
1545 }
1546
1547 #[rstest]
1549 fn prop_quantity_string_parsing_precision(
1550 integral in 0u32..1000000,
1551 fractional in 0u32..1000000,
1552 precision in precision_strategy_non_zero()
1553 ) {
1554 let pow = 10u128.pow(u32::from(precision));
1556 let fractional_mod = (fractional as u128) % pow;
1557 let fractional_str = format!("{:0width$}", fractional_mod, width = precision as usize);
1558 let quantity_str = format!("{integral}.{fractional_str}");
1559
1560 let parsed: Quantity = quantity_str.parse().unwrap();
1561 prop_assert_eq!(parsed.precision, precision);
1562
1563 let round_trip = parsed.to_string();
1565 let expected_value = format!("{integral}.{fractional_str}");
1566 prop_assert_eq!(round_trip, expected_value);
1567 }
1568
1569 #[rstest]
1571 fn prop_quantity_precision_information_preservation(
1572 value in quantity_value_strategy().prop_filter("Reasonable values", |&x| x < 1e6),
1573 precision1 in precision_strategy_non_zero(),
1574 precision2 in precision_strategy_non_zero()
1575 ) {
1576 prop_assume!(precision1 != precision2);
1578
1579 let _q1 = Quantity::new(value, precision1);
1580 let _q2 = Quantity::new(value, precision2);
1581
1582 let min_precision = precision1.min(precision2);
1585
1586 let scale = 10.0_f64.powi(min_precision as i32);
1588 let rounded_value = (value * scale).round() / scale;
1589
1590 let q1_reduced = Quantity::new(rounded_value, min_precision);
1591 let q2_reduced = Quantity::new(rounded_value, min_precision);
1592
1593 prop_assert_eq!(q1_reduced.raw, q2_reduced.raw, "Precision reduction inconsistent");
1595 }
1596
1597 #[rstest]
1599 fn prop_quantity_arithmetic_bounds(
1600 a in quantity_value_strategy(),
1601 b in quantity_value_strategy(),
1602 precision in precision_strategy()
1603 ) {
1604 let q_a = Quantity::new(a, precision);
1605 let q_b = Quantity::new(b, precision);
1606
1607 let sum_f64 = q_a.as_f64() + q_b.as_f64();
1609 if sum_f64.is_finite() && (QUANTITY_MIN..=QUANTITY_MAX).contains(&sum_f64) {
1610 let sum = q_a + q_b;
1611 prop_assert!(sum.as_f64().is_finite());
1612 prop_assert!(!sum.is_undefined());
1613 }
1614
1615 let diff_f64 = q_a.as_f64() - q_b.as_f64();
1617 if diff_f64.is_finite() && (QUANTITY_MIN..=QUANTITY_MAX).contains(&diff_f64) {
1618 let diff = q_a - q_b;
1619 prop_assert!(diff.as_f64().is_finite());
1620 prop_assert!(!diff.is_undefined());
1621 }
1622 }
1623
1624 #[rstest]
1626 fn prop_quantity_multiplication_non_negative(
1627 a in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 0.0 && x < 10.0),
1628 b in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 0.0 && x < 10.0),
1629 precision in precision_strategy()
1630 ) {
1631 let q_a = Quantity::new(a, precision);
1632 let q_b = Quantity::new(b, precision);
1633
1634 let raw_product_check = q_a.raw.checked_mul(q_b.raw);
1636
1637 if let Some(raw_product) = raw_product_check {
1638 let scaled_raw = raw_product / (FIXED_SCALAR as QuantityRaw);
1640 if scaled_raw <= QUANTITY_RAW_MAX {
1641 let product = q_a * q_b;
1643 prop_assert!(product.as_f64() >= 0.0, "Quantity multiplication produced negative value: {}", product.as_f64());
1644 }
1645 }
1646 }
1647
1648 #[rstest]
1650 fn prop_quantity_zero_addition_identity(
1651 value in quantity_value_strategy(),
1652 precision in precision_strategy()
1653 ) {
1654 let q = Quantity::new(value, precision);
1655 let zero = Quantity::zero(precision);
1656
1657 prop_assert_eq!(q + zero, q);
1659 prop_assert_eq!(zero + q, q);
1660 }
1661 }
1662
1663 proptest! {
1664 #[rstest]
1666 fn prop_quantity_from_raw_round_trip(
1667 (raw, precision) in raw_for_precision_strategy()
1668 ) {
1669 let quantity = Quantity::from_raw(raw, precision);
1670 prop_assert_eq!(quantity.raw, raw);
1671 prop_assert_eq!(quantity.precision, precision);
1672 }
1673 }
1674}