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
26use nautilus_core::{
27 correctness::{FAILED, check_in_range_inclusive_f64, check_predicate_true},
28 parsing::precision_from_str,
29};
30use rust_decimal::Decimal;
31use serde::{Deserialize, Deserializer, Serialize};
32use thousands::Separable;
33
34use super::fixed::{FIXED_PRECISION, FIXED_SCALAR, check_fixed_precision};
35#[cfg(not(feature = "high-precision"))]
36use super::fixed::{f64_to_fixed_u64, fixed_u64_to_f64};
37#[cfg(feature = "high-precision")]
38use super::fixed::{f64_to_fixed_u128, fixed_u128_to_f64};
39#[cfg(feature = "defi")]
40use crate::types::fixed::MAX_FLOAT_PRECISION;
41
42#[cfg(feature = "high-precision")]
47pub type QuantityRaw = u128;
48
49#[cfg(not(feature = "high-precision"))]
50pub type QuantityRaw = u64;
51
52#[unsafe(no_mangle)]
56#[allow(unsafe_code)]
57pub static QUANTITY_RAW_MAX: QuantityRaw = (QUANTITY_MAX * FIXED_SCALAR) as QuantityRaw;
58
59pub const QUANTITY_UNDEF: QuantityRaw = QuantityRaw::MAX;
61
62#[cfg(feature = "high-precision")]
67pub const QUANTITY_MAX: f64 = 34_028_236_692_093.0;
69
70#[cfg(not(feature = "high-precision"))]
71pub const QUANTITY_MAX: f64 = 18_446_744_073.0;
73
74pub const QUANTITY_MIN: f64 = 0.0;
78
79#[repr(C)]
90#[derive(Clone, Copy, Default, Eq)]
91#[cfg_attr(
92 feature = "python",
93 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
94)]
95pub struct Quantity {
96 pub raw: QuantityRaw,
98 pub precision: u8,
100}
101
102impl Quantity {
103 pub fn new_checked(value: f64, precision: u8) -> anyhow::Result<Self> {
115 check_in_range_inclusive_f64(value, QUANTITY_MIN, QUANTITY_MAX, "value")?;
116
117 #[cfg(feature = "defi")]
118 if precision > MAX_FLOAT_PRECISION {
119 anyhow::bail!(
121 "`precision` exceeded maximum float precision ({MAX_FLOAT_PRECISION}), use `Quantity::from_wei()` for WEI values instead"
122 );
123 }
124
125 check_fixed_precision(precision)?;
126
127 #[cfg(feature = "high-precision")]
128 let raw = f64_to_fixed_u128(value, precision);
129 #[cfg(not(feature = "high-precision"))]
130 let raw = f64_to_fixed_u64(value, precision);
131
132 Ok(Self { raw, precision })
133 }
134
135 pub fn non_zero_checked(value: f64, precision: u8) -> anyhow::Result<Self> {
149 check_predicate_true(value != 0.0, "value was zero")?;
150 check_fixed_precision(precision)?;
151 let rounded_value =
152 (value * 10.0_f64.powi(precision as i32)).round() / 10.0_f64.powi(precision as i32);
153 check_predicate_true(
154 rounded_value != 0.0,
155 &format!("value {value} was zero after rounding to precision {precision}"),
156 )?;
157
158 Self::new_checked(value, precision)
159 }
160
161 pub fn new(value: f64, precision: u8) -> Self {
167 Self::new_checked(value, precision).expect(FAILED)
168 }
169
170 pub fn non_zero(value: f64, precision: u8) -> Self {
176 Self::non_zero_checked(value, precision).expect(FAILED)
177 }
178
179 pub fn from_raw(raw: QuantityRaw, precision: u8) -> Self {
185 if raw == QUANTITY_UNDEF {
186 check_predicate_true(
187 precision == 0,
188 "`precision` must be 0 when `raw` is QUANTITY_UNDEF",
189 )
190 .expect(FAILED);
191 }
192 check_predicate_true(
193 raw == QUANTITY_UNDEF || raw <= QUANTITY_RAW_MAX,
194 &format!("raw outside valid range, was {raw}"),
195 )
196 .expect(FAILED);
197 check_fixed_precision(precision).expect(FAILED);
198 Self { raw, precision }
199 }
200
201 #[must_use]
207 pub fn zero(precision: u8) -> Self {
208 check_fixed_precision(precision).expect(FAILED);
209 Self::new(0.0, precision)
210 }
211
212 #[must_use]
214 pub fn is_undefined(&self) -> bool {
215 self.raw == QUANTITY_UNDEF
216 }
217
218 #[must_use]
220 pub fn is_zero(&self) -> bool {
221 self.raw == 0
222 }
223
224 #[must_use]
226 pub fn is_positive(&self) -> bool {
227 self.raw != QUANTITY_UNDEF && self.raw > 0
228 }
229
230 #[cfg(feature = "high-precision")]
231 #[must_use]
237 pub fn as_f64(&self) -> f64 {
238 #[cfg(feature = "defi")]
239 if self.precision > MAX_FLOAT_PRECISION {
240 panic!("Invalid f64 conversion beyond `MAX_FLOAT_PRECISION` (16)");
241 }
242
243 fixed_u128_to_f64(self.raw)
244 }
245
246 #[cfg(not(feature = "high-precision"))]
247 #[must_use]
253 pub fn as_f64(&self) -> f64 {
254 #[cfg(feature = "defi")]
255 if self.precision > MAX_FLOAT_PRECISION {
256 panic!("Invalid f64 conversion beyond `MAX_FLOAT_PRECISION` (16)");
257 }
258
259 fixed_u64_to_f64(self.raw)
260 }
261
262 #[must_use]
264 pub fn as_decimal(&self) -> Decimal {
265 let precision_diff = FIXED_PRECISION.saturating_sub(self.precision);
267 let rescaled_raw = self.raw / QuantityRaw::pow(10, u32::from(precision_diff));
268
269 #[allow(clippy::useless_conversion)] Decimal::from_i128_with_scale(rescaled_raw as i128, u32::from(self.precision))
274 }
275
276 #[must_use]
278 pub fn to_formatted_string(&self) -> String {
279 format!("{self}").separate_with_underscores()
280 }
281}
282
283impl From<Quantity> for f64 {
284 fn from(qty: Quantity) -> Self {
285 qty.as_f64()
286 }
287}
288
289impl From<&Quantity> for f64 {
290 fn from(qty: &Quantity) -> Self {
291 qty.as_f64()
292 }
293}
294
295impl From<i32> for Quantity {
296 fn from(value: i32) -> Self {
297 Self::new(value as f64, 0)
298 }
299}
300
301impl From<i64> for Quantity {
302 fn from(value: i64) -> Self {
303 Self::new(value as f64, 0)
304 }
305}
306
307impl From<u32> for Quantity {
308 fn from(value: u32) -> Self {
309 Self::new(value as f64, 0)
310 }
311}
312
313impl From<u64> for Quantity {
314 fn from(value: u64) -> Self {
315 Self::new(value as f64, 0)
316 }
317}
318
319impl Hash for Quantity {
320 fn hash<H: Hasher>(&self, state: &mut H) {
321 self.raw.hash(state);
322 }
323}
324
325impl PartialEq for Quantity {
326 fn eq(&self, other: &Self) -> bool {
327 self.raw == other.raw
328 }
329}
330
331impl PartialOrd for Quantity {
332 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
333 Some(self.cmp(other))
334 }
335
336 fn lt(&self, other: &Self) -> bool {
337 self.raw.lt(&other.raw)
338 }
339
340 fn le(&self, other: &Self) -> bool {
341 self.raw.le(&other.raw)
342 }
343
344 fn gt(&self, other: &Self) -> bool {
345 self.raw.gt(&other.raw)
346 }
347
348 fn ge(&self, other: &Self) -> bool {
349 self.raw.ge(&other.raw)
350 }
351}
352
353impl Ord for Quantity {
354 fn cmp(&self, other: &Self) -> Ordering {
355 self.raw.cmp(&other.raw)
356 }
357}
358
359impl Deref for Quantity {
360 type Target = QuantityRaw;
361
362 fn deref(&self) -> &Self::Target {
363 &self.raw
364 }
365}
366
367impl Add for Quantity {
368 type Output = Self;
369 fn add(self, rhs: Self) -> Self::Output {
370 let precision = match self.precision {
371 0 => rhs.precision,
372 _ => self.precision,
373 };
374 assert!(
375 self.precision >= rhs.precision,
376 "Precision mismatch: cannot add precision {} to precision {} (precision loss)",
377 rhs.precision,
378 self.precision,
379 );
380 Self {
381 raw: self
382 .raw
383 .checked_add(rhs.raw)
384 .expect("Overflow occurred when adding `Quantity`"),
385 precision,
386 }
387 }
388}
389
390impl Sub for Quantity {
391 type Output = Self;
392 fn sub(self, rhs: Self) -> Self::Output {
393 let precision = match self.precision {
394 0 => rhs.precision,
395 _ => self.precision,
396 };
397 assert!(
398 self.precision >= rhs.precision,
399 "Precision mismatch: cannot subtract precision {} from precision {} (precision loss)",
400 rhs.precision,
401 self.precision,
402 );
403 Self {
404 raw: self
405 .raw
406 .checked_sub(rhs.raw)
407 .expect("Underflow occurred when subtracting `Quantity`"),
408 precision,
409 }
410 }
411}
412
413#[allow(clippy::suspicious_arithmetic_impl)] impl Mul for Quantity {
415 type Output = Self;
416 fn mul(self, rhs: Self) -> Self::Output {
417 let precision = match self.precision {
418 0 => rhs.precision,
419 _ => self.precision,
420 };
421 assert!(
422 self.precision >= rhs.precision,
423 "Precision mismatch: cannot multiply precision {} with precision {} (precision loss)",
424 rhs.precision,
425 self.precision,
426 );
427
428 let result_raw = self
429 .raw
430 .checked_mul(rhs.raw)
431 .expect("Overflow occurred when multiplying `Quantity`");
432
433 Self {
434 raw: result_raw / (FIXED_SCALAR as QuantityRaw),
435 precision,
436 }
437 }
438}
439
440impl Mul<f64> for Quantity {
441 type Output = f64;
442 fn mul(self, rhs: f64) -> Self::Output {
443 self.as_f64() * rhs
444 }
445}
446
447impl From<Quantity> for QuantityRaw {
448 fn from(value: Quantity) -> Self {
449 value.raw
450 }
451}
452
453impl From<&Quantity> for QuantityRaw {
454 fn from(value: &Quantity) -> Self {
455 value.raw
456 }
457}
458
459impl FromStr for Quantity {
460 type Err = String;
461
462 fn from_str(value: &str) -> Result<Self, Self::Err> {
463 let float_from_input = value
464 .replace('_', "")
465 .parse::<f64>()
466 .map_err(|e| format!("Error parsing `input` string '{value}' as f64: {e}"))?;
467
468 Self::new_checked(float_from_input, precision_from_str(value)).map_err(|e| e.to_string())
469 }
470}
471
472impl From<&str> for Quantity {
474 fn from(value: &str) -> Self {
475 Self::from_str(value).expect("Valid string input for `Quantity`")
476 }
477}
478
479impl From<String> for Quantity {
480 fn from(value: String) -> Self {
481 Self::from_str(&value).expect("Valid string input for `Quantity`")
482 }
483}
484
485impl From<&String> for Quantity {
486 fn from(value: &String) -> Self {
487 Self::from_str(value).expect("Valid string input for `Quantity`")
488 }
489}
490
491impl<T: Into<QuantityRaw>> AddAssign<T> for Quantity {
492 fn add_assign(&mut self, other: T) {
493 self.raw = self
494 .raw
495 .checked_add(other.into())
496 .expect("Overflow occurred when adding `Quantity`");
497 }
498}
499
500impl<T: Into<QuantityRaw>> SubAssign<T> for Quantity {
501 fn sub_assign(&mut self, other: T) {
502 self.raw = self
503 .raw
504 .checked_sub(other.into())
505 .expect("Underflow occurred when subtracting `Quantity`");
506 }
507}
508
509impl<T: Into<QuantityRaw>> MulAssign<T> for Quantity {
510 fn mul_assign(&mut self, other: T) {
511 self.raw = self
512 .raw
513 .checked_mul(other.into())
514 .expect("Overflow occurred when multiplying `Quantity`");
515 }
516}
517
518impl Debug for Quantity {
519 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
520 if self.precision > crate::types::fixed::MAX_FLOAT_PRECISION {
521 write!(f, "{}({})", stringify!(Quantity), self.raw)
522 } else {
523 write!(
524 f,
525 "{}({:.*})",
526 stringify!(Quantity),
527 self.precision as usize,
528 self.as_f64(),
529 )
530 }
531 }
532}
533
534impl Display for Quantity {
535 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
536 if self.precision > crate::types::fixed::MAX_FLOAT_PRECISION {
537 write!(f, "{}", self.raw)
538 } else {
539 write!(f, "{:.*}", self.precision as usize, self.as_f64(),)
540 }
541 }
542}
543
544impl Serialize for Quantity {
545 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
546 where
547 S: serde::Serializer,
548 {
549 serializer.serialize_str(&self.to_string())
550 }
551}
552
553impl<'de> Deserialize<'de> for Quantity {
554 fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
555 where
556 D: Deserializer<'de>,
557 {
558 let qty_str: &str = Deserialize::deserialize(_deserializer)?;
559 let qty: Self = qty_str.into();
560 Ok(qty)
561 }
562}
563
564pub fn check_positive_quantity(value: Quantity, param: &str) -> anyhow::Result<()> {
570 if !value.is_positive() {
571 anyhow::bail!("invalid `Quantity` for '{param}' not positive, was {value}")
572 }
573 Ok(())
574}
575
576#[cfg(test)]
580mod tests {
581 use std::str::FromStr;
582
583 use float_cmp::approx_eq;
584 use rstest::rstest;
585 use rust_decimal_macros::dec;
586
587 use super::*;
588
589 #[rstest]
590 #[should_panic(expected = "invalid `Quantity` for 'qty' not positive, was 0")]
591 fn test_check_quantity_positive() {
592 let qty = Quantity::new(0.0, 0);
593 check_positive_quantity(qty, "qty").unwrap();
594 }
595
596 #[rstest]
597 #[cfg(not(feature = "defi"))]
598 #[should_panic(expected = "`precision` exceeded maximum `FIXED_PRECISION` (16), was 17")]
599 fn test_invalid_precision_new() {
600 let _ = Quantity::new(1.0, 17);
602 }
603
604 #[rstest]
605 #[cfg(not(feature = "defi"))]
606 #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
607 fn test_invalid_precision_from_raw() {
608 let _ = Quantity::from_raw(1, FIXED_PRECISION + 1);
610 }
611
612 #[rstest]
613 #[cfg(not(feature = "defi"))]
614 #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
615 fn test_invalid_precision_zero() {
616 let _ = Quantity::zero(FIXED_PRECISION + 1);
618 }
619
620 #[rstest]
621 #[should_panic(
622 expected = "Precision mismatch: cannot add precision 2 to precision 1 (precision loss)"
623 )]
624 fn test_precision_mismatch_add() {
625 let q1 = Quantity::new(1.0, 1);
626 let q2 = Quantity::new(1.0, 2);
627 let _ = q1 + q2;
628 }
629
630 #[rstest]
631 #[should_panic(
632 expected = "Precision mismatch: cannot subtract precision 2 from precision 1 (precision loss)"
633 )]
634 fn test_precision_mismatch_sub() {
635 let q1 = Quantity::new(1.0, 1);
636 let q2 = Quantity::new(1.0, 2);
637 let _ = q1 - q2;
638 }
639
640 #[rstest]
641 #[should_panic(
642 expected = "Precision mismatch: cannot multiply precision 2 with precision 1 (precision loss)"
643 )]
644 fn test_precision_mismatch_mul() {
645 let q1 = Quantity::new(2.0, 1);
646 let q2 = Quantity::new(3.0, 2);
647 let _ = q1 * q2;
648 }
649
650 #[rstest]
651 fn test_new_non_zero_ok() {
652 let qty = Quantity::non_zero_checked(123.456, 3).unwrap();
653 assert_eq!(qty.raw, Quantity::new(123.456, 3).raw);
654 assert!(qty.is_positive());
655 }
656
657 #[rstest]
658 fn test_new_non_zero_zero_input() {
659 assert!(Quantity::non_zero_checked(0.0, 0).is_err());
660 }
661
662 #[rstest]
663 fn test_new_non_zero_rounds_to_zero() {
664 assert!(Quantity::non_zero_checked(0.0004, 3).is_err());
666 }
667
668 #[rstest]
669 fn test_new_non_zero_negative() {
670 assert!(Quantity::non_zero_checked(-1.0, 0).is_err());
671 }
672
673 #[rstest]
674 fn test_new_non_zero_exceeds_max() {
675 assert!(Quantity::non_zero_checked(QUANTITY_MAX * 10.0, 0).is_err());
676 }
677
678 #[rstest]
679 fn test_new_non_zero_invalid_precision() {
680 assert!(Quantity::non_zero_checked(1.0, FIXED_PRECISION + 1).is_err());
681 }
682
683 #[rstest]
684 fn test_new() {
685 let value = 0.00812;
686 let qty = Quantity::new(value, 8);
687 assert_eq!(qty, qty);
688 assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
689 assert_eq!(qty.precision, 8);
690 assert_eq!(qty, Quantity::from("0.00812000"));
691 assert_eq!(qty.as_decimal(), dec!(0.00812000));
692 assert_eq!(qty.to_string(), "0.00812000");
693 assert!(!qty.is_zero());
694 assert!(qty.is_positive());
695 assert!(approx_eq!(f64, qty.as_f64(), 0.00812, epsilon = 0.000_001));
696 }
697
698 #[rstest]
699 fn test_check_quantity_positive_ok() {
700 let qty = Quantity::new(10.0, 0);
701 check_positive_quantity(qty, "qty").unwrap();
702 }
703
704 #[rstest]
705 fn test_negative_quantity_validation() {
706 assert!(Quantity::new_checked(-1.0, FIXED_PRECISION).is_err());
707 }
708
709 #[rstest]
710 fn test_undefined() {
711 let qty = Quantity::from_raw(QUANTITY_UNDEF, 0);
712 assert_eq!(qty.raw, QUANTITY_UNDEF);
713 assert!(qty.is_undefined());
714 }
715
716 #[rstest]
717 fn test_zero() {
718 let qty = Quantity::zero(8);
719 assert_eq!(qty.raw, 0);
720 assert_eq!(qty.precision, 8);
721 assert!(qty.is_zero());
722 assert!(!qty.is_positive());
723 }
724
725 #[rstest]
726 fn test_from_i32() {
727 let value = 100_000i32;
728 let qty = Quantity::from(value);
729 assert_eq!(qty, qty);
730 assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
731 assert_eq!(qty.precision, 0);
732 }
733
734 #[rstest]
735 fn test_from_u32() {
736 let value: u32 = 5000;
737 let qty = Quantity::from(value);
738 assert_eq!(qty.raw, Quantity::from(format!("{value}")).raw);
739 assert_eq!(qty.precision, 0);
740 }
741
742 #[rstest]
743 fn test_from_i64() {
744 let value = 100_000i64;
745 let qty = Quantity::from(value);
746 assert_eq!(qty, qty);
747 assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
748 assert_eq!(qty.precision, 0);
749 }
750
751 #[rstest]
752 fn test_from_u64() {
753 let value = 100_000u64;
754 let qty = Quantity::from(value);
755 assert_eq!(qty, qty);
756 assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
757 assert_eq!(qty.precision, 0);
758 }
759
760 #[rstest] fn test_with_maximum_value() {
762 let qty = Quantity::new_checked(QUANTITY_MAX, 0);
763 assert!(qty.is_ok());
764 }
765
766 #[rstest]
767 fn test_with_minimum_positive_value() {
768 let value = 0.000_000_001;
769 let qty = Quantity::new(value, 9);
770 assert_eq!(qty.raw, Quantity::from("0.000000001").raw);
771 assert_eq!(qty.as_decimal(), dec!(0.000000001));
772 assert_eq!(qty.to_string(), "0.000000001");
773 }
774
775 #[rstest]
776 fn test_with_minimum_value() {
777 let qty = Quantity::new(QUANTITY_MIN, 9);
778 assert_eq!(qty.raw, 0);
779 assert_eq!(qty.as_decimal(), dec!(0));
780 assert_eq!(qty.to_string(), "0.000000000");
781 }
782
783 #[rstest]
784 fn test_is_zero() {
785 let qty = Quantity::zero(8);
786 assert_eq!(qty, qty);
787 assert_eq!(qty.raw, 0);
788 assert_eq!(qty.precision, 8);
789 assert_eq!(qty, Quantity::from("0.00000000"));
790 assert_eq!(qty.as_decimal(), dec!(0));
791 assert_eq!(qty.to_string(), "0.00000000");
792 assert!(qty.is_zero());
793 }
794
795 #[rstest]
796 fn test_precision() {
797 let value = 1.001;
798 let qty = Quantity::new(value, 2);
799 assert_eq!(qty.to_string(), "1.00");
800 }
801
802 #[rstest]
803 fn test_new_from_str() {
804 let qty = Quantity::new(0.00812000, 8);
805 assert_eq!(qty, qty);
806 assert_eq!(qty.precision, 8);
807 assert_eq!(qty, Quantity::from("0.00812000"));
808 assert_eq!(qty.to_string(), "0.00812000");
809 }
810
811 #[rstest]
812 #[case("0", 0)]
813 #[case("1.1", 1)]
814 #[case("1.123456789", 9)]
815 fn test_from_str_valid_input(#[case] input: &str, #[case] expected_prec: u8) {
816 let qty = Quantity::from(input);
817 assert_eq!(qty.precision, expected_prec);
818 assert_eq!(qty.as_decimal(), Decimal::from_str(input).unwrap());
819 }
820
821 #[rstest]
822 #[should_panic]
823 fn test_from_str_invalid_input() {
824 let input = "invalid";
825 Quantity::new(f64::from_str(input).unwrap(), 8);
826 }
827
828 #[rstest]
829 fn test_add() {
830 let a = 1.0;
831 let b = 2.0;
832 let quantity1 = Quantity::new(1.0, 0);
833 let quantity2 = Quantity::new(2.0, 0);
834 let quantity3 = quantity1 + quantity2;
835 assert_eq!(quantity3.raw, Quantity::new(a + b, 0).raw);
836 }
837
838 #[rstest]
839 fn test_sub() {
840 let a = 3.0;
841 let b = 2.0;
842 let quantity1 = Quantity::new(a, 0);
843 let quantity2 = Quantity::new(b, 0);
844 let quantity3 = quantity1 - quantity2;
845 assert_eq!(quantity3.raw, Quantity::new(a - b, 0).raw);
846 }
847
848 #[rstest]
849 fn test_add_assign() {
850 let a = 1.0;
851 let b = 2.0;
852 let mut quantity1 = Quantity::new(a, 0);
853 let quantity2 = Quantity::new(b, 0);
854 quantity1 += quantity2;
855 assert_eq!(quantity1.raw, Quantity::new(a + b, 0).raw);
856 }
857
858 #[rstest]
859 fn test_sub_assign() {
860 let a = 3.0;
861 let b = 2.0;
862 let mut quantity1 = Quantity::new(a, 0);
863 let quantity2 = Quantity::new(b, 0);
864 quantity1 -= quantity2;
865 assert_eq!(quantity1.raw, Quantity::new(a - b, 0).raw);
866 }
867
868 #[rstest]
869 fn test_mul() {
870 let value = 2.0;
871 let quantity1 = Quantity::new(value, 1);
872 let quantity2 = Quantity::new(value, 1);
873 let quantity3 = quantity1 * quantity2;
874 assert_eq!(quantity3.raw, Quantity::new(value * value, 0).raw);
875 }
876
877 #[rstest]
878 fn test_mul_assign() {
879 let mut quantity = Quantity::new(2.0, 0);
880 quantity *= 3u64; assert_eq!(quantity.raw, Quantity::new(6.0, 0).raw);
882
883 let mut fraction = Quantity::new(1.5, 2);
884 fraction *= 2u64; assert_eq!(fraction.raw, Quantity::new(3.0, 2).raw);
886 }
887
888 #[rstest]
889 fn test_comparisons() {
890 assert_eq!(Quantity::new(1.0, 1), Quantity::new(1.0, 1));
891 assert_eq!(Quantity::new(1.0, 1), Quantity::new(1.0, 2));
892 assert_ne!(Quantity::new(1.1, 1), Quantity::new(1.0, 1));
893 assert!(Quantity::new(1.0, 1) <= Quantity::new(1.0, 2));
894 assert!(Quantity::new(1.1, 1) > Quantity::new(1.0, 1));
895 assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 1));
896 assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 2));
897 assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 2));
898 assert!(Quantity::new(0.9, 1) < Quantity::new(1.0, 1));
899 assert!(Quantity::new(0.9, 1) <= Quantity::new(1.0, 2));
900 assert!(Quantity::new(0.9, 1) <= Quantity::new(1.0, 1));
901 }
902
903 #[rstest]
904 fn test_debug() {
905 let quantity = Quantity::from_str("44.12").unwrap();
906 let result = format!("{quantity:?}");
907 assert_eq!(result, "Quantity(44.12)");
908 }
909
910 #[rstest]
911 fn test_display() {
912 let quantity = Quantity::from_str("44.12").unwrap();
913 let result = format!("{quantity}");
914 assert_eq!(result, "44.12");
915 }
916
917 #[rstest]
918 #[case(44.12, 2, "Quantity(44.12)", "44.12")] #[case(1234.567, 8, "Quantity(1234.56700000)", "1234.56700000")] #[cfg_attr(
921 feature = "defi",
922 case(
923 1_000_000_000_000_000_000.0,
924 18,
925 "Quantity(1000000000000000000)",
926 "1000000000000000000"
927 )
928 )] fn test_debug_display_precision_handling(
930 #[case] value: f64,
931 #[case] precision: u8,
932 #[case] expected_debug: &str,
933 #[case] expected_display: &str,
934 ) {
935 let quantity = if precision > crate::types::fixed::MAX_FLOAT_PRECISION {
936 Quantity::from_raw(value as QuantityRaw, precision)
938 } else {
939 Quantity::new(value, precision)
940 };
941
942 assert_eq!(format!("{quantity:?}"), expected_debug);
943 assert_eq!(format!("{quantity}"), expected_display);
944 }
945
946 #[rstest]
947 fn test_to_formatted_string() {
948 let qty = Quantity::new(1234.5678, 4);
949 let formatted = qty.to_formatted_string();
950 assert_eq!(formatted, "1_234.5678");
951 assert_eq!(qty.to_string(), "1234.5678");
952 }
953
954 #[rstest]
955 fn test_hash() {
956 use std::{
957 collections::hash_map::DefaultHasher,
958 hash::{Hash, Hasher},
959 };
960
961 let q1 = Quantity::new(100.0, 1);
962 let q2 = Quantity::new(100.0, 1);
963 let q3 = Quantity::new(200.0, 1);
964
965 let mut s1 = DefaultHasher::new();
966 let mut s2 = DefaultHasher::new();
967 let mut s3 = DefaultHasher::new();
968
969 q1.hash(&mut s1);
970 q2.hash(&mut s2);
971 q3.hash(&mut s3);
972
973 assert_eq!(
974 s1.finish(),
975 s2.finish(),
976 "Equal quantities must hash equally"
977 );
978 assert_ne!(
979 s1.finish(),
980 s3.finish(),
981 "Different quantities must hash differently"
982 );
983 }
984
985 #[rstest]
986 fn test_quantity_serde_json_round_trip() {
987 let original = Quantity::new(123.456, 3);
988 let json_str = serde_json::to_string(&original).unwrap();
989 assert_eq!(json_str, "\"123.456\"");
990
991 let deserialized: Quantity = serde_json::from_str(&json_str).unwrap();
992 assert_eq!(deserialized, original);
993 assert_eq!(deserialized.precision, 3);
994 }
995}
996
997#[cfg(test)]
1001mod property_tests {
1002 use proptest::prelude::*;
1003
1004 use super::*;
1005
1006 fn quantity_value_strategy() -> impl Strategy<Value = f64> {
1008 prop_oneof![
1010 0.00001..1.0,
1012 1.0..100_000.0,
1014 100_000.0..1_000_000.0,
1016 Just(0.0),
1018 ]
1019 }
1020
1021 fn precision_strategy() -> impl Strategy<Value = u8> {
1023 #[cfg(feature = "defi")]
1024 {
1025 0..=16u8
1027 }
1028 #[cfg(not(feature = "defi"))]
1029 {
1030 0..=FIXED_PRECISION
1031 }
1032 }
1033
1034 proptest! {
1035 #[test]
1037 fn prop_quantity_serde_round_trip(
1038 value in quantity_value_strategy(),
1039 precision in 0u8..=6u8 ) {
1041 let original = Quantity::new(value, precision);
1042
1043 let string_repr = original.to_string();
1045 let from_string: Quantity = string_repr.parse().unwrap();
1046 prop_assert_eq!(from_string.raw, original.raw);
1047 prop_assert_eq!(from_string.precision, original.precision);
1048
1049 let json = serde_json::to_string(&original).unwrap();
1051 let from_json: Quantity = serde_json::from_str(&json).unwrap();
1052 prop_assert_eq!(from_json.precision, original.precision);
1053 }
1055
1056 #[test]
1058 fn prop_quantity_arithmetic_associative(
1059 a in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
1060 b in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
1061 c in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
1062 precision in 0u8..=6u8 ) {
1064 let q_a = Quantity::new(a, precision);
1065 let q_b = Quantity::new(b, precision);
1066 let q_c = Quantity::new(c, precision);
1067
1068 let ab_raw = q_a.raw.checked_add(q_b.raw);
1070 let bc_raw = q_b.raw.checked_add(q_c.raw);
1071
1072 if let (Some(ab_raw), Some(bc_raw)) = (ab_raw, bc_raw) {
1073 let ab_c_raw = ab_raw.checked_add(q_c.raw);
1074 let a_bc_raw = q_a.raw.checked_add(bc_raw);
1075
1076 if let (Some(ab_c_raw), Some(a_bc_raw)) = (ab_c_raw, a_bc_raw) {
1077 prop_assert_eq!(ab_c_raw, a_bc_raw, "Associativity failed in raw arithmetic");
1079 }
1080 }
1081 }
1082
1083 #[test]
1085 fn prop_quantity_addition_subtraction_inverse(
1086 base in quantity_value_strategy().prop_filter("Reasonable values", |&x| x < 1e6),
1087 delta in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
1088 precision in 0u8..=6u8 ) {
1090 let q_base = Quantity::new(base, precision);
1091 let q_delta = Quantity::new(delta, precision);
1092
1093 if let Some(added_raw) = q_base.raw.checked_add(q_delta.raw) {
1095 if let Some(result_raw) = added_raw.checked_sub(q_delta.raw) {
1096 prop_assert_eq!(result_raw, q_base.raw, "Inverse operation failed in raw arithmetic");
1098 }
1099 }
1100 }
1101
1102 #[test]
1104 fn prop_quantity_ordering_transitive(
1105 a in quantity_value_strategy(),
1106 b in quantity_value_strategy(),
1107 c in quantity_value_strategy(),
1108 precision in precision_strategy()
1109 ) {
1110 let q_a = Quantity::new(a, precision);
1111 let q_b = Quantity::new(b, precision);
1112 let q_c = Quantity::new(c, precision);
1113
1114 if q_a <= q_b && q_b <= q_c {
1116 prop_assert!(q_a <= q_c, "Transitivity failed: {} <= {} <= {} but {} > {}",
1117 q_a.as_f64(), q_b.as_f64(), q_c.as_f64(), q_a.as_f64(), q_c.as_f64());
1118 }
1119 }
1120
1121 #[test]
1123 fn prop_quantity_string_parsing_precision(
1124 integral in 0u32..1000000,
1125 fractional in 0u32..1000000,
1126 precision in 1u8..=6
1127 ) {
1128 let fractional_str = format!("{:0width$}", fractional % 10_u32.pow(precision as u32), width = precision as usize);
1130 let quantity_str = format!("{integral}.{fractional_str}");
1131
1132 let parsed: Quantity = quantity_str.parse().unwrap();
1133 prop_assert_eq!(parsed.precision, precision);
1134
1135 let round_trip = parsed.to_string();
1137 let expected_value = format!("{integral}.{fractional_str}");
1138 prop_assert_eq!(round_trip, expected_value);
1139 }
1140
1141 #[test]
1143 fn prop_quantity_precision_information_preservation(
1144 value in quantity_value_strategy().prop_filter("Reasonable values", |&x| x < 1e6),
1145 precision1 in 1u8..=6u8, precision2 in 1u8..=6u8
1147 ) {
1148 prop_assume!(precision1 != precision2);
1150
1151 let _q1 = Quantity::new(value, precision1);
1152 let _q2 = Quantity::new(value, precision2);
1153
1154 let min_precision = precision1.min(precision2);
1157
1158 let scale = 10.0_f64.powi(min_precision as i32);
1160 let rounded_value = (value * scale).round() / scale;
1161
1162 let q1_reduced = Quantity::new(rounded_value, min_precision);
1163 let q2_reduced = Quantity::new(rounded_value, min_precision);
1164
1165 prop_assert_eq!(q1_reduced.raw, q2_reduced.raw, "Precision reduction inconsistent");
1167 }
1168
1169 #[test]
1171 fn prop_quantity_arithmetic_bounds(
1172 a in quantity_value_strategy(),
1173 b in quantity_value_strategy(),
1174 precision in precision_strategy()
1175 ) {
1176 let q_a = Quantity::new(a, precision);
1177 let q_b = Quantity::new(b, precision);
1178
1179 let sum_f64 = q_a.as_f64() + q_b.as_f64();
1181 if sum_f64.is_finite() && (QUANTITY_MIN..=QUANTITY_MAX).contains(&sum_f64) {
1182 let sum = q_a + q_b;
1183 prop_assert!(sum.as_f64().is_finite());
1184 prop_assert!(!sum.is_undefined());
1185 }
1186
1187 let diff_f64 = q_a.as_f64() - q_b.as_f64();
1189 if diff_f64.is_finite() && (QUANTITY_MIN..=QUANTITY_MAX).contains(&diff_f64) {
1190 let diff = q_a - q_b;
1191 prop_assert!(diff.as_f64().is_finite());
1192 prop_assert!(!diff.is_undefined());
1193 }
1194 }
1195
1196 #[test]
1198 fn prop_quantity_multiplication_non_negative(
1199 a in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 0.0 && x < 100.0),
1200 b in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 0.0 && x < 100.0),
1201 precision in precision_strategy()
1202 ) {
1203 let q_a = Quantity::new(a, precision);
1204 let q_b = Quantity::new(b, precision);
1205
1206 let product_f64 = q_a.as_f64() * q_b.as_f64();
1208 let safe_limit = if cfg!(any(feature = "defi", feature = "high-precision")) {
1210 QUANTITY_MAX / 10000.0 } else {
1212 QUANTITY_MAX
1213 };
1214
1215 if product_f64.is_finite() && product_f64 <= safe_limit {
1216 let product = q_a * q_b;
1218 prop_assert!(product.as_f64() >= 0.0, "Quantity multiplication produced negative value: {}", product.as_f64());
1219 }
1220 }
1221
1222 #[test]
1224 fn prop_quantity_zero_addition_identity(
1225 value in quantity_value_strategy(),
1226 precision in precision_strategy()
1227 ) {
1228 let q = Quantity::new(value, precision);
1229 let zero = Quantity::zero(precision);
1230
1231 prop_assert_eq!(q + zero, q);
1233 prop_assert_eq!(zero + q, q);
1234 }
1235 }
1236}