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 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 * FIXED_SCALAR) as PriceRaw;
97
98#[unsafe(no_mangle)]
107#[allow(unsafe_code)]
108pub static PRICE_RAW_MIN: PriceRaw = (PRICE_MIN * 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
550impl FromStr for Price {
551 type Err = String;
552
553 fn from_str(value: &str) -> Result<Self, Self::Err> {
554 let clean_value = value.replace('_', "");
555
556 let decimal = if clean_value.contains('e') || clean_value.contains('E') {
557 Decimal::from_scientific(&clean_value)
558 .map_err(|e| format!("Error parsing `input` string '{value}' as Decimal: {e}"))?
559 } else {
560 Decimal::from_str(&clean_value)
561 .map_err(|e| format!("Error parsing `input` string '{value}' as Decimal: {e}"))?
562 };
563
564 let precision = decimal.scale() as u8;
566
567 Self::from_decimal_dp(decimal, precision).map_err(|e| e.to_string())
568 }
569}
570
571impl<T: AsRef<str>> From<T> for Price {
572 fn from(value: T) -> Self {
573 Self::from_str(value.as_ref()).expect(FAILED)
574 }
575}
576
577impl From<Price> for f64 {
578 fn from(price: Price) -> Self {
579 price.as_f64()
580 }
581}
582
583impl From<&Price> for f64 {
584 fn from(price: &Price) -> Self {
585 price.as_f64()
586 }
587}
588
589impl From<Price> for Decimal {
590 fn from(value: Price) -> Self {
591 value.as_decimal()
592 }
593}
594
595impl From<&Price> for Decimal {
596 fn from(value: &Price) -> Self {
597 value.as_decimal()
598 }
599}
600
601impl Hash for Price {
602 fn hash<H: Hasher>(&self, state: &mut H) {
603 self.raw.hash(state);
604 }
605}
606
607impl PartialEq for Price {
608 fn eq(&self, other: &Self) -> bool {
609 self.raw == other.raw
610 }
611}
612
613impl PartialOrd for Price {
614 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
615 Some(self.cmp(other))
616 }
617
618 fn lt(&self, other: &Self) -> bool {
619 self.raw.lt(&other.raw)
620 }
621
622 fn le(&self, other: &Self) -> bool {
623 self.raw.le(&other.raw)
624 }
625
626 fn gt(&self, other: &Self) -> bool {
627 self.raw.gt(&other.raw)
628 }
629
630 fn ge(&self, other: &Self) -> bool {
631 self.raw.ge(&other.raw)
632 }
633}
634
635impl Ord for Price {
636 fn cmp(&self, other: &Self) -> Ordering {
637 self.raw.cmp(&other.raw)
638 }
639}
640
641impl Deref for Price {
642 type Target = PriceRaw;
643
644 fn deref(&self) -> &Self::Target {
645 &self.raw
646 }
647}
648
649impl Neg for Price {
650 type Output = Self;
651 fn neg(self) -> Self::Output {
652 if self.raw == PRICE_ERROR || self.raw == PRICE_UNDEF {
654 return self;
655 }
656 Self {
657 raw: -self.raw,
658 precision: self.precision,
659 }
660 }
661}
662
663impl Add for Price {
664 type Output = Self;
665 fn add(self, rhs: Self) -> Self::Output {
666 Self {
667 raw: self
668 .raw
669 .checked_add(rhs.raw)
670 .expect("Overflow occurred when adding `Price`"),
671 precision: self.precision.max(rhs.precision),
672 }
673 }
674}
675
676impl Sub for Price {
677 type Output = Self;
678 fn sub(self, rhs: Self) -> Self::Output {
679 Self {
680 raw: self
681 .raw
682 .checked_sub(rhs.raw)
683 .expect("Underflow occurred when subtracting `Price`"),
684 precision: self.precision.max(rhs.precision),
685 }
686 }
687}
688
689impl Add<Decimal> for Price {
690 type Output = Decimal;
691 fn add(self, rhs: Decimal) -> Self::Output {
692 self.as_decimal() + rhs
693 }
694}
695
696impl Sub<Decimal> for Price {
697 type Output = Decimal;
698 fn sub(self, rhs: Decimal) -> Self::Output {
699 self.as_decimal() - rhs
700 }
701}
702
703impl Mul<Decimal> for Price {
704 type Output = Decimal;
705 fn mul(self, rhs: Decimal) -> Self::Output {
706 self.as_decimal() * rhs
707 }
708}
709
710impl Div<Decimal> for Price {
711 type Output = Decimal;
712 fn div(self, rhs: Decimal) -> Self::Output {
713 self.as_decimal() / rhs
714 }
715}
716
717impl Add<f64> for Price {
718 type Output = f64;
719 fn add(self, rhs: f64) -> Self::Output {
720 self.as_f64() + rhs
721 }
722}
723
724impl Sub<f64> for Price {
725 type Output = f64;
726 fn sub(self, rhs: f64) -> Self::Output {
727 self.as_f64() - rhs
728 }
729}
730
731impl Mul<f64> for Price {
732 type Output = f64;
733 fn mul(self, rhs: f64) -> Self::Output {
734 self.as_f64() * rhs
735 }
736}
737
738impl Div<f64> for Price {
739 type Output = f64;
740 fn div(self, rhs: f64) -> Self::Output {
741 self.as_f64() / rhs
742 }
743}
744
745impl Debug for Price {
746 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
747 if self.precision > crate::types::fixed::MAX_FLOAT_PRECISION {
748 write!(f, "{}({})", stringify!(Price), self.raw)
749 } else {
750 write!(f, "{}({})", stringify!(Price), self.as_decimal())
751 }
752 }
753}
754
755impl Display for Price {
756 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
757 if self.precision > crate::types::fixed::MAX_FLOAT_PRECISION {
758 write!(f, "{}", self.raw)
759 } else {
760 write!(f, "{}", self.as_decimal())
761 }
762 }
763}
764
765impl Serialize for Price {
766 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
767 where
768 S: serde::Serializer,
769 {
770 serializer.serialize_str(&self.to_string())
771 }
772}
773
774impl<'de> Deserialize<'de> for Price {
775 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
776 where
777 D: Deserializer<'de>,
778 {
779 let price_str: &str = Deserialize::deserialize(deserializer)?;
780 let price: Self = price_str.into();
781 Ok(price)
782 }
783}
784
785pub fn check_positive_price(value: Price, param: &str) -> CorrectnessResult<()> {
791 if value.raw == PRICE_UNDEF {
792 return Err(CorrectnessError::InvalidValue {
793 param: param.to_string(),
794 value: "PRICE_UNDEF".to_string(),
795 type_name: "`Price`",
796 });
797 }
798
799 if !value.is_positive() {
800 return Err(CorrectnessError::NotPositive {
801 param: param.to_string(),
802 value: value.to_string(),
803 type_name: "`Price`",
804 });
805 }
806 Ok(())
807}
808
809#[cfg(feature = "high-precision")]
810#[must_use]
813pub fn decode_raw_price_i64(value: i64) -> PriceRaw {
814 PriceRaw::from(value) * PRECISION_DIFF_SCALAR as PriceRaw
815}
816
817#[cfg(not(feature = "high-precision"))]
818#[must_use]
819pub fn decode_raw_price_i64(value: i64) -> PriceRaw {
820 value
821}
822
823#[cfg(test)]
824mod tests {
825 use nautilus_core::{approx_eq, correctness::CorrectnessError};
826 use rstest::rstest;
827 use rust_decimal_macros::dec;
828
829 use super::*;
830
831 #[rstest]
832 #[cfg(all(not(feature = "defi"), not(feature = "high-precision")))]
833 #[should_panic(expected = "`precision` exceeded maximum `FIXED_PRECISION` (9), was 50")]
834 fn test_invalid_precision_new() {
835 let _ = Price::new(1.0, 50);
837 }
838
839 #[rstest]
840 #[cfg(all(not(feature = "defi"), feature = "high-precision"))]
841 #[should_panic(expected = "`precision` exceeded maximum `FIXED_PRECISION` (16), was 50")]
842 fn test_invalid_precision_new() {
843 let _ = Price::new(1.0, 50);
845 }
846
847 #[rstest]
848 #[cfg(not(feature = "defi"))]
849 #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
850 fn test_invalid_precision_from_raw() {
851 let _ = Price::from_raw(1, FIXED_PRECISION + 1);
853 }
854
855 #[rstest]
856 #[cfg(not(feature = "defi"))]
857 #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
858 fn test_invalid_precision_max() {
859 let _ = Price::max(FIXED_PRECISION + 1);
861 }
862
863 #[rstest]
864 #[cfg(not(feature = "defi"))]
865 #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
866 fn test_invalid_precision_min() {
867 let _ = Price::min(FIXED_PRECISION + 1);
869 }
870
871 #[rstest]
872 #[cfg(not(feature = "defi"))]
873 #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
874 fn test_invalid_precision_zero() {
875 let _ = Price::zero(FIXED_PRECISION + 1);
877 }
878
879 #[rstest]
880 #[should_panic(expected = "Condition failed: invalid f64 for 'value' not in range")]
881 fn test_max_value_exceeded() {
882 let _ = Price::new(PRICE_MAX + 0.1, FIXED_PRECISION);
883 }
884
885 #[rstest]
886 #[should_panic(expected = "Condition failed: invalid f64 for 'value' not in range")]
887 fn test_min_value_exceeded() {
888 let _ = Price::new(PRICE_MIN - 0.1, FIXED_PRECISION);
889 }
890
891 #[rstest]
892 fn test_is_positive_ok() {
893 let price = Price::new(42.0, 2);
895 assert!(price.is_positive());
896
897 check_positive_price(price, "price").unwrap();
899 }
900
901 #[rstest]
902 fn test_is_positive_rejects_non_positive() {
903 let zero = Price::zero(2);
905 let error = check_positive_price(zero, "price").unwrap_err();
906
907 assert_eq!(
908 error,
909 CorrectnessError::NotPositive {
910 param: "price".to_string(),
911 value: "0.00".to_string(),
912 type_name: "`Price`",
913 }
914 );
915 assert_eq!(
916 error.to_string(),
917 "invalid `Price` for 'price' not positive, was 0.00"
918 );
919 }
920
921 #[rstest]
922 fn test_is_positive_rejects_undefined() {
923 let undef = Price::from_raw(PRICE_UNDEF, 0);
925 let error = check_positive_price(undef, "price").unwrap_err();
926
927 assert_eq!(
928 error,
929 CorrectnessError::InvalidValue {
930 param: "price".to_string(),
931 value: "PRICE_UNDEF".to_string(),
932 type_name: "`Price`",
933 }
934 );
935 assert_eq!(
936 error.to_string(),
937 "invalid `Price` for 'price', was PRICE_UNDEF"
938 );
939 }
940
941 #[rstest]
942 fn test_construction() {
943 let price = Price::new_checked(1.23456, 4);
944 assert!(price.is_ok());
945 let price = price.unwrap();
946 assert_eq!(price.precision, 4);
947 assert!(approx_eq!(f64, price.as_f64(), 1.23456, epsilon = 0.0001));
948 }
949
950 #[rstest]
951 fn test_negative_price_in_range() {
952 let neg_price = Price::new(PRICE_MIN / 2.0, FIXED_PRECISION);
954 assert!(neg_price.raw < 0);
955 }
956
957 #[rstest]
958 fn test_new_checked() {
959 assert!(Price::new_checked(1.0, FIXED_PRECISION).is_ok());
961 assert!(Price::new_checked(f64::NAN, FIXED_PRECISION).is_err());
962 assert!(Price::new_checked(f64::INFINITY, FIXED_PRECISION).is_err());
963 }
964
965 #[rstest]
966 fn test_new_checked_returns_typed_error_with_stable_display() {
967 let error = Price::new_checked(PRICE_MAX + 1.0, FIXED_PRECISION).unwrap_err();
968
969 assert!(matches!(error, CorrectnessError::OutOfRange { .. }));
970 assert_eq!(
971 error.to_string(),
972 format!(
973 "invalid f64 for 'value' not in range [{PRICE_MIN}, {PRICE_MAX}], was {}",
974 PRICE_MAX + 1.0
975 )
976 );
977 }
978
979 #[rstest]
980 fn test_from_raw_checked_returns_typed_error_with_stable_display() {
981 let error = Price::from_raw_checked(PRICE_UNDEF, 3).unwrap_err();
982
983 assert_eq!(
984 error,
985 CorrectnessError::PredicateViolation {
986 message: "`precision` must be 0 when `raw` is PRICE_UNDEF".to_string(),
987 }
988 );
989 assert_eq!(
990 error.to_string(),
991 "`precision` must be 0 when `raw` is PRICE_UNDEF"
992 );
993 }
994
995 #[rstest]
996 fn test_from_raw() {
997 let raw = 100 * FIXED_SCALAR as PriceRaw;
998 let price = Price::from_raw(raw, 2);
999 assert_eq!(price.raw, raw);
1000 assert_eq!(price.precision, 2);
1001 }
1002
1003 #[rstest]
1004 fn test_zero_constructor() {
1005 let zero = Price::zero(3);
1006 assert!(zero.is_zero());
1007 assert_eq!(zero.precision, 3);
1008 }
1009
1010 #[rstest]
1011 fn test_max_constructor() {
1012 let max = Price::max(4);
1013 assert_eq!(max.raw, PRICE_RAW_MAX);
1014 assert_eq!(max.precision, 4);
1015 }
1016
1017 #[rstest]
1018 fn test_min_constructor() {
1019 let min = Price::min(4);
1020 assert_eq!(min.raw, PRICE_RAW_MIN);
1021 assert_eq!(min.precision, 4);
1022 }
1023
1024 #[rstest]
1025 fn test_nan_validation() {
1026 assert!(Price::new_checked(f64::NAN, FIXED_PRECISION).is_err());
1027 }
1028
1029 #[rstest]
1030 fn test_infinity_validation() {
1031 assert!(Price::new_checked(f64::INFINITY, FIXED_PRECISION).is_err());
1032 assert!(Price::new_checked(f64::NEG_INFINITY, FIXED_PRECISION).is_err());
1033 }
1034
1035 #[rstest]
1036 fn test_special_values() {
1037 let zero = Price::zero(5);
1038 assert!(zero.is_zero());
1039 assert_eq!(zero.to_string(), "0.00000");
1040
1041 let undef = Price::from_raw(PRICE_UNDEF, 0);
1042 assert!(undef.is_undefined());
1043
1044 let error = ERROR_PRICE;
1045 assert_eq!(error.precision, 255);
1046 }
1047
1048 #[rstest]
1049 fn test_string_parsing() {
1050 let price: Price = "123.456".into();
1051 assert_eq!(price.precision, 3);
1052 assert_eq!(price, Price::from("123.456"));
1053 }
1054
1055 #[rstest]
1056 fn test_negative_price_from_str() {
1057 let price: Price = "-123.45".parse().unwrap();
1058 assert_eq!(price.precision, 2);
1059 assert!(approx_eq!(f64, price.as_f64(), -123.45, epsilon = 1e-9));
1060 }
1061
1062 #[rstest]
1063 fn test_string_parsing_errors() {
1064 assert!(Price::from_str("invalid").is_err());
1065 }
1066
1067 #[rstest]
1068 #[case("1e7", 0, 10_000_000.0)]
1069 #[case("1.5e3", 0, 1_500.0)]
1070 #[case("1.234e-2", 5, 0.01234)]
1071 #[case("5E-3", 3, 0.005)]
1072 fn test_from_str_scientific_notation(
1073 #[case] input: &str,
1074 #[case] expected_precision: u8,
1075 #[case] expected_value: f64,
1076 ) {
1077 let price = Price::from_str(input).unwrap();
1078 assert_eq!(price.precision, expected_precision);
1079 assert!(approx_eq!(
1080 f64,
1081 price.as_f64(),
1082 expected_value,
1083 epsilon = 1e-10
1084 ));
1085 }
1086
1087 #[rstest]
1088 #[case("1_234.56", 2, 1234.56)]
1089 #[case("1000000", 0, 1_000_000.0)]
1090 #[case("99_999.999_99", 5, 99_999.999_99)]
1091 fn test_from_str_with_underscores(
1092 #[case] input: &str,
1093 #[case] expected_precision: u8,
1094 #[case] expected_value: f64,
1095 ) {
1096 let price = Price::from_str(input).unwrap();
1097 assert_eq!(price.precision, expected_precision);
1098 assert!(approx_eq!(
1099 f64,
1100 price.as_f64(),
1101 expected_value,
1102 epsilon = 1e-10
1103 ));
1104 }
1105
1106 #[rstest]
1107 fn test_from_decimal_dp_preservation() {
1108 let decimal = dec!(123.456789);
1110 let price = Price::from_decimal_dp(decimal, 6).unwrap();
1111 assert_eq!(price.precision, 6);
1112 assert!(approx_eq!(
1113 f64,
1114 price.as_f64(),
1115 123.456_789,
1116 epsilon = 1e-10
1117 ));
1118
1119 let expected_raw = 123_456_789 * 10_i64.pow(u32::from(FIXED_PRECISION - 6));
1121 assert_eq!(price.raw, PriceRaw::from(expected_raw));
1122 }
1123
1124 #[rstest]
1125 fn test_from_decimal_dp_rounding() {
1126 let decimal = dec!(1.005);
1128 let price = Price::from_decimal_dp(decimal, 2).unwrap();
1129 assert_eq!(price.as_f64(), 1.0); let decimal = dec!(1.015);
1132 let price = Price::from_decimal_dp(decimal, 2).unwrap();
1133 assert_eq!(price.as_f64(), 1.02); }
1135
1136 #[rstest]
1137 fn test_from_decimal_infers_precision() {
1138 let decimal = dec!(123.456);
1140 let price = Price::from_decimal(decimal).unwrap();
1141 assert_eq!(price.precision, 3);
1142 assert!(approx_eq!(f64, price.as_f64(), 123.456, epsilon = 1e-10));
1143
1144 let decimal = dec!(100);
1146 let price = Price::from_decimal(decimal).unwrap();
1147 assert_eq!(price.precision, 0);
1148 assert_eq!(price.as_f64(), 100.0);
1149
1150 let decimal = dec!(1.23456789);
1152 let price = Price::from_decimal(decimal).unwrap();
1153 assert_eq!(price.precision, 8);
1154 assert!(approx_eq!(
1155 f64,
1156 price.as_f64(),
1157 1.234_567_89,
1158 epsilon = 1e-10
1159 ));
1160 }
1161
1162 #[rstest]
1163 fn test_from_decimal_trailing_zeros() {
1164 let decimal = dec!(1.230);
1166 assert_eq!(decimal.scale(), 3); let price = Price::from_decimal(decimal).unwrap();
1170 assert_eq!(price.precision, 3);
1171 assert!(approx_eq!(f64, price.as_f64(), 1.23, epsilon = 1e-10));
1172
1173 let normalized = decimal.normalize();
1175 assert_eq!(normalized.scale(), 2);
1176 let price_normalized = Price::from_decimal(normalized).unwrap();
1177 assert_eq!(price_normalized.precision, 2);
1178 }
1179
1180 #[rstest]
1181 #[case("1.00", 2)]
1182 #[case("1.0", 1)]
1183 #[case("1.000", 3)]
1184 #[case("100.00", 2)]
1185 #[case("0.10", 2)]
1186 #[case("0.100", 3)]
1187 fn test_from_str_preserves_trailing_zeros(#[case] input: &str, #[case] expected_precision: u8) {
1188 let price = Price::from_str(input).unwrap();
1189 assert_eq!(price.precision, expected_precision);
1190 }
1191
1192 #[rstest]
1193 fn test_from_decimal_excessive_precision_inference() {
1194 let decimal = dec!(1.1234567890123456789012345678);
1197
1198 if decimal.scale() > u32::from(FIXED_PRECISION) {
1200 assert!(Price::from_decimal(decimal).is_err());
1201 }
1202 }
1203
1204 #[rstest]
1205 fn test_from_decimal_dp_out_of_range_returns_typed_error_with_stable_display() {
1206 let huge = Decimal::from_str("99999999999999999999.99").unwrap();
1207 let error = Price::from_decimal_dp(huge, 2).unwrap_err();
1208 match error {
1209 CorrectnessError::PredicateViolation { ref message } => {
1210 assert!(
1211 message.contains("PriceRaw range") || message.contains("for Price"),
1212 "unexpected message: {message:?}",
1213 );
1214 }
1215 _ => panic!("expected PredicateViolation, was {error:?}"),
1216 }
1217 }
1218
1219 #[rstest]
1220 fn test_from_decimal_negative_price() {
1221 let decimal = dec!(-123.45);
1223 let price = Price::from_decimal(decimal).unwrap();
1224 assert_eq!(price.precision, 2);
1225 assert!(approx_eq!(f64, price.as_f64(), -123.45, epsilon = 1e-10));
1226 assert!(price.raw < 0);
1227 }
1228
1229 #[rstest]
1230 fn test_string_formatting() {
1231 assert_eq!(format!("{}", Price::new(1234.5678, 4)), "1234.5678");
1232 assert_eq!(
1233 format!("{:?}", Price::new(1234.5678, 4)),
1234 "Price(1234.5678)"
1235 );
1236 assert_eq!(Price::new(1234.5678, 4).to_formatted_string(), "1_234.5678");
1237 }
1238
1239 #[rstest]
1240 #[case(1234.5678, 4, "Price(1234.5678)", "1234.5678")] #[case(123.456_789_012_345, 8, "Price(123.45678901)", "123.45678901")] #[cfg_attr(
1243 feature = "defi",
1244 case(
1245 2_000_000_000_000_000_000.0,
1246 18,
1247 "Price(2000000000000000000)",
1248 "2000000000000000000"
1249 )
1250 )] fn test_string_formatting_precision_handling(
1252 #[case] value: f64,
1253 #[case] precision: u8,
1254 #[case] expected_debug: &str,
1255 #[case] expected_display: &str,
1256 ) {
1257 let price = if precision > crate::types::fixed::MAX_FLOAT_PRECISION {
1258 Price::from_raw(value as PriceRaw, precision)
1259 } else {
1260 Price::new(value, precision)
1261 };
1262
1263 assert_eq!(format!("{price:?}"), expected_debug);
1264 assert_eq!(format!("{price}"), expected_display);
1265 assert_eq!(
1266 price.to_formatted_string().replace('_', ""),
1267 expected_display
1268 );
1269 }
1270
1271 #[rstest]
1272 fn test_decimal_conversions() {
1273 let price = Price::new(123.456, 3);
1274 assert_eq!(price.as_decimal(), dec!(123.456));
1275
1276 let price = Price::new(0.000_001, 6);
1277 assert_eq!(price.as_decimal(), dec!(0.000001));
1278 }
1279
1280 #[rstest]
1281 fn test_basic_arithmetic() {
1282 let p1 = Price::new(10.5, 2);
1283 let p2 = Price::new(5.25, 2);
1284 assert_eq!(p1 + p2, Price::from("15.75"));
1285 assert_eq!(p1 - p2, Price::from("5.25"));
1286 assert_eq!(-p1, Price::from("-10.5"));
1287 }
1288
1289 #[rstest]
1290 fn test_price_checked_add_within_bounds() {
1291 let a = Price::new(10.0, 2);
1292 let b = Price::new(5.0, 2);
1293 assert_eq!(a.checked_add(b), Some(Price::new(15.0, 2)));
1294
1295 let neg = Price::new(-3.0, 2);
1296 assert_eq!(a.checked_add(neg), Some(Price::new(7.0, 2)));
1297 }
1298
1299 #[rstest]
1300 fn test_price_checked_add_above_max_returns_none() {
1301 let near_max = Price::from_raw(PRICE_RAW_MAX, 0);
1302 let one = Price::new(1.0, 0);
1303 assert_eq!(near_max.checked_add(one), None);
1304 }
1305
1306 #[rstest]
1307 fn test_price_checked_sub_within_bounds() {
1308 let a = Price::new(10.0, 2);
1309 let b = Price::new(3.0, 2);
1310 assert_eq!(a.checked_sub(b), Some(Price::new(7.0, 2)));
1311 assert_eq!(b.checked_sub(a), Some(Price::new(-7.0, 2)));
1312 }
1313
1314 #[rstest]
1315 fn test_price_checked_sub_below_min_returns_none() {
1316 let near_min = Price::from_raw(PRICE_RAW_MIN, 0);
1317 let one = Price::new(1.0, 0);
1318 assert_eq!(near_min.checked_sub(one), None);
1319 }
1320
1321 #[rstest]
1322 fn test_price_checked_arith_uses_max_precision() {
1323 let a = Price::new(10.5, 1);
1324 let b = Price::new(5.25, 2);
1325 let sum = a.checked_add(b).unwrap();
1326 assert_eq!(sum.precision, 2);
1327 assert_eq!(sum.as_f64(), 15.75);
1328 }
1329
1330 #[rstest]
1331 fn test_price_checked_add_rejects_sentinel_undef() {
1332 let undef = Price::from_raw(PRICE_UNDEF, 0);
1333 let one = Price::new(1.0, 0);
1334 assert_eq!(undef.checked_add(one), None);
1335 assert_eq!(one.checked_add(undef), None);
1336 }
1337
1338 #[rstest]
1339 fn test_price_checked_sub_rejects_sentinel_undef() {
1340 let undef = Price::from_raw(PRICE_UNDEF, 0);
1341 let neg_one = Price::new(-1.0, 0);
1342 assert_eq!(undef.checked_sub(neg_one), None);
1343 }
1344
1345 #[rstest]
1346 fn test_price_checked_arith_rejects_error_price() {
1347 let one = Price::new(1.0, 0);
1348 assert_eq!(ERROR_PRICE.checked_add(one), None);
1349 assert_eq!(one.checked_sub(ERROR_PRICE), None);
1350 }
1351
1352 #[rstest]
1353 fn test_price_checked_arith_rejects_raw_error() {
1354 let error = Price::from_raw(PRICE_ERROR, 0);
1355 let one = Price::new(1.0, 0);
1356 assert_eq!(error.checked_add(one), None);
1357 assert_eq!(one.checked_add(error), None);
1358 assert_eq!(error.checked_sub(one), None);
1359 assert_eq!(one.checked_sub(error), None);
1360 }
1361
1362 #[rstest]
1363 fn test_price_checked_add_at_exact_max_returns_some() {
1364 let near_max = Price::from_raw(PRICE_RAW_MAX - 1, 0);
1365 let one_unit = Price::from_raw(1, 0);
1366 assert_eq!(
1367 near_max.checked_add(one_unit),
1368 Some(Price::from_raw(PRICE_RAW_MAX, 0)),
1369 );
1370 }
1371
1372 #[rstest]
1373 fn test_price_checked_sub_at_exact_min_returns_some() {
1374 let near_min = Price::from_raw(PRICE_RAW_MIN + 1, 0);
1375 let one_unit = Price::from_raw(1, 0);
1376 assert_eq!(
1377 near_min.checked_sub(one_unit),
1378 Some(Price::from_raw(PRICE_RAW_MIN, 0)),
1379 );
1380 }
1381
1382 #[rstest]
1383 fn test_mixed_precision_add() {
1384 let p1 = Price::new(10.5, 1);
1385 let p2 = Price::new(5.25, 2);
1386 let result = p1 + p2;
1387 assert_eq!(result.precision, 2);
1388 assert_eq!(result.as_f64(), 15.75);
1389 }
1390
1391 #[rstest]
1392 fn test_mixed_precision_sub() {
1393 let p1 = Price::new(10.5, 1);
1394 let p2 = Price::new(5.25, 2);
1395 let result = p1 - p2;
1396 assert_eq!(result.precision, 2);
1397 assert_eq!(result.as_f64(), 5.25);
1398 }
1399
1400 #[rstest]
1401 fn test_f64_operations() {
1402 let p = Price::new(10.5, 2);
1403 assert_eq!(p + 1.0, 11.5);
1404 assert_eq!(p - 1.0, 9.5);
1405 assert_eq!(p * 2.0, 21.0);
1406 assert_eq!(p / 2.0, 5.25);
1407 }
1408
1409 #[rstest]
1410 fn test_equality_and_comparisons() {
1411 let p1 = Price::new(10.0, 1);
1412 let p2 = Price::new(20.0, 1);
1413 let p3 = Price::new(10.0, 1);
1414
1415 assert!(p1 < p2);
1416 assert!(p2 > p1);
1417 assert!(p1 <= p3);
1418 assert!(p1 >= p3);
1419 assert_eq!(p1, p3);
1420 assert_ne!(p1, p2);
1421
1422 assert_eq!(Price::from("1.0"), Price::from("1.0"));
1423 assert_ne!(Price::from("1.1"), Price::from("1.0"));
1424 assert!(Price::from("1.0") <= Price::from("1.0"));
1425 assert!(Price::from("1.1") > Price::from("1.0"));
1426 assert!(Price::from("1.0") >= Price::from("1.0"));
1427 assert!(Price::from("1.0") >= Price::from("1.0"));
1428 assert!(Price::from("1.0") >= Price::from("1.0"));
1429 assert!(Price::from("0.9") < Price::from("1.0"));
1430 assert!(Price::from("0.9") <= Price::from("1.0"));
1431 assert!(Price::from("0.9") <= Price::from("1.0"));
1432 }
1433
1434 #[rstest]
1435 fn test_deref() {
1436 let price = Price::new(10.0, 1);
1437 assert_eq!(*price, price.raw);
1438 }
1439
1440 #[rstest]
1441 fn test_decode_raw_price_i64() {
1442 let raw_scaled_by_1e9 = 42_000_000_000i64; let decoded = decode_raw_price_i64(raw_scaled_by_1e9);
1444 let price = Price::from_raw(decoded, FIXED_PRECISION);
1445 assert!(
1446 approx_eq!(f64, price.as_f64(), 42.0, epsilon = 1e-9),
1447 "Expected 42.0 f64, was {} (precision = {})",
1448 price.as_f64(),
1449 price.precision
1450 );
1451 }
1452
1453 #[rstest]
1454 fn test_hash() {
1455 use std::{
1456 collections::hash_map::DefaultHasher,
1457 hash::{Hash, Hasher},
1458 };
1459
1460 let price1 = Price::new(1.0, 2);
1461 let price2 = Price::new(1.0, 2);
1462 let price3 = Price::new(1.1, 2);
1463
1464 let mut hasher1 = DefaultHasher::new();
1465 let mut hasher2 = DefaultHasher::new();
1466 let mut hasher3 = DefaultHasher::new();
1467
1468 price1.hash(&mut hasher1);
1469 price2.hash(&mut hasher2);
1470 price3.hash(&mut hasher3);
1471
1472 assert_eq!(hasher1.finish(), hasher2.finish());
1473 assert_ne!(hasher1.finish(), hasher3.finish());
1474 }
1475
1476 #[rstest]
1477 fn test_price_serde_json_round_trip() {
1478 let price = Price::new(1.0500, 4);
1479 let json = serde_json::to_string(&price).unwrap();
1480 let deserialized: Price = serde_json::from_str(&json).unwrap();
1481 assert_eq!(deserialized, price);
1482 }
1483
1484 #[rstest]
1485 fn test_from_mantissa_exponent_exact_precision() {
1486 let price = Price::from_mantissa_exponent(12345, -2, 2);
1487 assert_eq!(price.as_f64(), 123.45);
1488 }
1489
1490 #[rstest]
1491 fn test_from_mantissa_exponent_excess_rounds_down() {
1492 let price = Price::from_mantissa_exponent(12345, -3, 2);
1494 assert_eq!(price.as_f64(), 12.34);
1495 }
1496
1497 #[rstest]
1498 fn test_from_mantissa_exponent_excess_rounds_up() {
1499 let price = Price::from_mantissa_exponent(12355, -3, 2);
1501 assert_eq!(price.as_f64(), 12.36);
1502 }
1503
1504 #[rstest]
1505 fn test_from_mantissa_exponent_positive_exponent() {
1506 let price = Price::from_mantissa_exponent(5, 2, 0);
1507 assert_eq!(price.as_f64(), 500.0);
1508 }
1509
1510 #[rstest]
1511 fn test_from_mantissa_exponent_negative_mantissa() {
1512 let price = Price::from_mantissa_exponent(-12345, -2, 2);
1513 assert_eq!(price.as_f64(), -123.45);
1514 }
1515
1516 #[rstest]
1517 fn test_from_mantissa_exponent_zero() {
1518 let price = Price::from_mantissa_exponent(0, 2, 2);
1519 assert_eq!(price.as_f64(), 0.0);
1520 }
1521
1522 #[rstest]
1523 #[should_panic(expected = "Overflow")]
1524 fn test_from_mantissa_exponent_overflow_panics() {
1525 let _ = Price::from_mantissa_exponent(i64::MAX, 9, 0);
1526 }
1527
1528 #[rstest]
1529 #[should_panic(expected = "exceeds i128 range")]
1530 fn test_from_mantissa_exponent_large_exponent_panics() {
1531 let _ = Price::from_mantissa_exponent(1, 119, 0);
1532 }
1533
1534 #[rstest]
1535 fn test_from_mantissa_exponent_zero_with_large_exponent() {
1536 let price = Price::from_mantissa_exponent(0, 119, 0);
1537 assert_eq!(price.as_f64(), 0.0);
1538 }
1539
1540 #[rstest]
1541 fn test_from_mantissa_exponent_very_negative_exponent_rounds_to_zero() {
1542 let price = Price::from_mantissa_exponent(12345, -120, 2);
1543 assert_eq!(price.as_f64(), 0.0);
1544 }
1545
1546 #[rstest]
1547 fn test_decimal_arithmetic_operations() {
1548 let price = Price::new(100.0, 2);
1549 assert_eq!(price + dec!(50.25), dec!(150.25));
1550 assert_eq!(price - dec!(30.50), dec!(69.50));
1551 assert_eq!(price * dec!(1.5), dec!(150.00));
1552 assert_eq!(price / dec!(4), dec!(25.00));
1553 }
1554}
1555
1556#[cfg(test)]
1557mod property_tests {
1558 use proptest::prelude::*;
1559 use rstest::rstest;
1560
1561 use super::*;
1562
1563 fn price_value_strategy() -> impl Strategy<Value = f64> {
1565 prop_oneof![
1568 0.00001..1.0,
1570 1.0..100_000.0,
1572 100_000.0..1_000_000.0,
1574 -1_000.0..0.0,
1576 Just(PRICE_MIN / 2.0),
1578 Just(PRICE_MAX / 2.0),
1579 ]
1580 }
1581
1582 fn float_precision_upper_bound() -> u8 {
1583 FIXED_PRECISION.min(crate::types::fixed::MAX_FLOAT_PRECISION)
1584 }
1585
1586 fn precision_strategy() -> impl Strategy<Value = u8> {
1588 let upper = float_precision_upper_bound();
1589 prop_oneof![Just(0u8), 0u8..=upper, Just(FIXED_PRECISION),]
1590 }
1591
1592 fn precision_strategy_non_zero() -> impl Strategy<Value = u8> {
1593 let upper = float_precision_upper_bound().max(1);
1594 prop_oneof![Just(upper), Just(FIXED_PRECISION.max(1)), 1u8..=upper,]
1595 }
1596
1597 fn valid_precision_raw_strategy() -> impl Strategy<Value = (u8, PriceRaw)> {
1601 precision_strategy().prop_flat_map(|precision| {
1602 let scale: PriceRaw = if precision >= FIXED_PRECISION {
1603 1
1604 } else {
1605 (10 as PriceRaw).pow(u32::from(FIXED_PRECISION - precision))
1606 };
1607 let max_base = PRICE_RAW_MAX / scale;
1609 let min_base = PRICE_RAW_MIN / scale;
1610 (min_base..=max_base).prop_map(move |base| (precision, base * scale))
1611 })
1612 }
1613
1614 fn float_precision_strategy() -> impl Strategy<Value = u8> {
1616 precision_strategy()
1617 }
1618
1619 const DECIMAL_MAX_MANTISSA: i128 = 79_228_162_514_264_337_593_543_950_335;
1620
1621 #[expect(
1622 clippy::useless_conversion,
1623 reason = "PriceRaw is i64 or i128 depending on feature"
1624 )]
1625 fn decimal_compatible(raw: PriceRaw, precision: u8) -> bool {
1626 if precision > crate::types::fixed::MAX_FLOAT_PRECISION {
1627 return false;
1628 }
1629 let precision_diff = u32::from(FIXED_PRECISION.saturating_sub(precision));
1630 let divisor = (10 as PriceRaw).pow(precision_diff);
1631 let rescaled_raw = raw / divisor;
1632 i128::from(rescaled_raw.abs()) <= DECIMAL_MAX_MANTISSA
1633 }
1634
1635 proptest! {
1636 #[rstest]
1638 fn prop_price_serde_round_trip(
1639 value in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() < 1e6),
1640 precision in precision_strategy()
1641 ) {
1642 let original = Price::new(value, precision);
1643
1644 let string_repr = original.to_string();
1646 let from_string: Price = string_repr.parse().unwrap();
1647 prop_assert_eq!(from_string.raw, original.raw);
1648 prop_assert_eq!(from_string.precision, original.precision);
1649
1650 let json = serde_json::to_string(&original).unwrap();
1652 let from_json: Price = serde_json::from_str(&json).unwrap();
1653 prop_assert_eq!(from_json.precision, original.precision);
1654 }
1656
1657 #[rstest]
1659 fn prop_price_arithmetic_associative(
1660 a in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() > 1e-3 && x.abs() < 1e6),
1661 b in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() > 1e-3 && x.abs() < 1e6),
1662 c in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() > 1e-3 && x.abs() < 1e6),
1663 precision in precision_strategy()
1664 ) {
1665 let p_a = Price::new(a, precision);
1666 let p_b = Price::new(b, precision);
1667 let p_c = Price::new(c, precision);
1668
1669 let ab_raw = p_a.raw.checked_add(p_b.raw);
1671 let bc_raw = p_b.raw.checked_add(p_c.raw);
1672
1673 if let (Some(ab_raw), Some(bc_raw)) = (ab_raw, bc_raw) {
1674 let ab_c_raw = ab_raw.checked_add(p_c.raw);
1675 let a_bc_raw = p_a.raw.checked_add(bc_raw);
1676
1677 if let (Some(ab_c_raw), Some(a_bc_raw)) = (ab_c_raw, a_bc_raw) {
1678 prop_assert_eq!(ab_c_raw, a_bc_raw, "Associativity failed in raw arithmetic");
1680 }
1681 }
1682 }
1683
1684 #[rstest]
1686 fn prop_price_addition_subtraction_inverse(
1687 base in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() < 1e6),
1688 delta in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() > 1e-3 && x.abs() < 1e6),
1689 precision in precision_strategy()
1690 ) {
1691 let p_base = Price::new(base, precision);
1692 let p_delta = Price::new(delta, precision);
1693
1694 if let Some(added_raw) = p_base.raw.checked_add(p_delta.raw)
1696 && let Some(result_raw) = added_raw.checked_sub(p_delta.raw) {
1697 prop_assert_eq!(result_raw, p_base.raw, "Inverse operation failed in raw arithmetic");
1699 }
1700 }
1701
1702 #[rstest]
1704 fn prop_price_ordering_transitive(
1705 a in price_value_strategy(),
1706 b in price_value_strategy(),
1707 c in price_value_strategy(),
1708 precision in float_precision_strategy()
1709 ) {
1710 let p_a = Price::new(a, precision);
1711 let p_b = Price::new(b, precision);
1712 let p_c = Price::new(c, precision);
1713
1714 if p_a <= p_b && p_b <= p_c {
1716 prop_assert!(p_a <= p_c, "Transitivity failed: {} <= {} <= {} but {} > {}",
1717 p_a.as_f64(), p_b.as_f64(), p_c.as_f64(), p_a.as_f64(), p_c.as_f64());
1718 }
1719 }
1720
1721 #[rstest]
1723 fn prop_price_string_parsing_precision(
1724 integral in 0u32..1_000_000,
1725 fractional in 0u32..1_000_000,
1726 precision in precision_strategy_non_zero()
1727 ) {
1728 let pow = 10u128.pow(u32::from(precision));
1730 let fractional_mod = u128::from(fractional) % pow;
1731 let fractional_str = format!("{:0width$}", fractional_mod, width = precision as usize);
1732 let price_str = format!("{integral}.{fractional_str}");
1733
1734 let parsed: Price = price_str.parse().unwrap();
1735 prop_assert_eq!(parsed.precision, precision);
1736
1737 let round_trip = parsed.to_string();
1739 let expected_value = format!("{integral}.{fractional_str}");
1740 prop_assert_eq!(round_trip, expected_value);
1741 }
1742
1743 #[rstest]
1745 fn prop_price_precision_information_preservation(
1746 value in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() < 1e6),
1747 precision1 in precision_strategy_non_zero(),
1748 precision2 in precision_strategy_non_zero()
1749 ) {
1750 prop_assume!(precision1 != precision2);
1752
1753 let _p1 = Price::new(value, precision1);
1754 let _p2 = Price::new(value, precision2);
1755
1756 let min_precision = precision1.min(precision2);
1759
1760 let scale = 10.0_f64.powi(i32::from(min_precision));
1762 let rounded_value = (value * scale).round() / scale;
1763
1764 let p1_reduced = Price::new(rounded_value, min_precision);
1765 let p2_reduced = Price::new(rounded_value, min_precision);
1766
1767 prop_assert_eq!(p1_reduced.raw, p2_reduced.raw, "Precision reduction inconsistent");
1769 }
1770
1771 #[rstest]
1773 fn prop_price_arithmetic_bounds(
1774 a in price_value_strategy(),
1775 b in price_value_strategy(),
1776 precision in float_precision_strategy()
1777 ) {
1778 let p_a = Price::new(a, precision);
1779 let p_b = Price::new(b, precision);
1780
1781 let sum_f64 = p_a.as_f64() + p_b.as_f64();
1783 if sum_f64.is_finite() && (PRICE_MIN..=PRICE_MAX).contains(&sum_f64) {
1784 let sum = p_a + p_b;
1785 prop_assert!(sum.as_f64().is_finite());
1786 prop_assert!(!sum.is_undefined());
1787 }
1788
1789 let diff_f64 = p_a.as_f64() - p_b.as_f64();
1791 if diff_f64.is_finite() && (PRICE_MIN..=PRICE_MAX).contains(&diff_f64) {
1792 let diff = p_a - p_b;
1793 prop_assert!(diff.as_f64().is_finite());
1794 prop_assert!(!diff.is_undefined());
1795 }
1796 }
1797
1798 #[rstest]
1801 fn prop_price_checked_add_matches_spec(
1802 a in price_value_strategy(),
1803 b in price_value_strategy(),
1804 precision in float_precision_strategy()
1805 ) {
1806 let p_a = Price::new(a, precision);
1807 let p_b = Price::new(b, precision);
1808 let expected = p_a.raw
1809 .checked_add(p_b.raw)
1810 .filter(|r| (PRICE_RAW_MIN..=PRICE_RAW_MAX).contains(r))
1811 .filter(|_| !p_a.is_sentinel() && !p_b.is_sentinel())
1812 .map(|raw| Price { raw, precision: p_a.precision.max(p_b.precision) });
1813 prop_assert_eq!(p_a.checked_add(p_b), expected);
1814 }
1815
1816 #[rstest]
1819 fn prop_price_checked_sub_matches_spec(
1820 a in price_value_strategy(),
1821 b in price_value_strategy(),
1822 precision in float_precision_strategy()
1823 ) {
1824 let p_a = Price::new(a, precision);
1825 let p_b = Price::new(b, precision);
1826 let expected = p_a.raw
1827 .checked_sub(p_b.raw)
1828 .filter(|r| (PRICE_RAW_MIN..=PRICE_RAW_MAX).contains(r))
1829 .filter(|_| !p_a.is_sentinel() && !p_b.is_sentinel())
1830 .map(|raw| Price { raw, precision: p_a.precision.max(p_b.precision) });
1831 prop_assert_eq!(p_a.checked_sub(p_b), expected);
1832 }
1833 }
1834
1835 proptest! {
1836 #[rstest]
1838 fn prop_price_as_decimal_preserves_precision(
1839 (precision, raw) in valid_precision_raw_strategy()
1840 ) {
1841 prop_assume!(decimal_compatible(raw, precision));
1842 let price = Price::from_raw(raw, precision);
1843 let decimal = price.as_decimal();
1844 prop_assert_eq!(decimal.scale(), u32::from(precision));
1845 }
1846
1847 #[rstest]
1849 fn prop_price_as_decimal_matches_display(
1850 value in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() < 1e6),
1851 precision in float_precision_strategy()
1852 ) {
1853 let price = Price::new(value, precision);
1854 prop_assume!(decimal_compatible(price.raw, precision));
1855 let display_str = format!("{price}");
1856 let decimal_str = price.as_decimal().to_string();
1857 prop_assert_eq!(display_str, decimal_str);
1858 }
1859
1860 #[rstest]
1862 fn prop_price_from_decimal_roundtrip(
1863 (precision, raw) in valid_precision_raw_strategy()
1864 ) {
1865 prop_assume!(decimal_compatible(raw, precision));
1866 let original = Price::from_raw(raw, precision);
1867 let decimal = original.as_decimal();
1868 let reconstructed = Price::from_decimal(decimal).unwrap();
1869 prop_assert_eq!(original.raw, reconstructed.raw);
1870 prop_assert_eq!(original.precision, reconstructed.precision);
1871 }
1872
1873 #[rstest]
1875 fn prop_price_from_raw_round_trip(
1876 (precision, raw) in valid_precision_raw_strategy()
1877 ) {
1878 let price = Price::from_raw(raw, precision);
1879 prop_assert_eq!(price.raw, raw);
1880 prop_assert_eq!(price.precision, precision);
1881 }
1882 }
1883}