1use std::{
43 cmp::Ordering,
44 fmt::{Debug, Display},
45 hash::{Hash, Hasher},
46 ops::{Add, Deref, Div, Mul, Neg, Sub},
47 str::FromStr,
48};
49
50use nautilus_core::{
51 correctness::{
52 CorrectnessError, CorrectnessResult, CorrectnessResultExt, FAILED,
53 check_in_range_inclusive_f64,
54 },
55 string::formatting::Separable,
56};
57use rust_decimal::Decimal;
58use serde::{Deserialize, Deserializer, Serialize};
59
60use super::fixed::{
61 FIXED_PRECISION, FIXED_SCALAR, check_fixed_precision, mantissa_exponent_to_fixed_i128,
62 mantissa_exponent_to_raw_checked, raw_scales_match,
63};
64#[cfg(feature = "high-precision")]
65use super::fixed::{PRECISION_DIFF_SCALAR, f64_to_fixed_i128, fixed_i128_to_f64};
66#[cfg(not(feature = "high-precision"))]
67use super::fixed::{f64_to_fixed_i64, fixed_i64_to_f64};
68#[cfg(feature = "defi")]
69use crate::types::fixed::MAX_FLOAT_PRECISION;
70
71#[cfg(feature = "high-precision")]
79pub type PriceRaw = i128;
80
81#[cfg(not(feature = "high-precision"))]
82pub type PriceRaw = i64;
83
84#[unsafe(no_mangle)]
95#[allow(unsafe_code)]
96pub static PRICE_RAW_MAX: PriceRaw = (PRICE_MAX as PriceRaw) * (FIXED_SCALAR as PriceRaw);
97
98#[unsafe(no_mangle)]
107#[allow(unsafe_code)]
108pub static PRICE_RAW_MIN: PriceRaw = (PRICE_MIN as PriceRaw) * (FIXED_SCALAR as PriceRaw);
109
110pub const PRICE_UNDEF: PriceRaw = PriceRaw::MAX;
112
113pub const PRICE_ERROR: PriceRaw = PriceRaw::MIN;
115
116#[cfg(feature = "high-precision")]
122pub const PRICE_MAX: f64 = 17_014_118_346_046.0;
123
124#[cfg(not(feature = "high-precision"))]
125pub const PRICE_MAX: f64 = 9_223_372_036.0;
127
128#[cfg(feature = "high-precision")]
133pub const PRICE_MIN: f64 = -17_014_118_346_046.0;
135
136#[cfg(not(feature = "high-precision"))]
137pub const PRICE_MIN: f64 = -9_223_372_036.0;
139
140pub const ERROR_PRICE: Price = Price {
144 raw: 0,
145 precision: 255,
146};
147
148#[repr(C)]
159#[derive(Clone, Copy, Default, Eq)]
160#[cfg_attr(
161 feature = "python",
162 pyo3::pyclass(
163 module = "nautilus_trader.core.nautilus_pyo3.model",
164 frozen,
165 from_py_object
166 )
167)]
168#[cfg_attr(
169 feature = "python",
170 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
171)]
172pub struct Price {
173 pub raw: PriceRaw,
175 pub precision: u8,
177}
178
179impl Price {
180 pub fn new_checked(value: f64, precision: u8) -> CorrectnessResult<Self> {
192 check_in_range_inclusive_f64(value, PRICE_MIN, PRICE_MAX, "value")?;
193
194 #[cfg(feature = "defi")]
195 if precision > MAX_FLOAT_PRECISION {
196 return Err(CorrectnessError::PredicateViolation {
198 message: format!(
199 "`precision` exceeded maximum float precision ({MAX_FLOAT_PRECISION}), use `Price::from_wei()` for wei values instead"
200 ),
201 });
202 }
203
204 check_fixed_precision(precision)?;
205
206 #[cfg(feature = "high-precision")]
207 let raw = f64_to_fixed_i128(value, precision);
208
209 #[cfg(not(feature = "high-precision"))]
210 let raw = f64_to_fixed_i64(value, precision);
211
212 Ok(Self { raw, precision })
213 }
214
215 #[must_use]
221 pub fn new(value: f64, precision: u8) -> Self {
222 Self::new_checked(value, precision).expect_display(FAILED)
223 }
224
225 #[must_use]
232 pub fn from_raw(raw: PriceRaw, precision: u8) -> Self {
233 assert!(
234 raw == PRICE_ERROR
235 || raw == PRICE_UNDEF
236 || (raw >= PRICE_RAW_MIN && raw <= PRICE_RAW_MAX),
237 "`raw` value {raw} outside valid range [{PRICE_RAW_MIN}, {PRICE_RAW_MAX}] for Price"
238 );
239
240 if raw == PRICE_UNDEF {
241 assert!(
242 precision == 0,
243 "`precision` must be 0 when `raw` is PRICE_UNDEF"
244 );
245 }
246 check_fixed_precision(precision).expect_display(FAILED);
247
248 Self { raw, precision }
257 }
258
259 pub fn from_raw_checked(raw: PriceRaw, precision: u8) -> CorrectnessResult<Self> {
270 if raw == PRICE_UNDEF && precision != 0 {
271 return Err(CorrectnessError::PredicateViolation {
272 message: "`precision` must be 0 when `raw` is PRICE_UNDEF".to_string(),
273 });
274 }
275
276 if raw != PRICE_ERROR && raw != PRICE_UNDEF && (raw < PRICE_RAW_MIN || raw > PRICE_RAW_MAX)
277 {
278 return Err(CorrectnessError::PredicateViolation {
279 message: format!(
280 "raw value {raw} outside valid range [{PRICE_RAW_MIN}, {PRICE_RAW_MAX}]"
281 ),
282 });
283 }
284
285 check_fixed_precision(precision)?;
286
287 Ok(Self { raw, precision })
288 }
289
290 #[must_use]
296 pub fn zero(precision: u8) -> Self {
297 check_fixed_precision(precision).expect_display(FAILED);
298 Self { raw: 0, precision }
299 }
300
301 #[must_use]
307 pub fn max(precision: u8) -> Self {
308 check_fixed_precision(precision).expect_display(FAILED);
309 Self {
310 raw: PRICE_RAW_MAX,
311 precision,
312 }
313 }
314
315 #[must_use]
321 pub fn min(precision: u8) -> Self {
322 check_fixed_precision(precision).expect_display(FAILED);
323 Self {
324 raw: PRICE_RAW_MIN,
325 precision,
326 }
327 }
328
329 #[must_use]
337 pub fn checked_add(self, rhs: Self) -> Option<Self> {
338 if self.is_sentinel() || rhs.is_sentinel() {
339 return None;
340 }
341
342 if !raw_scales_match(self.precision, rhs.precision) {
343 return None;
344 }
345 let raw = self.raw.checked_add(rhs.raw)?;
346 if raw < PRICE_RAW_MIN || raw > PRICE_RAW_MAX {
347 return None;
348 }
349 Some(Self {
350 raw,
351 precision: self.precision.max(rhs.precision),
352 })
353 }
354
355 #[must_use]
363 pub fn checked_sub(self, rhs: Self) -> Option<Self> {
364 if self.is_sentinel() || rhs.is_sentinel() {
365 return None;
366 }
367
368 if !raw_scales_match(self.precision, rhs.precision) {
369 return None;
370 }
371 let raw = self.raw.checked_sub(rhs.raw)?;
372 if raw < PRICE_RAW_MIN || raw > PRICE_RAW_MAX {
373 return None;
374 }
375 Some(Self {
376 raw,
377 precision: self.precision.max(rhs.precision),
378 })
379 }
380
381 #[inline]
382 fn is_sentinel(self) -> bool {
383 self.raw == PRICE_UNDEF || self.raw == PRICE_ERROR || self.precision == u8::MAX
387 }
388
389 #[must_use]
391 pub fn is_undefined(&self) -> bool {
392 self.raw == PRICE_UNDEF
393 }
394
395 #[must_use]
397 pub fn is_zero(&self) -> bool {
398 self.raw == 0
399 }
400
401 #[must_use]
403 pub fn is_positive(&self) -> bool {
404 self.raw != PRICE_UNDEF && self.raw > 0
405 }
406
407 #[cfg(feature = "high-precision")]
408 #[must_use]
414 pub fn as_f64(&self) -> f64 {
415 #[cfg(feature = "defi")]
416 assert!(
417 self.precision <= MAX_FLOAT_PRECISION,
418 "Invalid f64 conversion beyond `MAX_FLOAT_PRECISION` (16)"
419 );
420
421 fixed_i128_to_f64(self.raw)
422 }
423
424 #[cfg(not(feature = "high-precision"))]
425 #[must_use]
431 pub fn as_f64(&self) -> f64 {
432 #[cfg(feature = "defi")]
433 if self.precision > MAX_FLOAT_PRECISION {
434 panic!("Invalid f64 conversion beyond `MAX_FLOAT_PRECISION` (16)");
435 }
436
437 fixed_i64_to_f64(self.raw)
438 }
439
440 #[must_use]
442 pub fn as_decimal(&self) -> Decimal {
443 let precision_diff = FIXED_PRECISION.saturating_sub(self.precision);
445 let rescaled_raw = self.raw / PriceRaw::pow(10, u32::from(precision_diff));
446 #[allow(
447 clippy::unnecessary_cast,
448 clippy::cast_lossless,
449 reason = "cast is real when PriceRaw is i64, no-op when i128"
450 )]
451 Decimal::from_i128_with_scale(rescaled_raw as i128, u32::from(self.precision))
452 }
453
454 #[must_use]
456 pub fn to_formatted_string(&self) -> String {
457 format!("{self}").separate_with_underscores()
458 }
459
460 pub fn from_decimal_dp(decimal: Decimal, precision: u8) -> CorrectnessResult<Self> {
472 let exponent = -(decimal.scale() as i8);
473 let raw_i128 = mantissa_exponent_to_fixed_i128(decimal.mantissa(), exponent, precision)?;
474
475 #[allow(
476 clippy::useless_conversion,
477 reason = "i128 to PriceRaw is real when not high-precision"
478 )]
479 let raw: PriceRaw =
480 raw_i128
481 .try_into()
482 .map_err(|_| CorrectnessError::PredicateViolation {
483 message: format!(
484 "Decimal value exceeds PriceRaw range [{PRICE_RAW_MIN}, {PRICE_RAW_MAX}]"
485 ),
486 })?;
487
488 if !(raw >= PRICE_RAW_MIN && raw <= PRICE_RAW_MAX) {
489 return Err(CorrectnessError::PredicateViolation {
490 message: format!(
491 "Raw value {raw} outside valid range [{PRICE_RAW_MIN}, {PRICE_RAW_MAX}] for Price"
492 ),
493 });
494 }
495
496 Ok(Self { raw, precision })
497 }
498
499 pub fn from_decimal(decimal: Decimal) -> CorrectnessResult<Self> {
511 let precision = decimal.scale() as u8;
512 Self::from_decimal_dp(decimal, precision)
513 }
514
515 #[must_use]
524 pub fn from_mantissa_exponent(mantissa: i64, exponent: i8, precision: u8) -> Self {
525 check_fixed_precision(precision).expect_display(FAILED);
526
527 if mantissa == 0 {
528 return Self { raw: 0, precision };
529 }
530
531 let raw_i128 = mantissa_exponent_to_fixed_i128(i128::from(mantissa), exponent, precision)
532 .expect("Overflow in Price::from_mantissa_exponent");
533
534 #[allow(
535 clippy::useless_conversion,
536 reason = "i128 to PriceRaw is real when not high-precision"
537 )]
538 let raw: PriceRaw = raw_i128
539 .try_into()
540 .expect("Raw value exceeds PriceRaw range in Price::from_mantissa_exponent");
541 assert!(
542 raw >= PRICE_RAW_MIN && raw <= PRICE_RAW_MAX,
543 "`raw` value {raw} exceeded bounds [{PRICE_RAW_MIN}, {PRICE_RAW_MAX}] for Price"
544 );
545
546 Self { raw, precision }
547 }
548
549 pub fn from_mantissa_exponent_checked(
556 mantissa: i64,
557 exponent: i8,
558 precision: u8,
559 ) -> CorrectnessResult<Self> {
560 let raw = mantissa_exponent_to_raw_checked::<PriceRaw>(
561 i128::from(mantissa),
562 exponent,
563 precision,
564 "Price::from_mantissa_exponent",
565 "PriceRaw",
566 "Price",
567 )?;
568
569 Self::from_raw_checked(raw, precision)
570 }
571}
572
573impl FromStr for Price {
574 type Err = String;
575
576 fn from_str(value: &str) -> Result<Self, Self::Err> {
577 let clean_value = value.replace('_', "");
578
579 let decimal = if clean_value.contains('e') || clean_value.contains('E') {
580 Decimal::from_scientific(&clean_value)
581 .map_err(|e| format!("Error parsing `input` string '{value}' as Decimal: {e}"))?
582 } else {
583 Decimal::from_str(&clean_value)
584 .map_err(|e| format!("Error parsing `input` string '{value}' as Decimal: {e}"))?
585 };
586
587 let precision = decimal.scale() as u8;
589
590 Self::from_decimal_dp(decimal, precision).map_err(|e| e.to_string())
591 }
592}
593
594impl<T: AsRef<str>> From<T> for Price {
595 fn from(value: T) -> Self {
596 Self::from_str(value.as_ref()).expect(FAILED)
597 }
598}
599
600impl From<Price> for f64 {
601 fn from(price: Price) -> Self {
602 price.as_f64()
603 }
604}
605
606impl From<&Price> for f64 {
607 fn from(price: &Price) -> Self {
608 price.as_f64()
609 }
610}
611
612impl From<Price> for Decimal {
613 fn from(value: Price) -> Self {
614 value.as_decimal()
615 }
616}
617
618impl From<&Price> for Decimal {
619 fn from(value: &Price) -> Self {
620 value.as_decimal()
621 }
622}
623
624impl Hash for Price {
625 fn hash<H: Hasher>(&self, state: &mut H) {
626 self.raw.hash(state);
627 }
628}
629
630impl PartialEq for Price {
631 fn eq(&self, other: &Self) -> bool {
632 self.raw == other.raw
633 }
634}
635
636impl PartialOrd for Price {
637 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
638 Some(self.cmp(other))
639 }
640
641 fn lt(&self, other: &Self) -> bool {
642 self.raw.lt(&other.raw)
643 }
644
645 fn le(&self, other: &Self) -> bool {
646 self.raw.le(&other.raw)
647 }
648
649 fn gt(&self, other: &Self) -> bool {
650 self.raw.gt(&other.raw)
651 }
652
653 fn ge(&self, other: &Self) -> bool {
654 self.raw.ge(&other.raw)
655 }
656}
657
658impl Ord for Price {
659 fn cmp(&self, other: &Self) -> Ordering {
660 self.raw.cmp(&other.raw)
661 }
662}
663
664impl Deref for Price {
665 type Target = PriceRaw;
666
667 fn deref(&self) -> &Self::Target {
668 &self.raw
669 }
670}
671
672impl Neg for Price {
673 type Output = Self;
674 fn neg(self) -> Self::Output {
675 if self.raw == PRICE_ERROR || self.raw == PRICE_UNDEF {
677 return self;
678 }
679 Self {
680 raw: -self.raw,
681 precision: self.precision,
682 }
683 }
684}
685
686impl Add for Price {
687 type Output = Self;
688 fn add(self, rhs: Self) -> Self::Output {
689 Self {
690 raw: self
691 .raw
692 .checked_add(rhs.raw)
693 .expect("Overflow occurred when adding `Price`"),
694 precision: self.precision.max(rhs.precision),
695 }
696 }
697}
698
699impl Sub for Price {
700 type Output = Self;
701 fn sub(self, rhs: Self) -> Self::Output {
702 Self {
703 raw: self
704 .raw
705 .checked_sub(rhs.raw)
706 .expect("Underflow occurred when subtracting `Price`"),
707 precision: self.precision.max(rhs.precision),
708 }
709 }
710}
711
712impl Add<Decimal> for Price {
713 type Output = Decimal;
714 fn add(self, rhs: Decimal) -> Self::Output {
715 self.as_decimal() + rhs
716 }
717}
718
719impl Sub<Decimal> for Price {
720 type Output = Decimal;
721 fn sub(self, rhs: Decimal) -> Self::Output {
722 self.as_decimal() - rhs
723 }
724}
725
726impl Mul<Decimal> for Price {
727 type Output = Decimal;
728 fn mul(self, rhs: Decimal) -> Self::Output {
729 self.as_decimal() * rhs
730 }
731}
732
733impl Div<Decimal> for Price {
734 type Output = Decimal;
735 fn div(self, rhs: Decimal) -> Self::Output {
736 self.as_decimal() / rhs
737 }
738}
739
740impl Add<f64> for Price {
741 type Output = f64;
742 fn add(self, rhs: f64) -> Self::Output {
743 self.as_f64() + rhs
744 }
745}
746
747impl Sub<f64> for Price {
748 type Output = f64;
749 fn sub(self, rhs: f64) -> Self::Output {
750 self.as_f64() - rhs
751 }
752}
753
754impl Mul<f64> for Price {
755 type Output = f64;
756 fn mul(self, rhs: f64) -> Self::Output {
757 self.as_f64() * rhs
758 }
759}
760
761impl Div<f64> for Price {
762 type Output = f64;
763 fn div(self, rhs: f64) -> Self::Output {
764 self.as_f64() / rhs
765 }
766}
767
768impl Debug for Price {
769 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
770 if self.precision > crate::types::fixed::MAX_FLOAT_PRECISION {
771 write!(f, "{}({})", stringify!(Price), self.raw)
772 } else {
773 write!(f, "{}({})", stringify!(Price), self.as_decimal())
774 }
775 }
776}
777
778impl Display for Price {
779 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
780 if self.precision > crate::types::fixed::MAX_FLOAT_PRECISION {
781 write!(f, "{}", self.raw)
782 } else {
783 write!(f, "{}", self.as_decimal())
784 }
785 }
786}
787
788impl Serialize for Price {
789 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
790 where
791 S: serde::Serializer,
792 {
793 serializer.serialize_str(&self.to_string())
794 }
795}
796
797impl<'de> Deserialize<'de> for Price {
798 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
799 where
800 D: Deserializer<'de>,
801 {
802 let price_str: std::borrow::Cow<'de, str> = Deserialize::deserialize(deserializer)?;
803 Self::from_str(price_str.as_ref()).map_err(serde::de::Error::custom)
804 }
805}
806
807pub fn check_positive_price(value: Price, param: &str) -> CorrectnessResult<()> {
813 if value.raw == PRICE_UNDEF {
814 return Err(CorrectnessError::InvalidValue {
815 param: param.to_string(),
816 value: "PRICE_UNDEF".to_string(),
817 type_name: "`Price`",
818 });
819 }
820
821 if !value.is_positive() {
822 return Err(CorrectnessError::NotPositive {
823 param: param.to_string(),
824 value: value.to_string(),
825 type_name: "`Price`",
826 });
827 }
828 Ok(())
829}
830
831#[cfg(feature = "high-precision")]
832#[must_use]
835pub fn decode_raw_price_i64(value: i64) -> PriceRaw {
836 PriceRaw::from(value) * PRECISION_DIFF_SCALAR as PriceRaw
837}
838
839#[cfg(not(feature = "high-precision"))]
840#[must_use]
841pub fn decode_raw_price_i64(value: i64) -> PriceRaw {
842 value
843}
844
845#[cfg(test)]
846mod tests {
847 use nautilus_core::{approx_eq, correctness::CorrectnessError};
848 use rstest::rstest;
849 use rust_decimal_macros::dec;
850
851 use super::*;
852
853 #[rstest]
854 fn test_extreme_prices_round_trip_through_raw() {
855 let max = Price::new(PRICE_MAX, 0);
858 let min = Price::new(PRICE_MIN, 0);
859
860 assert_eq!(max.raw, PRICE_RAW_MAX);
861 assert_eq!(min.raw, PRICE_RAW_MIN);
862 assert!(Price::from_raw_checked(max.raw, 0).is_ok());
863 assert!(Price::from_raw_checked(min.raw, 0).is_ok());
864 }
865
866 #[rstest]
867 #[cfg(all(not(feature = "defi"), not(feature = "high-precision")))]
868 #[should_panic(expected = "`precision` exceeded maximum `FIXED_PRECISION` (9), was 50")]
869 fn test_invalid_precision_new() {
870 let _ = Price::new(1.0, 50);
872 }
873
874 #[rstest]
875 #[cfg(all(not(feature = "defi"), feature = "high-precision"))]
876 #[should_panic(expected = "`precision` exceeded maximum `FIXED_PRECISION` (16), was 50")]
877 fn test_invalid_precision_new() {
878 let _ = Price::new(1.0, 50);
880 }
881
882 #[rstest]
883 #[cfg(not(feature = "defi"))]
884 #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
885 fn test_invalid_precision_from_raw() {
886 let _ = Price::from_raw(1, FIXED_PRECISION + 1);
888 }
889
890 #[rstest]
891 #[cfg(not(feature = "defi"))]
892 #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
893 fn test_invalid_precision_max() {
894 let _ = Price::max(FIXED_PRECISION + 1);
896 }
897
898 #[rstest]
899 #[cfg(not(feature = "defi"))]
900 #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
901 fn test_invalid_precision_min() {
902 let _ = Price::min(FIXED_PRECISION + 1);
904 }
905
906 #[rstest]
907 #[cfg(not(feature = "defi"))]
908 #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
909 fn test_invalid_precision_zero() {
910 let _ = Price::zero(FIXED_PRECISION + 1);
912 }
913
914 #[rstest]
915 #[should_panic(expected = "Condition failed: invalid f64 for 'value' not in range")]
916 fn test_max_value_exceeded() {
917 let _ = Price::new(PRICE_MAX + 0.1, FIXED_PRECISION);
918 }
919
920 #[rstest]
921 #[should_panic(expected = "Condition failed: invalid f64 for 'value' not in range")]
922 fn test_min_value_exceeded() {
923 let _ = Price::new(PRICE_MIN - 0.1, FIXED_PRECISION);
924 }
925
926 #[rstest]
927 fn test_is_positive_ok() {
928 let price = Price::new(42.0, 2);
930 assert!(price.is_positive());
931
932 check_positive_price(price, "price").unwrap();
934 }
935
936 #[rstest]
937 fn test_is_positive_rejects_non_positive() {
938 let zero = Price::zero(2);
940 let error = check_positive_price(zero, "price").unwrap_err();
941
942 assert_eq!(
943 error,
944 CorrectnessError::NotPositive {
945 param: "price".to_string(),
946 value: "0.00".to_string(),
947 type_name: "`Price`",
948 }
949 );
950 assert_eq!(
951 error.to_string(),
952 "invalid `Price` for 'price' not positive, was 0.00"
953 );
954 }
955
956 #[rstest]
957 fn test_is_positive_rejects_undefined() {
958 let undef = Price::from_raw(PRICE_UNDEF, 0);
960 let error = check_positive_price(undef, "price").unwrap_err();
961
962 assert_eq!(
963 error,
964 CorrectnessError::InvalidValue {
965 param: "price".to_string(),
966 value: "PRICE_UNDEF".to_string(),
967 type_name: "`Price`",
968 }
969 );
970 assert_eq!(
971 error.to_string(),
972 "invalid `Price` for 'price', was PRICE_UNDEF"
973 );
974 }
975
976 #[rstest]
977 fn test_construction() {
978 let price = Price::new_checked(1.23456, 4);
979 assert!(price.is_ok());
980 let price = price.unwrap();
981 assert_eq!(price.precision, 4);
982 assert!(approx_eq!(f64, price.as_f64(), 1.23456, epsilon = 0.0001));
983 }
984
985 #[rstest]
986 fn test_negative_price_in_range() {
987 let neg_price = Price::new(PRICE_MIN / 2.0, FIXED_PRECISION);
989 assert!(neg_price.raw < 0);
990 }
991
992 #[rstest]
993 fn test_new_checked() {
994 assert!(Price::new_checked(1.0, FIXED_PRECISION).is_ok());
996 assert!(Price::new_checked(f64::NAN, FIXED_PRECISION).is_err());
997 assert!(Price::new_checked(f64::INFINITY, FIXED_PRECISION).is_err());
998 }
999
1000 #[rstest]
1001 fn test_new_checked_returns_typed_error_with_stable_display() {
1002 let error = Price::new_checked(PRICE_MAX + 1.0, FIXED_PRECISION).unwrap_err();
1003
1004 assert!(matches!(error, CorrectnessError::OutOfRange { .. }));
1005 assert_eq!(
1006 error.to_string(),
1007 format!(
1008 "invalid f64 for 'value' not in range [{PRICE_MIN}, {PRICE_MAX}], was {}",
1009 PRICE_MAX + 1.0
1010 )
1011 );
1012 }
1013
1014 #[rstest]
1015 fn test_from_raw_checked_returns_typed_error_with_stable_display() {
1016 let error = Price::from_raw_checked(PRICE_UNDEF, 3).unwrap_err();
1017
1018 assert_eq!(
1019 error,
1020 CorrectnessError::PredicateViolation {
1021 message: "`precision` must be 0 when `raw` is PRICE_UNDEF".to_string(),
1022 }
1023 );
1024 assert_eq!(
1025 error.to_string(),
1026 "`precision` must be 0 when `raw` is PRICE_UNDEF"
1027 );
1028 }
1029
1030 #[rstest]
1031 fn test_from_raw() {
1032 let raw = 100 * FIXED_SCALAR as PriceRaw;
1033 let price = Price::from_raw(raw, 2);
1034 assert_eq!(price.raw, raw);
1035 assert_eq!(price.precision, 2);
1036 }
1037
1038 #[rstest]
1039 fn test_zero_constructor() {
1040 let zero = Price::zero(3);
1041 assert!(zero.is_zero());
1042 assert_eq!(zero.precision, 3);
1043 }
1044
1045 #[rstest]
1046 fn test_max_constructor() {
1047 let max = Price::max(4);
1048 assert_eq!(max.raw, PRICE_RAW_MAX);
1049 assert_eq!(max.precision, 4);
1050 }
1051
1052 #[rstest]
1053 fn test_min_constructor() {
1054 let min = Price::min(4);
1055 assert_eq!(min.raw, PRICE_RAW_MIN);
1056 assert_eq!(min.precision, 4);
1057 }
1058
1059 #[rstest]
1060 fn test_nan_validation() {
1061 assert!(Price::new_checked(f64::NAN, FIXED_PRECISION).is_err());
1062 }
1063
1064 #[rstest]
1065 fn test_infinity_validation() {
1066 assert!(Price::new_checked(f64::INFINITY, FIXED_PRECISION).is_err());
1067 assert!(Price::new_checked(f64::NEG_INFINITY, FIXED_PRECISION).is_err());
1068 }
1069
1070 #[rstest]
1071 fn test_special_values() {
1072 let zero = Price::zero(5);
1073 assert!(zero.is_zero());
1074 assert_eq!(zero.to_string(), "0.00000");
1075
1076 let undef = Price::from_raw(PRICE_UNDEF, 0);
1077 assert!(undef.is_undefined());
1078
1079 let error = ERROR_PRICE;
1080 assert_eq!(error.precision, 255);
1081 }
1082
1083 #[rstest]
1084 fn test_string_parsing() {
1085 let price: Price = "123.456".into();
1086 assert_eq!(price.precision, 3);
1087 assert_eq!(price, Price::from("123.456"));
1088 }
1089
1090 #[rstest]
1091 fn test_negative_price_from_str() {
1092 let price: Price = "-123.45".parse().unwrap();
1093 assert_eq!(price.precision, 2);
1094 assert!(approx_eq!(f64, price.as_f64(), -123.45, epsilon = 1e-9));
1095 }
1096
1097 #[rstest]
1098 fn test_string_parsing_errors() {
1099 assert!(Price::from_str("invalid").is_err());
1100 }
1101
1102 #[rstest]
1103 #[case("1e7", 0, 10_000_000.0)]
1104 #[case("1.5e3", 0, 1_500.0)]
1105 #[case("1.234e-2", 5, 0.01234)]
1106 #[case("5E-3", 3, 0.005)]
1107 fn test_from_str_scientific_notation(
1108 #[case] input: &str,
1109 #[case] expected_precision: u8,
1110 #[case] expected_value: f64,
1111 ) {
1112 let price = Price::from_str(input).unwrap();
1113 assert_eq!(price.precision, expected_precision);
1114 assert!(approx_eq!(
1115 f64,
1116 price.as_f64(),
1117 expected_value,
1118 epsilon = 1e-10
1119 ));
1120 }
1121
1122 #[rstest]
1123 #[case("1_234.56", 2, 1234.56)]
1124 #[case("1000000", 0, 1_000_000.0)]
1125 #[case("99_999.999_99", 5, 99_999.999_99)]
1126 fn test_from_str_with_underscores(
1127 #[case] input: &str,
1128 #[case] expected_precision: u8,
1129 #[case] expected_value: f64,
1130 ) {
1131 let price = Price::from_str(input).unwrap();
1132 assert_eq!(price.precision, expected_precision);
1133 assert!(approx_eq!(
1134 f64,
1135 price.as_f64(),
1136 expected_value,
1137 epsilon = 1e-10
1138 ));
1139 }
1140
1141 #[rstest]
1142 fn test_from_decimal_dp_preservation() {
1143 let decimal = dec!(123.456789);
1145 let price = Price::from_decimal_dp(decimal, 6).unwrap();
1146 assert_eq!(price.precision, 6);
1147 assert!(approx_eq!(
1148 f64,
1149 price.as_f64(),
1150 123.456_789,
1151 epsilon = 1e-10
1152 ));
1153
1154 let expected_raw = 123_456_789 * 10_i64.pow(u32::from(FIXED_PRECISION - 6));
1156 assert_eq!(price.raw, PriceRaw::from(expected_raw));
1157 }
1158
1159 #[rstest]
1160 fn test_from_decimal_dp_rounding() {
1161 let decimal = dec!(1.005);
1163 let price = Price::from_decimal_dp(decimal, 2).unwrap();
1164 assert_eq!(price.as_f64(), 1.0); let decimal = dec!(1.015);
1167 let price = Price::from_decimal_dp(decimal, 2).unwrap();
1168 assert_eq!(price.as_f64(), 1.02); }
1170
1171 #[rstest]
1172 fn test_from_decimal_infers_precision() {
1173 let decimal = dec!(123.456);
1175 let price = Price::from_decimal(decimal).unwrap();
1176 assert_eq!(price.precision, 3);
1177 assert!(approx_eq!(f64, price.as_f64(), 123.456, epsilon = 1e-10));
1178
1179 let decimal = dec!(100);
1181 let price = Price::from_decimal(decimal).unwrap();
1182 assert_eq!(price.precision, 0);
1183 assert_eq!(price.as_f64(), 100.0);
1184
1185 let decimal = dec!(1.23456789);
1187 let price = Price::from_decimal(decimal).unwrap();
1188 assert_eq!(price.precision, 8);
1189 assert!(approx_eq!(
1190 f64,
1191 price.as_f64(),
1192 1.234_567_89,
1193 epsilon = 1e-10
1194 ));
1195 }
1196
1197 #[rstest]
1198 fn test_from_decimal_trailing_zeros() {
1199 let decimal = dec!(1.230);
1201 assert_eq!(decimal.scale(), 3); let price = Price::from_decimal(decimal).unwrap();
1205 assert_eq!(price.precision, 3);
1206 assert!(approx_eq!(f64, price.as_f64(), 1.23, epsilon = 1e-10));
1207
1208 let normalized = decimal.normalize();
1210 assert_eq!(normalized.scale(), 2);
1211 let price_normalized = Price::from_decimal(normalized).unwrap();
1212 assert_eq!(price_normalized.precision, 2);
1213 }
1214
1215 #[rstest]
1216 #[case("1.00", 2)]
1217 #[case("1.0", 1)]
1218 #[case("1.000", 3)]
1219 #[case("100.00", 2)]
1220 #[case("0.10", 2)]
1221 #[case("0.100", 3)]
1222 fn test_from_str_preserves_trailing_zeros(#[case] input: &str, #[case] expected_precision: u8) {
1223 let price = Price::from_str(input).unwrap();
1224 assert_eq!(price.precision, expected_precision);
1225 }
1226
1227 #[rstest]
1228 fn test_from_decimal_excessive_precision_inference() {
1229 let decimal = dec!(1.1234567890123456789012345678);
1232
1233 if decimal.scale() > u32::from(FIXED_PRECISION) {
1235 assert!(Price::from_decimal(decimal).is_err());
1236 }
1237 }
1238
1239 #[rstest]
1240 fn test_from_decimal_dp_out_of_range_returns_typed_error_with_stable_display() {
1241 let huge = Decimal::from_str("99999999999999999999.99").unwrap();
1242 let error = Price::from_decimal_dp(huge, 2).unwrap_err();
1243 match error {
1244 CorrectnessError::PredicateViolation { ref message } => {
1245 assert!(
1246 message.contains("PriceRaw range") || message.contains("for Price"),
1247 "unexpected message: {message:?}",
1248 );
1249 }
1250 _ => panic!("expected PredicateViolation, was {error:?}"),
1251 }
1252 }
1253
1254 #[rstest]
1255 fn test_from_decimal_negative_price() {
1256 let decimal = dec!(-123.45);
1258 let price = Price::from_decimal(decimal).unwrap();
1259 assert_eq!(price.precision, 2);
1260 assert!(approx_eq!(f64, price.as_f64(), -123.45, epsilon = 1e-10));
1261 assert!(price.raw < 0);
1262 }
1263
1264 #[rstest]
1265 fn test_string_formatting() {
1266 assert_eq!(format!("{}", Price::new(1234.5678, 4)), "1234.5678");
1267 assert_eq!(
1268 format!("{:?}", Price::new(1234.5678, 4)),
1269 "Price(1234.5678)"
1270 );
1271 assert_eq!(Price::new(1234.5678, 4).to_formatted_string(), "1_234.5678");
1272 }
1273
1274 #[rstest]
1275 #[case(1234.5678, 4, "Price(1234.5678)", "1234.5678")] #[case(123.456_789_012_345, 8, "Price(123.45678901)", "123.45678901")] #[cfg_attr(
1278 feature = "defi",
1279 case(
1280 2_000_000_000_000_000_000.0,
1281 18,
1282 "Price(2000000000000000000)",
1283 "2000000000000000000"
1284 )
1285 )] fn test_string_formatting_precision_handling(
1287 #[case] value: f64,
1288 #[case] precision: u8,
1289 #[case] expected_debug: &str,
1290 #[case] expected_display: &str,
1291 ) {
1292 let price = if precision > crate::types::fixed::MAX_FLOAT_PRECISION {
1293 Price::from_raw(value as PriceRaw, precision)
1294 } else {
1295 Price::new(value, precision)
1296 };
1297
1298 assert_eq!(format!("{price:?}"), expected_debug);
1299 assert_eq!(format!("{price}"), expected_display);
1300 assert_eq!(
1301 price.to_formatted_string().replace('_', ""),
1302 expected_display
1303 );
1304 }
1305
1306 #[rstest]
1307 fn test_decimal_conversions() {
1308 let price = Price::new(123.456, 3);
1309 assert_eq!(price.as_decimal(), dec!(123.456));
1310
1311 let price = Price::new(0.000_001, 6);
1312 assert_eq!(price.as_decimal(), dec!(0.000001));
1313 }
1314
1315 #[rstest]
1316 fn test_basic_arithmetic() {
1317 let p1 = Price::new(10.5, 2);
1318 let p2 = Price::new(5.25, 2);
1319 assert_eq!(p1 + p2, Price::from("15.75"));
1320 assert_eq!(p1 - p2, Price::from("5.25"));
1321 assert_eq!(-p1, Price::from("-10.5"));
1322 }
1323
1324 #[rstest]
1325 fn test_price_checked_add_within_bounds() {
1326 let a = Price::new(10.0, 2);
1327 let b = Price::new(5.0, 2);
1328 assert_eq!(a.checked_add(b), Some(Price::new(15.0, 2)));
1329
1330 let neg = Price::new(-3.0, 2);
1331 assert_eq!(a.checked_add(neg), Some(Price::new(7.0, 2)));
1332 }
1333
1334 #[rstest]
1335 fn test_price_checked_add_above_max_returns_none() {
1336 let near_max = Price::from_raw(PRICE_RAW_MAX, 0);
1337 let one = Price::new(1.0, 0);
1338 assert_eq!(near_max.checked_add(one), None);
1339 }
1340
1341 #[rstest]
1342 fn test_price_checked_sub_within_bounds() {
1343 let a = Price::new(10.0, 2);
1344 let b = Price::new(3.0, 2);
1345 assert_eq!(a.checked_sub(b), Some(Price::new(7.0, 2)));
1346 assert_eq!(b.checked_sub(a), Some(Price::new(-7.0, 2)));
1347 }
1348
1349 #[rstest]
1350 fn test_price_checked_sub_below_min_returns_none() {
1351 let near_min = Price::from_raw(PRICE_RAW_MIN, 0);
1352 let one = Price::new(1.0, 0);
1353 assert_eq!(near_min.checked_sub(one), None);
1354 }
1355
1356 #[rstest]
1357 fn test_price_checked_arith_uses_max_precision() {
1358 let a = Price::new(10.5, 1);
1359 let b = Price::new(5.25, 2);
1360 let sum = a.checked_add(b).unwrap();
1361 assert_eq!(sum.precision, 2);
1362 assert_eq!(sum.as_f64(), 15.75);
1363 }
1364
1365 #[rstest]
1366 fn test_price_checked_add_rejects_sentinel_undef() {
1367 let undef = Price::from_raw(PRICE_UNDEF, 0);
1368 let one = Price::new(1.0, 0);
1369 assert_eq!(undef.checked_add(one), None);
1370 assert_eq!(one.checked_add(undef), None);
1371 }
1372
1373 #[rstest]
1374 fn test_price_checked_sub_rejects_sentinel_undef() {
1375 let undef = Price::from_raw(PRICE_UNDEF, 0);
1376 let neg_one = Price::new(-1.0, 0);
1377 assert_eq!(undef.checked_sub(neg_one), None);
1378 }
1379
1380 #[rstest]
1381 fn test_price_checked_arith_rejects_error_price() {
1382 let one = Price::new(1.0, 0);
1383 assert_eq!(ERROR_PRICE.checked_add(one), None);
1384 assert_eq!(one.checked_sub(ERROR_PRICE), None);
1385 }
1386
1387 #[rstest]
1388 fn test_price_checked_arith_rejects_raw_error() {
1389 let error = Price::from_raw(PRICE_ERROR, 0);
1390 let one = Price::new(1.0, 0);
1391 assert_eq!(error.checked_add(one), None);
1392 assert_eq!(one.checked_add(error), None);
1393 assert_eq!(error.checked_sub(one), None);
1394 assert_eq!(one.checked_sub(error), None);
1395 }
1396
1397 #[rstest]
1398 fn test_price_checked_add_at_exact_max_returns_some() {
1399 let near_max = Price::from_raw(PRICE_RAW_MAX - 1, 0);
1400 let one_unit = Price::from_raw(1, 0);
1401 assert_eq!(
1402 near_max.checked_add(one_unit),
1403 Some(Price::from_raw(PRICE_RAW_MAX, 0)),
1404 );
1405 }
1406
1407 #[rstest]
1408 fn test_price_checked_sub_at_exact_min_returns_some() {
1409 let near_min = Price::from_raw(PRICE_RAW_MIN + 1, 0);
1410 let one_unit = Price::from_raw(1, 0);
1411 assert_eq!(
1412 near_min.checked_sub(one_unit),
1413 Some(Price::from_raw(PRICE_RAW_MIN, 0)),
1414 );
1415 }
1416
1417 #[rstest]
1418 fn test_mixed_precision_add() {
1419 let p1 = Price::new(10.5, 1);
1420 let p2 = Price::new(5.25, 2);
1421 let result = p1 + p2;
1422 assert_eq!(result.precision, 2);
1423 assert_eq!(result.as_f64(), 15.75);
1424 }
1425
1426 #[rstest]
1427 fn test_mixed_precision_sub() {
1428 let p1 = Price::new(10.5, 1);
1429 let p2 = Price::new(5.25, 2);
1430 let result = p1 - p2;
1431 assert_eq!(result.precision, 2);
1432 assert_eq!(result.as_f64(), 5.25);
1433 }
1434
1435 #[rstest]
1436 fn test_f64_operations() {
1437 let p = Price::new(10.5, 2);
1438 assert_eq!(p + 1.0, 11.5);
1439 assert_eq!(p - 1.0, 9.5);
1440 assert_eq!(p * 2.0, 21.0);
1441 assert_eq!(p / 2.0, 5.25);
1442 }
1443
1444 #[rstest]
1445 fn test_equality_and_comparisons() {
1446 let p1 = Price::new(10.0, 1);
1447 let p2 = Price::new(20.0, 1);
1448 let p3 = Price::new(10.0, 1);
1449
1450 assert!(p1 < p2);
1451 assert!(p2 > p1);
1452 assert!(p1 <= p3);
1453 assert!(p1 >= p3);
1454 assert_eq!(p1, p3);
1455 assert_ne!(p1, p2);
1456
1457 assert_eq!(Price::from("1.0"), Price::from("1.0"));
1458 assert_ne!(Price::from("1.1"), Price::from("1.0"));
1459 assert!(Price::from("1.0") <= Price::from("1.0"));
1460 assert!(Price::from("1.1") > Price::from("1.0"));
1461 assert!(Price::from("1.0") >= Price::from("1.0"));
1462 assert!(Price::from("1.0") >= Price::from("1.0"));
1463 assert!(Price::from("1.0") >= Price::from("1.0"));
1464 assert!(Price::from("0.9") < Price::from("1.0"));
1465 assert!(Price::from("0.9") <= Price::from("1.0"));
1466 assert!(Price::from("0.9") <= Price::from("1.0"));
1467 }
1468
1469 #[rstest]
1470 fn test_deref() {
1471 let price = Price::new(10.0, 1);
1472 assert_eq!(*price, price.raw);
1473 }
1474
1475 #[rstest]
1476 fn test_decode_raw_price_i64() {
1477 let raw_scaled_by_1e9 = 42_000_000_000i64; let decoded = decode_raw_price_i64(raw_scaled_by_1e9);
1479 let price = Price::from_raw(decoded, FIXED_PRECISION);
1480 assert!(
1481 approx_eq!(f64, price.as_f64(), 42.0, epsilon = 1e-9),
1482 "Expected 42.0 f64, was {} (precision = {})",
1483 price.as_f64(),
1484 price.precision
1485 );
1486 }
1487
1488 #[rstest]
1489 fn test_hash() {
1490 use std::{
1491 collections::hash_map::DefaultHasher,
1492 hash::{Hash, Hasher},
1493 };
1494
1495 let price1 = Price::new(1.0, 2);
1496 let price2 = Price::new(1.0, 2);
1497 let price3 = Price::new(1.1, 2);
1498
1499 let mut hasher1 = DefaultHasher::new();
1500 let mut hasher2 = DefaultHasher::new();
1501 let mut hasher3 = DefaultHasher::new();
1502
1503 price1.hash(&mut hasher1);
1504 price2.hash(&mut hasher2);
1505 price3.hash(&mut hasher3);
1506
1507 assert_eq!(hasher1.finish(), hasher2.finish());
1508 assert_ne!(hasher1.finish(), hasher3.finish());
1509 }
1510
1511 #[rstest]
1512 fn test_price_serde_json_round_trip() {
1513 let price = Price::new(1.0500, 4);
1514 let json = serde_json::to_string(&price).unwrap();
1515 let deserialized: Price = serde_json::from_str(&json).unwrap();
1516 assert_eq!(deserialized, price);
1517 }
1518
1519 #[rstest]
1520 fn test_price_serde_json_from_value_round_trip() {
1521 let price = Price::new(1.0500, 4);
1522 let value = serde_json::to_value(price).unwrap();
1523
1524 let deserialized: Price = serde_json::from_value(value).unwrap();
1525 assert_eq!(deserialized, price);
1526 assert_eq!(deserialized.precision, 4);
1527 }
1528
1529 #[rstest]
1530 fn test_price_deserialize_invalid_string_returns_error() {
1531 let result = serde_json::from_str::<Price>("\"not-a-price\"");
1532 let error = result.unwrap_err();
1533 assert!(
1534 error.to_string().contains("Error parsing"),
1535 "unexpected message: {error}"
1536 );
1537 }
1538
1539 #[rstest]
1540 fn test_price_deserialize_out_of_range_returns_error() {
1541 let result = serde_json::from_str::<Price>("\"99999999999999999999.99\"");
1542 assert!(result.is_err());
1543 }
1544
1545 #[rstest]
1546 fn test_from_mantissa_exponent_exact_precision() {
1547 let price = Price::from_mantissa_exponent(12345, -2, 2);
1548 assert_eq!(price.as_f64(), 123.45);
1549 }
1550
1551 #[rstest]
1552 fn test_from_mantissa_exponent_excess_rounds_down() {
1553 let price = Price::from_mantissa_exponent(12345, -3, 2);
1555 assert_eq!(price.as_f64(), 12.34);
1556 }
1557
1558 #[rstest]
1559 fn test_from_mantissa_exponent_excess_rounds_up() {
1560 let price = Price::from_mantissa_exponent(12355, -3, 2);
1562 assert_eq!(price.as_f64(), 12.36);
1563 }
1564
1565 #[rstest]
1566 fn test_from_mantissa_exponent_positive_exponent() {
1567 let price = Price::from_mantissa_exponent(5, 2, 0);
1568 assert_eq!(price.as_f64(), 500.0);
1569 }
1570
1571 #[rstest]
1572 fn test_from_mantissa_exponent_negative_mantissa() {
1573 let price = Price::from_mantissa_exponent(-12345, -2, 2);
1574 assert_eq!(price.as_f64(), -123.45);
1575 }
1576
1577 #[rstest]
1578 fn test_from_mantissa_exponent_zero() {
1579 let price = Price::from_mantissa_exponent(0, 2, 2);
1580 assert_eq!(price.as_f64(), 0.0);
1581 }
1582
1583 #[rstest]
1584 fn test_from_mantissa_exponent_checked_exact_precision() {
1585 let price = Price::from_mantissa_exponent_checked(12345, -2, 2).unwrap();
1586 assert_eq!(price.as_decimal(), dec!(123.45));
1587 }
1588
1589 #[rstest]
1590 fn test_from_mantissa_exponent_checked_zero_with_large_exponent() {
1591 let price = Price::from_mantissa_exponent_checked(0, 119, 2).unwrap();
1592 assert_eq!(price.as_decimal(), dec!(0.00));
1593 }
1594
1595 #[rstest]
1596 fn test_from_mantissa_exponent_checked_invalid_precision() {
1597 #[cfg(feature = "defi")]
1598 let invalid_precision = crate::defi::WEI_PRECISION + 1;
1599 #[cfg(not(feature = "defi"))]
1600 let invalid_precision = FIXED_PRECISION + 1;
1601
1602 let error = Price::from_mantissa_exponent_checked(1, 0, invalid_precision).unwrap_err();
1603 assert!(error.to_string().contains("`precision` exceeded maximum"));
1604 }
1605
1606 #[rstest]
1607 fn test_from_mantissa_exponent_checked_overflow_returns_error() {
1608 let error = Price::from_mantissa_exponent_checked(i64::MAX, 100, 0).unwrap_err();
1609 assert!(
1610 error
1611 .to_string()
1612 .contains("Overflow in Price::from_mantissa_exponent")
1613 );
1614 }
1615
1616 #[rstest]
1617 #[should_panic(expected = "Price::from_mantissa_exponent")]
1618 fn test_from_mantissa_exponent_overflow_panics() {
1619 let _ = Price::from_mantissa_exponent(i64::MAX, 9, 0);
1620 }
1621
1622 #[rstest]
1623 #[should_panic(expected = "exceeds i128 range")]
1624 fn test_from_mantissa_exponent_large_exponent_panics() {
1625 let _ = Price::from_mantissa_exponent(1, 119, 0);
1626 }
1627
1628 #[rstest]
1629 fn test_from_mantissa_exponent_zero_with_large_exponent() {
1630 let price = Price::from_mantissa_exponent(0, 119, 0);
1631 assert_eq!(price.as_f64(), 0.0);
1632 }
1633
1634 #[rstest]
1635 fn test_from_mantissa_exponent_very_negative_exponent_rounds_to_zero() {
1636 let price = Price::from_mantissa_exponent(12345, -120, 2);
1637 assert_eq!(price.as_f64(), 0.0);
1638 }
1639
1640 #[rstest]
1641 fn test_decimal_arithmetic_operations() {
1642 let price = Price::new(100.0, 2);
1643 assert_eq!(price + dec!(50.25), dec!(150.25));
1644 assert_eq!(price - dec!(30.50), dec!(69.50));
1645 assert_eq!(price * dec!(1.5), dec!(150.00));
1646 assert_eq!(price / dec!(4), dec!(25.00));
1647 }
1648}
1649
1650#[cfg(test)]
1651mod property_tests {
1652 use proptest::prelude::*;
1653 use rstest::rstest;
1654
1655 use super::*;
1656
1657 fn price_value_strategy() -> impl Strategy<Value = f64> {
1659 prop_oneof![
1662 0.00001..1.0,
1664 1.0..100_000.0,
1666 100_000.0..1_000_000.0,
1668 -1_000.0..0.0,
1670 Just(PRICE_MIN / 2.0),
1672 Just(PRICE_MAX / 2.0),
1673 ]
1674 }
1675
1676 fn float_precision_upper_bound() -> u8 {
1677 FIXED_PRECISION.min(crate::types::fixed::MAX_FLOAT_PRECISION)
1678 }
1679
1680 fn precision_strategy() -> impl Strategy<Value = u8> {
1682 let upper = float_precision_upper_bound();
1683 prop_oneof![Just(0u8), 0u8..=upper, Just(FIXED_PRECISION),]
1684 }
1685
1686 fn precision_strategy_non_zero() -> impl Strategy<Value = u8> {
1687 let upper = float_precision_upper_bound().max(1);
1688 prop_oneof![Just(upper), Just(FIXED_PRECISION.max(1)), 1u8..=upper,]
1689 }
1690
1691 fn valid_precision_raw_strategy() -> impl Strategy<Value = (u8, PriceRaw)> {
1695 precision_strategy().prop_flat_map(|precision| {
1696 let scale: PriceRaw = if precision >= FIXED_PRECISION {
1697 1
1698 } else {
1699 (10 as PriceRaw).pow(u32::from(FIXED_PRECISION - precision))
1700 };
1701 let max_base = PRICE_RAW_MAX / scale;
1703 let min_base = PRICE_RAW_MIN / scale;
1704 (min_base..=max_base).prop_map(move |base| (precision, base * scale))
1705 })
1706 }
1707
1708 fn float_precision_strategy() -> impl Strategy<Value = u8> {
1710 precision_strategy()
1711 }
1712
1713 const DECIMAL_MAX_MANTISSA: i128 = 79_228_162_514_264_337_593_543_950_335;
1714
1715 #[expect(
1716 clippy::useless_conversion,
1717 reason = "PriceRaw is i64 or i128 depending on feature"
1718 )]
1719 fn decimal_compatible(raw: PriceRaw, precision: u8) -> bool {
1720 if precision > crate::types::fixed::MAX_FLOAT_PRECISION {
1721 return false;
1722 }
1723 let precision_diff = u32::from(FIXED_PRECISION.saturating_sub(precision));
1724 let divisor = (10 as PriceRaw).pow(precision_diff);
1725 let rescaled_raw = raw / divisor;
1726 i128::from(rescaled_raw.abs()) <= DECIMAL_MAX_MANTISSA
1727 }
1728
1729 proptest! {
1730 #[rstest]
1732 fn prop_price_serde_round_trip(
1733 value in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() < 1e6),
1734 precision in precision_strategy()
1735 ) {
1736 let original = Price::new(value, precision);
1737
1738 let string_repr = original.to_string();
1740 let from_string: Price = string_repr.parse().unwrap();
1741 prop_assert_eq!(from_string.raw, original.raw);
1742 prop_assert_eq!(from_string.precision, original.precision);
1743
1744 let json = serde_json::to_string(&original).unwrap();
1746 let from_json: Price = serde_json::from_str(&json).unwrap();
1747 prop_assert_eq!(from_json.precision, original.precision);
1748 }
1750
1751 #[rstest]
1753 fn prop_price_arithmetic_associative(
1754 a in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() > 1e-3 && x.abs() < 1e6),
1755 b in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() > 1e-3 && x.abs() < 1e6),
1756 c in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() > 1e-3 && x.abs() < 1e6),
1757 precision in precision_strategy()
1758 ) {
1759 let p_a = Price::new(a, precision);
1760 let p_b = Price::new(b, precision);
1761 let p_c = Price::new(c, precision);
1762
1763 let ab_raw = p_a.raw.checked_add(p_b.raw);
1765 let bc_raw = p_b.raw.checked_add(p_c.raw);
1766
1767 if let (Some(ab_raw), Some(bc_raw)) = (ab_raw, bc_raw) {
1768 let ab_c_raw = ab_raw.checked_add(p_c.raw);
1769 let a_bc_raw = p_a.raw.checked_add(bc_raw);
1770
1771 if let (Some(ab_c_raw), Some(a_bc_raw)) = (ab_c_raw, a_bc_raw) {
1772 prop_assert_eq!(ab_c_raw, a_bc_raw, "Associativity failed in raw arithmetic");
1774 }
1775 }
1776 }
1777
1778 #[rstest]
1780 fn prop_price_addition_subtraction_inverse(
1781 base in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() < 1e6),
1782 delta in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() > 1e-3 && x.abs() < 1e6),
1783 precision in precision_strategy()
1784 ) {
1785 let p_base = Price::new(base, precision);
1786 let p_delta = Price::new(delta, precision);
1787
1788 if let Some(added_raw) = p_base.raw.checked_add(p_delta.raw)
1790 && let Some(result_raw) = added_raw.checked_sub(p_delta.raw) {
1791 prop_assert_eq!(result_raw, p_base.raw, "Inverse operation failed in raw arithmetic");
1793 }
1794 }
1795
1796 #[rstest]
1798 fn prop_price_ordering_transitive(
1799 a in price_value_strategy(),
1800 b in price_value_strategy(),
1801 c in price_value_strategy(),
1802 precision in float_precision_strategy()
1803 ) {
1804 let p_a = Price::new(a, precision);
1805 let p_b = Price::new(b, precision);
1806 let p_c = Price::new(c, precision);
1807
1808 if p_a <= p_b && p_b <= p_c {
1810 prop_assert!(p_a <= p_c, "Transitivity failed: {} <= {} <= {} but {} > {}",
1811 p_a.as_f64(), p_b.as_f64(), p_c.as_f64(), p_a.as_f64(), p_c.as_f64());
1812 }
1813 }
1814
1815 #[rstest]
1817 fn prop_price_string_parsing_precision(
1818 integral in 0u32..1_000_000,
1819 fractional in 0u32..1_000_000,
1820 precision in precision_strategy_non_zero()
1821 ) {
1822 let pow = 10u128.pow(u32::from(precision));
1824 let fractional_mod = u128::from(fractional) % pow;
1825 let fractional_str = format!("{:0width$}", fractional_mod, width = precision as usize);
1826 let price_str = format!("{integral}.{fractional_str}");
1827
1828 let parsed: Price = price_str.parse().unwrap();
1829 prop_assert_eq!(parsed.precision, precision);
1830
1831 let round_trip = parsed.to_string();
1833 let expected_value = format!("{integral}.{fractional_str}");
1834 prop_assert_eq!(round_trip, expected_value);
1835 }
1836
1837 #[rstest]
1839 fn prop_price_precision_information_preservation(
1840 value in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() < 1e6),
1841 precision1 in precision_strategy_non_zero(),
1842 precision2 in precision_strategy_non_zero()
1843 ) {
1844 prop_assume!(precision1 != precision2);
1846
1847 let _p1 = Price::new(value, precision1);
1848 let _p2 = Price::new(value, precision2);
1849
1850 let min_precision = precision1.min(precision2);
1853
1854 let scale = 10.0_f64.powi(i32::from(min_precision));
1856 let rounded_value = (value * scale).round() / scale;
1857
1858 let p1_reduced = Price::new(rounded_value, min_precision);
1859 let p2_reduced = Price::new(rounded_value, min_precision);
1860
1861 prop_assert_eq!(p1_reduced.raw, p2_reduced.raw, "Precision reduction inconsistent");
1863 }
1864
1865 #[rstest]
1867 fn prop_price_arithmetic_bounds(
1868 a in price_value_strategy(),
1869 b in price_value_strategy(),
1870 precision in float_precision_strategy()
1871 ) {
1872 let p_a = Price::new(a, precision);
1873 let p_b = Price::new(b, precision);
1874
1875 let sum_f64 = p_a.as_f64() + p_b.as_f64();
1877 if sum_f64.is_finite() && (PRICE_MIN..=PRICE_MAX).contains(&sum_f64) {
1878 let sum = p_a + p_b;
1879 prop_assert!(sum.as_f64().is_finite());
1880 prop_assert!(!sum.is_undefined());
1881 }
1882
1883 let diff_f64 = p_a.as_f64() - p_b.as_f64();
1885 if diff_f64.is_finite() && (PRICE_MIN..=PRICE_MAX).contains(&diff_f64) {
1886 let diff = p_a - p_b;
1887 prop_assert!(diff.as_f64().is_finite());
1888 prop_assert!(!diff.is_undefined());
1889 }
1890 }
1891
1892 #[rstest]
1895 fn prop_price_checked_add_matches_spec(
1896 a in price_value_strategy(),
1897 b in price_value_strategy(),
1898 precision in float_precision_strategy()
1899 ) {
1900 let p_a = Price::new(a, precision);
1901 let p_b = Price::new(b, precision);
1902 let expected = p_a.raw
1903 .checked_add(p_b.raw)
1904 .filter(|r| (PRICE_RAW_MIN..=PRICE_RAW_MAX).contains(r))
1905 .filter(|_| !p_a.is_sentinel() && !p_b.is_sentinel())
1906 .map(|raw| Price { raw, precision: p_a.precision.max(p_b.precision) });
1907 prop_assert_eq!(p_a.checked_add(p_b), expected);
1908 }
1909
1910 #[rstest]
1913 fn prop_price_checked_sub_matches_spec(
1914 a in price_value_strategy(),
1915 b in price_value_strategy(),
1916 precision in float_precision_strategy()
1917 ) {
1918 let p_a = Price::new(a, precision);
1919 let p_b = Price::new(b, precision);
1920 let expected = p_a.raw
1921 .checked_sub(p_b.raw)
1922 .filter(|r| (PRICE_RAW_MIN..=PRICE_RAW_MAX).contains(r))
1923 .filter(|_| !p_a.is_sentinel() && !p_b.is_sentinel())
1924 .map(|raw| Price { raw, precision: p_a.precision.max(p_b.precision) });
1925 prop_assert_eq!(p_a.checked_sub(p_b), expected);
1926 }
1927 }
1928
1929 proptest! {
1930 #[rstest]
1932 fn prop_price_as_decimal_preserves_precision(
1933 (precision, raw) in valid_precision_raw_strategy()
1934 ) {
1935 prop_assume!(decimal_compatible(raw, precision));
1936 let price = Price::from_raw(raw, precision);
1937 let decimal = price.as_decimal();
1938 prop_assert_eq!(decimal.scale(), u32::from(precision));
1939 }
1940
1941 #[rstest]
1943 fn prop_price_as_decimal_matches_display(
1944 value in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() < 1e6),
1945 precision in float_precision_strategy()
1946 ) {
1947 let price = Price::new(value, precision);
1948 prop_assume!(decimal_compatible(price.raw, precision));
1949 let display_str = format!("{price}");
1950 let decimal_str = price.as_decimal().to_string();
1951 prop_assert_eq!(display_str, decimal_str);
1952 }
1953
1954 #[rstest]
1956 fn prop_price_from_decimal_roundtrip(
1957 (precision, raw) in valid_precision_raw_strategy()
1958 ) {
1959 prop_assume!(decimal_compatible(raw, precision));
1960 let original = Price::from_raw(raw, precision);
1961 let decimal = original.as_decimal();
1962 let reconstructed = Price::from_decimal(decimal).unwrap();
1963 prop_assert_eq!(original.raw, reconstructed.raw);
1964 prop_assert_eq!(original.precision, reconstructed.precision);
1965 }
1966
1967 #[rstest]
1969 fn prop_price_from_raw_round_trip(
1970 (precision, raw) in valid_precision_raw_strategy()
1971 ) {
1972 let price = Price::from_raw(raw, precision);
1973 prop_assert_eq!(price.raw, raw);
1974 prop_assert_eq!(price.precision, precision);
1975 }
1976 }
1977}