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