1use std::{cmp::Ordering, fmt::Display};
62
63use nautilus_core::correctness::{
64 CorrectnessError, CorrectnessResult, CorrectnessResultExt, FAILED,
65};
66use rust_decimal::Decimal;
67
68use crate::types::{price::PriceRaw, quantity::QuantityRaw};
69
70#[unsafe(no_mangle)]
78#[allow(unsafe_code)]
79pub static HIGH_PRECISION_MODE: u8 = cfg!(feature = "high-precision") as u8;
80
81#[cfg(feature = "high-precision")]
86pub const FIXED_PRECISION: u8 = 16;
88
89pub const FIXED_PRECISION_STANDARD: u8 = 9;
91
92#[cfg(not(feature = "high-precision"))]
93pub const FIXED_PRECISION: u8 = FIXED_PRECISION_STANDARD;
95
96#[cfg(feature = "high-precision")]
101pub const PRECISION_BYTES: i32 = 16;
103
104#[cfg(not(feature = "high-precision"))]
105pub const PRECISION_BYTES: i32 = 8;
107
108pub const FIXED_DECIMAL: &str = "Decimal128(38, 16)";
110
111#[cfg(feature = "high-precision")]
116pub(crate) const FIXED_SCALAR_RAW: QuantityRaw = 10_000_000_000_000_000;
117
118#[cfg(not(feature = "high-precision"))]
119pub(crate) const FIXED_SCALAR_RAW: QuantityRaw = 1_000_000_000;
120
121#[cfg(feature = "high-precision")]
122pub const FIXED_SCALAR: f64 = 10_000_000_000_000_000.0;
124
125#[cfg(not(feature = "high-precision"))]
126pub const FIXED_SCALAR: f64 = 1_000_000_000.0;
128
129#[cfg(feature = "high-precision")]
134pub const PRECISION_DIFF_SCALAR: f64 = 10_000_000.0; #[cfg(not(feature = "high-precision"))]
138pub const PRECISION_DIFF_SCALAR: f64 = 1.0;
140
141const POWERS_OF_10: [u64; 17] = [
150 1, 10, 100, 1_000, 10_000, 100_000, 1_000_000, 10_000_000, 100_000_000, 1_000_000_000, 10_000_000_000, 100_000_000_000, 1_000_000_000_000, 10_000_000_000_000, 100_000_000_000_000, 1_000_000_000_000_000, 10_000_000_000_000_000, ];
168
169const _: () = assert!(
172 (FIXED_PRECISION as usize) < POWERS_OF_10.len(),
173 "FIXED_PRECISION exceeds POWERS_OF_10 table size"
174);
175
176pub const MAX_FLOAT_PRECISION: u8 = 16;
187
188pub fn check_fixed_precision(precision: u8) -> CorrectnessResult<()> {
196 #[cfg(feature = "defi")]
197 if precision > crate::defi::WEI_PRECISION {
198 return Err(CorrectnessError::PredicateViolation {
199 message: format!("`precision` exceeded maximum `WEI_PRECISION` (18), was {precision}"),
200 });
201 }
202
203 #[cfg(not(feature = "defi"))]
204 if precision > FIXED_PRECISION {
205 return Err(CorrectnessError::PredicateViolation {
206 message: format!(
207 "`precision` exceeded maximum `FIXED_PRECISION` ({FIXED_PRECISION}), was {precision}"
208 ),
209 });
210 }
211
212 Ok(())
213}
214
215pub fn check_float_precision(precision: u8) -> CorrectnessResult<()> {
221 if precision > MAX_FLOAT_PRECISION {
222 return Err(CorrectnessError::PredicateViolation {
223 message: format!(
224 "Fixed-point precision {precision} exceeds maximum float precision {MAX_FLOAT_PRECISION}"
225 ),
226 });
227 }
228
229 Ok(())
230}
231
232#[inline]
243#[must_use]
244pub fn raw_scales_match(a: u8, b: u8) -> bool {
245 a == b || a.max(b) <= FIXED_PRECISION
246}
247
248#[inline]
250#[must_use]
251pub(crate) fn raw_scale(precision: u8) -> u128 {
252 10_u128.pow(u32::from(precision.max(FIXED_PRECISION)))
253}
254
255#[must_use]
257pub(crate) fn canonical_raw(raw: impl Into<u128>, precision: u8) -> (u128, u8) {
258 let mut raw = raw.into();
259 let mut precision = if raw == 0 {
260 FIXED_PRECISION
261 } else {
262 precision.max(FIXED_PRECISION)
263 };
264
265 while precision > FIXED_PRECISION && raw % 10 == 0 {
266 raw /= 10;
267 precision -= 1;
268 }
269
270 (raw, precision)
271}
272
273#[inline]
274#[must_use]
275pub(crate) fn compare_raw_signed(
276 lhs: PriceRaw,
277 lhs_precision: u8,
278 rhs: PriceRaw,
279 rhs_precision: u8,
280) -> Ordering {
281 if raw_scales_match(lhs_precision, rhs_precision) {
282 return lhs.cmp(&rhs);
283 }
284
285 lhs.signum().cmp(&rhs.signum()).then_with(|| {
286 let ordering = compare_raw(
287 lhs.unsigned_abs(),
288 lhs_precision,
289 rhs.unsigned_abs(),
290 rhs_precision,
291 );
292
293 if lhs < 0 {
294 ordering.reverse()
295 } else {
296 ordering
297 }
298 })
299}
300
301#[inline]
302#[must_use]
303pub(crate) fn compare_raw(
304 lhs: impl Into<u128>,
305 lhs_precision: u8,
306 rhs: impl Into<u128>,
307 rhs_precision: u8,
308) -> Ordering {
309 let lhs = lhs.into();
310 let rhs = rhs.into();
311
312 if (lhs == 0 && rhs == 0) || raw_scales_match(lhs_precision, rhs_precision) {
314 return lhs.cmp(&rhs);
315 }
316
317 let lhs_scale = raw_scale(lhs_precision);
318 let rhs_scale = raw_scale(rhs_precision);
319 let scale = lhs_scale.max(rhs_scale);
320
321 (lhs / lhs_scale).cmp(&(rhs / rhs_scale)).then_with(|| {
323 let lhs_fraction = (lhs % lhs_scale) * (scale / lhs_scale);
324 let rhs_fraction = (rhs % rhs_scale) * (scale / rhs_scale);
325 lhs_fraction.cmp(&rhs_fraction)
326 })
327}
328
329#[must_use]
338pub(crate) fn scaled_raw_to_decimal(scaled_raw: i128, precision: u8) -> Decimal {
339 let scale = u32::from(precision);
340
341 Decimal::try_from_i128_with_scale(scaled_raw, scale).unwrap_or_else(|_| {
342 let divisor = 10_i128.pow(scale);
343
344 Decimal::from(scaled_raw / divisor)
345 + Decimal::from_i128_with_scale(scaled_raw % divisor, scale)
346 })
347}
348
349pub(crate) fn format_scaled_i128(raw: i128, precision: u8) -> String {
350 let sign = if raw < 0 { "-" } else { "" };
351 format!(
352 "{sign}{}",
353 format_scaled_u128(raw.unsigned_abs(), precision)
354 )
355}
356
357pub(crate) fn parse_decimal_mantissa(value: &str) -> Result<(i128, u8), String> {
359 let (negative, unsigned) = value
360 .strip_prefix('-')
361 .map_or((false, value), |value| (true, value));
362 let unsigned = if negative {
363 unsigned
364 } else {
365 unsigned.strip_prefix('+').unwrap_or(unsigned)
366 };
367 let (whole, fraction) = unsigned.split_once('.').unwrap_or((unsigned, ""));
368 if fraction.contains('.') {
369 return Err(format!("Invalid decimal value '{value}'"));
370 }
371 let digits = format!("{whole}{fraction}");
372 if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
373 return Err(format!("Invalid decimal value '{value}'"));
374 }
375 let precision = u8::try_from(fraction.len())
376 .map_err(|_| format!("Decimal value '{value}' has too many fractional digits"))?;
377 let mut mantissa = 0_i128;
378 for digit in digits.bytes().map(|byte| i128::from(byte - b'0')) {
379 mantissa = if negative {
380 mantissa
381 .checked_mul(10)
382 .and_then(|value| value.checked_sub(digit))
383 } else {
384 mantissa
385 .checked_mul(10)
386 .and_then(|value| value.checked_add(digit))
387 }
388 .ok_or_else(|| format!("Decimal value '{value}' exceeds i128 range"))?;
389 }
390 Ok((mantissa, precision))
391}
392
393pub(crate) fn format_scaled_u128(raw: u128, precision: u8) -> String {
394 if precision == 0 {
395 return raw.to_string();
396 }
397
398 let scale = 10_u128.pow(u32::from(precision));
399 format!(
400 "{}.{:0>width$}",
401 raw / scale,
402 raw % scale,
403 width = usize::from(precision),
404 )
405}
406
407#[must_use]
411pub(crate) fn checked_mul_div_fixed(lhs: QuantityRaw, rhs: QuantityRaw) -> Option<QuantityRaw> {
412 checked_mul_div_raw(lhs, rhs, FIXED_SCALAR_RAW)
413}
414
415#[must_use]
418pub(crate) fn checked_mul_div_raw(
419 lhs: QuantityRaw,
420 rhs: QuantityRaw,
421 scalar: QuantityRaw,
422) -> Option<QuantityRaw> {
423 let lhs_whole = lhs / scalar;
424 let lhs_remainder = lhs % scalar;
425 let rhs_whole = rhs / scalar;
426 let rhs_remainder = rhs % scalar;
427
428 lhs_whole
429 .checked_mul(rhs)
430 .and_then(|whole| {
431 lhs_remainder
432 .checked_mul(rhs_whole)
433 .and_then(|mixed| whole.checked_add(mixed))
434 })
435 .and_then(|whole_and_mixed| {
436 lhs_remainder
437 .checked_mul(rhs_remainder)
438 .map(|fractional| fractional / scalar)
439 .and_then(|fractional| whole_and_mixed.checked_add(fractional))
440 })
441}
442
443const _: () = {
444 assert!(FIXED_SCALAR_RAW > 0);
445 assert!((FIXED_SCALAR_RAW as f64).to_bits() == FIXED_SCALAR.to_bits());
446 let max_remainder = FIXED_SCALAR_RAW - 1;
447 assert!(max_remainder.checked_mul(max_remainder).is_some());
448};
449
450#[inline(always)]
460fn should_skip_validation(precision: u8) -> bool {
461 #[cfg(not(feature = "defi"))]
462 debug_assert!(
463 precision <= FIXED_PRECISION,
464 "precision {precision} exceeds FIXED_PRECISION {FIXED_PRECISION}: \
465 raw value validation is not possible at this precision"
466 );
467
468 precision >= FIXED_PRECISION
469}
470
471#[cold]
473fn invalid_raw_error(
474 raw: impl Display,
475 precision: u8,
476 remainder: impl Display,
477 scale: impl Display,
478) -> anyhow::Error {
479 anyhow::anyhow!(
480 "Invalid fixed-point raw value {raw} for precision {precision}: \
481 remainder {remainder} when divided by scale {scale}. \
482 Raw value should be a multiple of {scale}. \
483 This indicates data corruption or incorrect precision/scaling upstream"
484 )
485}
486
487#[inline(always)]
517pub fn check_fixed_raw_u128(raw: u128, precision: u8) -> anyhow::Result<()> {
518 if should_skip_validation(precision) {
519 return Ok(());
520 }
521
522 let exp = usize::from(FIXED_PRECISION - precision);
523 let scale = u128::from(POWERS_OF_10[exp]);
524 let remainder = raw % scale;
525
526 if remainder != 0 {
527 return Err(invalid_raw_error(raw, precision, remainder, scale));
528 }
529
530 Ok(())
531}
532
533#[inline(always)]
542pub fn check_fixed_raw_u64(raw: u64, precision: u8) -> anyhow::Result<()> {
543 if should_skip_validation(precision) {
544 return Ok(());
545 }
546
547 let exp = usize::from(FIXED_PRECISION - precision);
548 let scale = POWERS_OF_10[exp];
549 let remainder = raw % scale;
550
551 if remainder != 0 {
552 return Err(invalid_raw_error(raw, precision, remainder, scale));
553 }
554
555 Ok(())
556}
557
558#[inline(always)]
588pub fn check_fixed_raw_i128(raw: i128, precision: u8) -> anyhow::Result<()> {
589 if should_skip_validation(precision) {
590 return Ok(());
591 }
592
593 let exp = usize::from(FIXED_PRECISION - precision);
594 let scale = i128::from(POWERS_OF_10[exp]);
595 let remainder = raw % scale;
596
597 if remainder != 0 {
598 return Err(invalid_raw_error(raw, precision, remainder, scale));
599 }
600
601 Ok(())
602}
603
604#[inline(always)]
613pub fn check_fixed_raw_i64(raw: i64, precision: u8) -> anyhow::Result<()> {
614 if should_skip_validation(precision) {
615 return Ok(());
616 }
617
618 let exp = usize::from(FIXED_PRECISION - precision);
619 let scale = POWERS_OF_10[exp].cast_signed();
620 let remainder = raw % scale;
621
622 if remainder != 0 {
623 return Err(invalid_raw_error(raw, precision, remainder, scale));
624 }
625
626 Ok(())
627}
628
629#[must_use]
646pub fn correct_raw_u128(raw: u128, precision: u8) -> u128 {
647 if precision >= FIXED_PRECISION {
648 return raw;
649 }
650 let exp = usize::from(FIXED_PRECISION - precision);
651 let scale = u128::from(POWERS_OF_10[exp]);
652 let half_scale = scale / 2;
653 let remainder = raw % scale;
654 if remainder == 0 {
655 raw
656 } else if remainder >= half_scale {
657 raw.checked_add(scale - remainder)
658 .unwrap_or(raw - remainder)
659 } else {
660 raw - remainder
661 }
662}
663
664#[must_use]
672pub fn correct_raw_u64(raw: u64, precision: u8) -> u64 {
673 if precision >= FIXED_PRECISION {
674 return raw;
675 }
676 let exp = usize::from(FIXED_PRECISION - precision);
677 let scale = POWERS_OF_10[exp];
678 let half_scale = scale / 2;
679 let remainder = raw % scale;
680 if remainder == 0 {
681 raw
682 } else if remainder >= half_scale {
683 raw.checked_add(scale - remainder)
684 .unwrap_or(raw - remainder)
685 } else {
686 raw - remainder
687 }
688}
689
690#[must_use]
698pub fn correct_raw_i128(raw: i128, precision: u8) -> i128 {
699 if precision >= FIXED_PRECISION {
700 return raw;
701 }
702 let exp = usize::from(FIXED_PRECISION - precision);
703 let scale = i128::from(POWERS_OF_10[exp]);
704 let half_scale = scale / 2;
705 let remainder = raw % scale;
706 if remainder == 0 {
707 raw
708 } else if raw >= 0 {
709 if remainder >= half_scale {
710 raw.checked_add(scale - remainder)
711 .unwrap_or(raw - remainder)
712 } else {
713 raw - remainder
714 }
715 } else {
716 if remainder.abs() >= half_scale {
718 raw.checked_sub(scale + remainder)
719 .unwrap_or(raw - remainder)
720 } else {
721 raw - remainder
722 }
723 }
724}
725
726#[must_use]
734pub fn correct_raw_i64(raw: i64, precision: u8) -> i64 {
735 if precision >= FIXED_PRECISION {
736 return raw;
737 }
738 let exp = usize::from(FIXED_PRECISION - precision);
739 let scale = POWERS_OF_10[exp].cast_signed();
740 let half_scale = scale / 2;
741 let remainder = raw % scale;
742 if remainder == 0 {
743 raw
744 } else if raw >= 0 {
745 if remainder >= half_scale {
746 raw.checked_add(scale - remainder)
747 .unwrap_or(raw - remainder)
748 } else {
749 raw - remainder
750 }
751 } else {
752 if remainder.abs() >= half_scale {
754 raw.checked_sub(scale + remainder)
755 .unwrap_or(raw - remainder)
756 } else {
757 raw - remainder
758 }
759 }
760}
761
762#[must_use]
768#[inline]
769pub fn correct_price_raw(raw: PriceRaw, precision: u8) -> PriceRaw {
770 #[cfg(feature = "high-precision")]
771 {
772 correct_raw_i128(raw, precision)
773 }
774 #[cfg(not(feature = "high-precision"))]
775 {
776 correct_raw_i64(raw, precision)
777 }
778}
779
780#[must_use]
786#[inline]
787pub fn correct_quantity_raw(raw: QuantityRaw, precision: u8) -> QuantityRaw {
788 #[cfg(feature = "high-precision")]
789 {
790 correct_raw_u128(raw, precision)
791 }
792 #[cfg(not(feature = "high-precision"))]
793 {
794 correct_raw_u64(raw, precision)
795 }
796}
797
798#[must_use]
803#[inline]
804pub fn bankers_round(mantissa: i128, excess: u32) -> i128 {
805 if excess == 0 {
806 return mantissa;
807 }
808
809 if excess >= 39 {
811 return 0;
812 }
813
814 let divisor = 10i128.pow(excess);
815 let quotient = mantissa / divisor;
816 let remainder = mantissa % divisor;
817 let half = divisor / 2;
818
819 if remainder.abs() > half || (remainder.abs() == half && quotient % 2 != 0) {
820 quotient + mantissa.signum()
821 } else {
822 quotient
823 }
824}
825
826pub fn mantissa_exponent_to_fixed_i128(
841 mantissa: i128,
842 exponent: i8,
843 precision: u8,
844) -> CorrectnessResult<i128> {
845 check_fixed_precision(precision)?;
846
847 let precision_i16 = i16::from(precision);
848 let target_scale = i16::from(FIXED_PRECISION).max(precision_i16);
849 let frac_digits = -i16::from(exponent);
850
851 let mantissa = if frac_digits > precision_i16 {
852 let excess = u32::from((frac_digits - precision_i16).cast_unsigned());
853 bankers_round(mantissa, excess)
854 } else {
855 mantissa
856 };
857
858 let scale_after_rounding = frac_digits.min(precision_i16);
859 let scale_exp = target_scale - scale_after_rounding;
860 if scale_exp > 38 {
861 return Err(CorrectnessError::PredicateViolation {
862 message: format!(
863 "Exponent {exponent} produces scale factor 10^{scale_exp} which exceeds i128 range"
864 ),
865 });
866 }
867
868 if scale_exp >= 0 {
869 mantissa.checked_mul(10i128.pow(u32::from(scale_exp.cast_unsigned())))
870 } else {
871 Some(mantissa / 10i128.pow(u32::from((-scale_exp).cast_unsigned())))
872 }
873 .ok_or_else(|| CorrectnessError::PredicateViolation {
874 message: "Overflow when scaling mantissa to fixed precision".to_string(),
875 })
876}
877
878pub(crate) fn mantissa_exponent_to_raw_checked<R>(
879 mantissa: i128,
880 exponent: i8,
881 precision: u8,
882 context: &'static str,
883 raw_type_name: &'static str,
884 value_type_name: &'static str,
885) -> CorrectnessResult<R>
886where
887 R: TryFrom<i128>,
888{
889 check_fixed_precision(precision)?;
890
891 let raw_i128 = if mantissa == 0 {
892 0
893 } else {
894 mantissa_exponent_to_fixed_i128(mantissa, exponent, precision).map_err(|_| {
895 CorrectnessError::PredicateViolation {
896 message: format!(
897 "Overflow in {context} (mantissa={mantissa}, exponent={exponent}, precision={precision})"
898 ),
899 }
900 })?
901 };
902
903 raw_i128
904 .try_into()
905 .map_err(|_| CorrectnessError::PredicateViolation {
906 message: format!(
907 "Raw value {raw_i128} exceeds {raw_type_name} range for {value_type_name}"
908 ),
909 })
910}
911
912#[must_use]
929#[expect(
930 clippy::cast_precision_loss,
931 clippy::cast_possible_truncation,
932 reason = "f64 to fixed-point conversion is inherently lossy; callers validate range and finiteness"
933)]
934pub fn f64_to_fixed_i64(value: f64, precision: u8) -> i64 {
935 check_fixed_precision(precision).expect_display(FAILED);
936 let pow1 = 10_i64.pow(u32::from(precision));
937 let pow2 = 10_i64.pow(u32::from(FIXED_PRECISION - precision));
938 let rounded = (value * pow1 as f64).round() as i64;
939 rounded
940 .checked_mul(pow2)
941 .expect("Overflow when scaling f64 to fixed-point i64")
942}
943
944#[must_use]
954#[expect(
955 clippy::cast_precision_loss,
956 clippy::cast_possible_truncation,
957 reason = "f64 to fixed-point conversion is inherently lossy; callers validate range and finiteness"
958)]
959pub fn f64_to_fixed_i128(value: f64, precision: u8) -> i128 {
960 check_fixed_precision(precision).expect_display(FAILED);
961 let pow1 = 10_i128.pow(u32::from(precision));
962 let pow2 = 10_i128.pow(u32::from(FIXED_PRECISION - precision));
963 let rounded = (value * pow1 as f64).round() as i128;
964 rounded
965 .checked_mul(pow2)
966 .expect("Overflow when scaling f64 to fixed-point i128")
967}
968
969#[must_use]
979#[expect(
980 clippy::cast_precision_loss,
981 clippy::cast_possible_truncation,
982 clippy::cast_sign_loss,
983 reason = "f64 to fixed-point conversion is inherently lossy; callers validate range and finiteness"
984)]
985pub fn f64_to_fixed_u64(value: f64, precision: u8) -> u64 {
986 check_fixed_precision(precision).expect_display(FAILED);
987 let pow1 = 10_u64.pow(u32::from(precision));
988 let pow2 = 10_u64.pow(u32::from(FIXED_PRECISION - precision));
989 let rounded = (value * pow1 as f64).round() as u64;
990 rounded
991 .checked_mul(pow2)
992 .expect("Overflow when scaling f64 to fixed-point u64")
993}
994
995#[must_use]
1005#[expect(
1006 clippy::cast_precision_loss,
1007 clippy::cast_possible_truncation,
1008 clippy::cast_sign_loss,
1009 reason = "f64 to fixed-point conversion is inherently lossy; callers validate range and finiteness"
1010)]
1011pub fn f64_to_fixed_u128(value: f64, precision: u8) -> u128 {
1012 check_fixed_precision(precision).expect_display(FAILED);
1013 let pow1 = 10_u128.pow(u32::from(precision));
1014 let pow2 = 10_u128.pow(u32::from(FIXED_PRECISION - precision));
1015 let rounded = (value * pow1 as f64).round() as u128;
1016 rounded
1017 .checked_mul(pow2)
1018 .expect("Overflow when scaling f64 to fixed-point u128")
1019}
1020
1021#[must_use]
1023#[expect(
1024 clippy::cast_precision_loss,
1025 reason = "i64 to f64 is inherently lossy above 2^53; accepted for float interop"
1026)]
1027pub fn fixed_i64_to_f64(value: i64) -> f64 {
1028 (value as f64) / FIXED_SCALAR
1029}
1030
1031#[must_use]
1033#[expect(
1034 clippy::cast_precision_loss,
1035 reason = "i128 to f64 is inherently lossy above 2^53; accepted for float interop"
1036)]
1037pub fn fixed_i128_to_f64(value: i128) -> f64 {
1038 (value as f64) / FIXED_SCALAR
1039}
1040
1041#[must_use]
1043#[expect(
1044 clippy::cast_precision_loss,
1045 reason = "u64 to f64 is inherently lossy above 2^53; accepted for float interop"
1046)]
1047pub fn fixed_u64_to_f64(value: u64) -> f64 {
1048 (value as f64) / FIXED_SCALAR
1049}
1050
1051#[must_use]
1053#[expect(
1054 clippy::cast_precision_loss,
1055 reason = "u128 to f64 is inherently lossy above 2^53; accepted for float interop"
1056)]
1057pub fn fixed_u128_to_f64(value: u128) -> f64 {
1058 (value as f64) / FIXED_SCALAR
1059}
1060
1061#[cfg(feature = "high-precision")]
1062#[cfg(test)]
1063mod tests {
1064 use nautilus_core::approx_eq;
1065 use rstest::rstest;
1066
1067 use super::*;
1068
1069 #[rstest]
1070 fn test_correct_raw_rounds_half_away_from_zero() {
1071 let precision = FIXED_PRECISION - 1;
1072
1073 assert_eq!(correct_raw_u128(20, precision), 20);
1074 assert_eq!(correct_raw_u128(14, precision), 10);
1075 assert_eq!(correct_raw_u128(15, precision), 20);
1076 assert_eq!(correct_raw_u64(14, precision), 10);
1077 assert_eq!(correct_raw_u64(15, precision), 20);
1078 assert_eq!(correct_raw_i128(15, precision), 20);
1079 assert_eq!(correct_raw_i128(-14, precision), -10);
1080 assert_eq!(correct_raw_i128(-15, precision), -20);
1081 assert_eq!(correct_raw_i64(15, precision), 20);
1082 assert_eq!(correct_raw_i64(-14, precision), -10);
1083 assert_eq!(correct_raw_i64(-15, precision), -20);
1084 }
1085
1086 #[rstest]
1087 fn test_f64_fixed_u128_round_trip() {
1088 let raw = f64_to_fixed_u128(1.5, 1);
1089
1090 assert_eq!(raw, 15 * 10_u128.pow(u32::from(FIXED_PRECISION - 1)));
1091 assert_eq!(fixed_u128_to_f64(raw), 1.5);
1092 }
1093
1094 #[rstest]
1095 fn test_mantissa_exponent_to_fixed_i128_allows_max_scale_factor() {
1096 let exponent = i8::try_from(38 - FIXED_PRECISION).unwrap();
1097
1098 assert_eq!(
1099 mantissa_exponent_to_fixed_i128(1, exponent, 0).unwrap(),
1100 10_i128.pow(38)
1101 );
1102 assert_eq!(
1103 mantissa_exponent_to_fixed_i128(1, exponent + 1, 0)
1104 .unwrap_err()
1105 .to_string(),
1106 format!(
1107 "Exponent {} produces scale factor 10^39 which exceeds i128 range",
1108 exponent + 1
1109 )
1110 );
1111 }
1112
1113 #[rstest]
1114 fn test_raw_scales_match_requires_equal_effective_scale() {
1115 assert!(raw_scales_match(0, FIXED_PRECISION));
1116 assert!(raw_scales_match(FIXED_PRECISION + 1, FIXED_PRECISION + 1));
1117 assert!(!raw_scales_match(FIXED_PRECISION, FIXED_PRECISION + 1));
1118 }
1119
1120 #[rstest]
1121 fn test_canonical_raw_trims_native_scale_trailing_zeros() {
1122 assert_eq!(
1123 canonical_raw(0_u128, FIXED_PRECISION + 2),
1124 (0, FIXED_PRECISION)
1125 );
1126 assert_eq!(
1127 canonical_raw(1_200_u128, FIXED_PRECISION + 2),
1128 (12, FIXED_PRECISION)
1129 );
1130 assert_eq!(
1131 canonical_raw(12_000_u128, FIXED_PRECISION + 2),
1132 (120, FIXED_PRECISION)
1133 );
1134 assert_eq!(
1135 canonical_raw(1_205_u128, FIXED_PRECISION + 2),
1136 (1_205, FIXED_PRECISION + 2)
1137 );
1138 assert_eq!(
1139 canonical_raw(5_u128, FIXED_PRECISION - 1),
1140 (5, FIXED_PRECISION)
1141 );
1142 }
1143
1144 #[rstest]
1145 fn test_compare_raw_zero_operands_are_equal_across_scales() {
1146 assert_eq!(compare_raw(0_u128, u8::MAX, 0_u128, 2), Ordering::Equal);
1147 }
1148
1149 #[rstest]
1150 #[case("1.00", 100, 2)]
1151 #[case("+1.00", 100, 2)]
1152 #[case("-1.00", -100, 2)]
1153 #[case("-0.00", 0, 2)]
1154 fn test_parse_decimal_mantissa_sign(
1155 #[case] input: &str,
1156 #[case] mantissa: i128,
1157 #[case] precision: u8,
1158 ) {
1159 assert_eq!(parse_decimal_mantissa(input), Ok((mantissa, precision)));
1160 }
1161
1162 #[rstest]
1163 #[case("-+1.00")]
1164 #[case("+-1.00")]
1165 #[case("--1.00")]
1166 #[case("++1.00")]
1167 #[case("-+0.00")]
1168 fn test_parse_decimal_mantissa_rejects_multiple_signs(#[case] input: &str) {
1169 assert_eq!(
1170 parse_decimal_mantissa(input),
1171 Err(format!("Invalid decimal value '{input}'")),
1172 );
1173 assert!(input.parse::<crate::types::Price>().is_err());
1174 assert!(input.parse::<crate::types::Quantity>().is_err());
1175 assert!(
1176 format!("{input} USD")
1177 .parse::<crate::types::Money>()
1178 .is_err()
1179 );
1180 }
1181
1182 #[rstest]
1183 #[case(i128::MIN)]
1184 #[case(i128::MAX)]
1185 fn test_parse_decimal_mantissa_integer_limits(#[case] value: i128) {
1186 assert_eq!(parse_decimal_mantissa(&value.to_string()), Ok((value, 0)));
1187 }
1188
1189 #[rstest]
1190 #[case("170141183460469231731687303715884105728")]
1191 #[case("-170141183460469231731687303715884105729")]
1192 fn test_parse_decimal_mantissa_overflow(#[case] input: &str) {
1193 assert_eq!(
1194 parse_decimal_mantissa(input),
1195 Err(format!("Decimal value '{input}' exceeds i128 range")),
1196 );
1197 }
1198
1199 #[rstest]
1200 #[case(".5", 5, 1)]
1201 #[case("-.5", -5, 1)]
1202 #[case("1.", 1, 0)]
1203 #[case("0001.0200", 10200, 4)]
1204 fn test_parse_decimal_mantissa_syntax(
1205 #[case] input: &str,
1206 #[case] mantissa: i128,
1207 #[case] precision: u8,
1208 ) {
1209 assert_eq!(parse_decimal_mantissa(input), Ok((mantissa, precision)));
1210 }
1211
1212 #[rstest]
1213 #[case("")]
1214 #[case(".")]
1215 #[case("+")]
1216 #[case("-")]
1217 #[case("1.2.3")]
1218 #[case(" 1")]
1219 #[case("1 ")]
1220 #[case("1 2")]
1221 #[case("1")]
1222 fn test_parse_decimal_mantissa_invalid_syntax(#[case] input: &str) {
1223 assert_eq!(
1224 parse_decimal_mantissa(input),
1225 Err(format!("Invalid decimal value '{input}'")),
1226 );
1227 }
1228
1229 #[rstest]
1230 fn test_parse_decimal_mantissa_fraction_length_limit() {
1231 let accepted = format!("0.{}", "0".repeat(255));
1232 let rejected = format!("{accepted}0");
1233
1234 assert_eq!(parse_decimal_mantissa(&accepted), Ok((0, 255)));
1235 assert_eq!(
1236 parse_decimal_mantissa(&rejected),
1237 Err(format!(
1238 "Decimal value '{rejected}' has too many fractional digits"
1239 )),
1240 );
1241 }
1242
1243 #[rstest]
1244 fn test_decimal_string_domain_precision_limit() {
1245 use crate::types::{Price, Quantity};
1246
1247 #[cfg(feature = "defi")]
1248 let precision = crate::defi::WEI_PRECISION;
1249 #[cfg(not(feature = "defi"))]
1250 let precision = FIXED_PRECISION;
1251 let accepted = format!("0.{}1", "0".repeat(usize::from(precision - 1)));
1252 let rejected = format!("{accepted}0");
1253 let price = accepted.parse::<Price>().unwrap();
1254 let quantity = accepted.parse::<Quantity>().unwrap();
1255
1256 assert_eq!(price.raw, 1);
1257 assert_eq!(price.precision, precision);
1258 assert_eq!(quantity.raw, 1);
1259 assert_eq!(quantity.precision, precision);
1260 assert!(rejected.parse::<Price>().is_err());
1261 assert!(rejected.parse::<Quantity>().is_err());
1262 }
1263
1264 #[rstest]
1265 #[case(0, 0, "0")]
1266 #[case(125, 2, "1.25")]
1267 #[case(-1234, 2, "-12.34")]
1268 #[case(1, 16, "0.0000000000000001")]
1269 #[case(-1, 16, "-0.0000000000000001")]
1270 #[case(1_000_000_000_000_000_000, 18, "1.000000000000000000")]
1271 fn test_scaled_raw_to_decimal_matches_plain_conversion(
1272 #[case] raw: i128,
1273 #[case] precision: u8,
1274 #[case] expected: &str,
1275 ) {
1276 let plain = Decimal::from_i128_with_scale(raw, u32::from(precision));
1277 let result = scaled_raw_to_decimal(raw, precision);
1278
1279 assert_eq!(result, plain);
1280 assert_eq!(result.scale(), plain.scale());
1281 assert_eq!(result.to_string(), expected);
1282 }
1283
1284 #[rstest]
1285 #[case(80_000_000_000_000_000_000_000_000_000, 16, "8000000000000")]
1286 #[case(340_282_366_920_930_000_000_000_000_000, 16, "34028236692093")]
1287 #[case(170_141_183_460_460_000_000_000_000_000, 16, "17014118346046")]
1288 #[case(-170_141_183_460_460_000_000_000_000_000, 16, "-17014118346046")]
1289 #[case(
1292 80_000_000_000_000_005_000_000_000_000,
1293 16,
1294 "8000000000000.000500000000000"
1295 )]
1296 #[case(-80_000_000_000_000_005_000_000_000_000, 16, "-8000000000000.000500000000000")]
1297 #[case(
1298 80_000_000_000_000_000_000_000_000_001,
1299 16,
1300 "8000000000000.000000000000000"
1301 )]
1302 #[case(
1303 80_000_000_000_000_000_250_000_000_000,
1304 18,
1305 "80000000000.00000025000000000"
1306 )]
1307 #[case(-80_000_000_000_000_000_250_000_000_000, 18, "-80000000000.00000025000000000")]
1308 fn test_scaled_raw_to_decimal_beyond_mantissa_rounds_rather_than_panics(
1309 #[case] raw: i128,
1310 #[case] precision: u8,
1311 #[case] expected: &str,
1312 ) {
1313 assert_eq!(scaled_raw_to_decimal(raw, precision).to_string(), expected);
1317 }
1318
1319 #[cfg(not(feature = "defi"))]
1320 #[rstest]
1321 fn test_precision_boundaries() {
1322 assert!(check_fixed_precision(0).is_ok());
1323 assert!(check_fixed_precision(FIXED_PRECISION).is_ok());
1324 assert!(check_fixed_precision(FIXED_PRECISION + 1).is_err());
1325 }
1326
1327 #[cfg(feature = "defi")]
1328 #[rstest]
1329 fn test_precision_boundaries() {
1330 use crate::defi::WEI_PRECISION;
1331
1332 assert!(check_fixed_precision(0).is_ok());
1333 assert!(check_fixed_precision(WEI_PRECISION).is_ok());
1334 assert!(check_fixed_precision(WEI_PRECISION + 1).is_err());
1335 }
1336
1337 #[rstest]
1338 #[case(0.0)]
1339 #[case(1.0)]
1340 #[case(-1.0)]
1341 fn test_basic_roundtrip(#[case] value: f64) {
1342 for precision in 0..=FIXED_PRECISION {
1343 let fixed = f64_to_fixed_i128(value, precision);
1344 let result = fixed_i128_to_f64(fixed);
1345 assert!(approx_eq!(f64, value, result, epsilon = 0.001));
1346 }
1347 }
1348
1349 #[rstest]
1350 #[case(1_000_000.0)]
1351 #[case(-1_000_000.0)]
1352 fn test_large_value_roundtrip(#[case] value: f64) {
1353 for precision in 0..=FIXED_PRECISION {
1354 let fixed = f64_to_fixed_i128(value, precision);
1355 let result = fixed_i128_to_f64(fixed);
1356 assert!(approx_eq!(f64, value, result, epsilon = 0.000_1));
1357 }
1358 }
1359
1360 #[rstest]
1361 #[case(0, 123_456.0)]
1362 #[case(0, 123_456.7)]
1363 #[case(1, 123_456.7)]
1364 #[case(2, 123_456.78)]
1365 #[case(8, 123_456.123_456_78)]
1366 fn test_precision_specific_values_basic(#[case] precision: u8, #[case] value: f64) {
1367 let result = f64_to_fixed_i128(value, precision);
1368 let back_converted = fixed_i128_to_f64(result);
1369 let scale = 10.0_f64.powi(i32::from(precision));
1371 let expected_rounded = (value * scale).round() / scale;
1372 assert!((back_converted - expected_rounded).abs() < 1e-10);
1373 }
1374
1375 #[rstest]
1376 fn test_max_precision_values() {
1377 let test_value = 123_456.123_456_789;
1379 let result = f64_to_fixed_i128(test_value, FIXED_PRECISION);
1380 let back_converted = fixed_i128_to_f64(result);
1381 assert!((back_converted - test_value).abs() < 1e-6);
1383 }
1384
1385 #[rstest]
1386 #[case(0.0)]
1387 #[case(1.0)]
1388 #[case(1_000_000.0)]
1389 fn test_unsigned_basic_roundtrip(#[case] value: f64) {
1390 for precision in 0..=FIXED_PRECISION {
1391 let fixed = f64_to_fixed_u128(value, precision);
1392 let result = fixed_u128_to_f64(fixed);
1393 assert!(approx_eq!(f64, value, result, epsilon = 0.001));
1394 }
1395 }
1396
1397 #[rstest]
1398 #[case(0)]
1399 #[case(FIXED_PRECISION)]
1400 fn test_valid_precision(#[case] precision: u8) {
1401 let result = check_fixed_precision(precision);
1402 assert!(result.is_ok());
1403 }
1404
1405 #[cfg(not(feature = "defi"))]
1406 #[rstest]
1407 fn test_invalid_precision() {
1408 let precision = FIXED_PRECISION + 1;
1409 let result = check_fixed_precision(precision);
1410 assert!(result.is_err());
1411 }
1412
1413 #[cfg(feature = "defi")]
1414 #[rstest]
1415 fn test_invalid_precision() {
1416 use crate::defi::WEI_PRECISION;
1417 let precision = WEI_PRECISION + 1;
1418 let result = check_fixed_precision(precision);
1419 assert!(result.is_err());
1420 }
1421
1422 #[rstest]
1423 fn test_check_float_precision_matches_supported_range() {
1424 for precision in 0..=u8::MAX {
1425 let expected = if precision <= MAX_FLOAT_PRECISION {
1426 Ok(())
1427 } else {
1428 Err(CorrectnessError::PredicateViolation {
1429 message: format!(
1430 "Fixed-point precision {precision} exceeds maximum float precision 16"
1431 ),
1432 })
1433 };
1434
1435 assert_eq!(check_float_precision(precision), expected);
1436 }
1437 }
1438
1439 #[rstest]
1440 fn test_check_float_precision_error_display() {
1441 let error = check_float_precision(18).unwrap_err();
1442
1443 assert_eq!(
1444 error.to_string(),
1445 "Fixed-point precision 18 exceeds maximum float precision 16"
1446 );
1447 }
1448
1449 #[cfg(not(feature = "defi"))]
1450 #[rstest]
1451 fn test_check_fixed_precision_returns_typed_error_with_stable_display() {
1452 let error = check_fixed_precision(FIXED_PRECISION + 1).unwrap_err();
1453
1454 assert_eq!(
1455 error,
1456 CorrectnessError::PredicateViolation {
1457 message: format!(
1458 "`precision` exceeded maximum `FIXED_PRECISION` ({FIXED_PRECISION}), was {}",
1459 FIXED_PRECISION + 1
1460 ),
1461 }
1462 );
1463 assert_eq!(
1464 error.to_string(),
1465 format!(
1466 "`precision` exceeded maximum `FIXED_PRECISION` ({FIXED_PRECISION}), was {}",
1467 FIXED_PRECISION + 1
1468 )
1469 );
1470 }
1471
1472 #[cfg(feature = "defi")]
1473 #[rstest]
1474 fn test_check_fixed_precision_returns_typed_error_with_stable_display() {
1475 use crate::defi::WEI_PRECISION;
1476
1477 let error = check_fixed_precision(WEI_PRECISION + 1).unwrap_err();
1478
1479 assert_eq!(
1480 error,
1481 CorrectnessError::PredicateViolation {
1482 message: format!(
1483 "`precision` exceeded maximum `WEI_PRECISION` (18), was {}",
1484 WEI_PRECISION + 1
1485 ),
1486 }
1487 );
1488 assert_eq!(
1489 error.to_string(),
1490 format!(
1491 "`precision` exceeded maximum `WEI_PRECISION` (18), was {}",
1492 WEI_PRECISION + 1
1493 )
1494 );
1495 }
1496
1497 #[rstest]
1498 #[case(0, 0.0)]
1499 #[case(1, 1.0)]
1500 #[case(1, 1.1)]
1501 #[case(9, 0.000_000_001)]
1502 #[case(16, 0.000_000_000_000_000_1)]
1503 #[case(0, -0.0)]
1504 #[case(1, -1.0)]
1505 #[case(1, -1.1)]
1506 #[case(9, -0.000_000_001)]
1507 #[case(16, -0.000_000_000_000_000_1)]
1508 fn test_f64_to_fixed_i128_to_fixed(#[case] precision: u8, #[case] value: f64) {
1509 let fixed = f64_to_fixed_i128(value, precision);
1510 let result = fixed_i128_to_f64(fixed);
1511 assert_eq!(result, value);
1512 }
1513
1514 #[rstest]
1515 #[case(0, 0.0)]
1516 #[case(1, 1.0)]
1517 #[case(1, 1.1)]
1518 #[case(9, 0.000_000_001)]
1519 #[case(16, 0.000_000_000_000_000_1)]
1520 fn test_f64_to_fixed_u128_to_fixed(#[case] precision: u8, #[case] value: f64) {
1521 let fixed = f64_to_fixed_u128(value, precision);
1522 let result = fixed_u128_to_f64(fixed);
1523 assert_eq!(result, value);
1524 }
1525
1526 #[rstest]
1527 #[case(0, 123_456.0)]
1528 #[case(0, 123_456.7)]
1529 #[case(0, 123_456.4)]
1530 #[case(1, 123_456.0)]
1531 #[case(1, 123_456.7)]
1532 #[case(1, 123_456.4)]
1533 #[case(2, 123_456.0)]
1534 #[case(2, 123_456.7)]
1535 #[case(2, 123_456.4)]
1536 fn test_f64_to_fixed_i128_with_precision(#[case] precision: u8, #[case] value: f64) {
1537 let result = f64_to_fixed_i128(value, precision);
1538
1539 let pow1 = 10_i128.pow(u32::from(precision));
1541 let pow2 = 10_i128.pow(u32::from(FIXED_PRECISION - precision));
1542 let rounded = (value * pow1 as f64).round() as i128;
1543 let expected = rounded * pow2;
1544
1545 assert_eq!(
1546 result, expected,
1547 "Failed for precision {precision}, value {value}: got {result}, expected {expected}"
1548 );
1549 }
1550
1551 #[rstest]
1552 #[case(0, 5.555_555_555_555_555)]
1553 #[case(1, 5.555_555_555_555_555)]
1554 #[case(2, 5.555_555_555_555_555)]
1555 #[case(3, 5.555_555_555_555_555)]
1556 #[case(4, 5.555_555_555_555_555)]
1557 #[case(5, 5.555_555_555_555_555)]
1558 #[case(6, 5.555_555_555_555_555)]
1559 #[case(7, 5.555_555_555_555_555)]
1560 #[case(8, 5.555_555_555_555_555)]
1561 #[case(9, 5.555_555_555_555_555)]
1562 #[case(10, 5.555_555_555_555_555)]
1563 #[case(11, 5.555_555_555_555_555)]
1564 #[case(12, 5.555_555_555_555_555)]
1565 #[case(13, 5.555_555_555_555_555)]
1566 #[case(14, 5.555_555_555_555_555)]
1567 #[case(15, 5.555_555_555_555_555)]
1568 #[case(0, -5.555_555_555_555_555)]
1569 #[case(1, -5.555_555_555_555_555)]
1570 #[case(2, -5.555_555_555_555_555)]
1571 #[case(3, -5.555_555_555_555_555)]
1572 #[case(4, -5.555_555_555_555_555)]
1573 #[case(5, -5.555_555_555_555_555)]
1574 #[case(6, -5.555_555_555_555_555)]
1575 #[case(7, -5.555_555_555_555_555)]
1576 #[case(8, -5.555_555_555_555_555)]
1577 #[case(9, -5.555_555_555_555_555)]
1578 #[case(10, -5.555_555_555_555_555)]
1579 #[case(11, -5.555_555_555_555_555)]
1580 #[case(12, -5.555_555_555_555_555)]
1581 #[case(13, -5.555_555_555_555_555)]
1582 #[case(14, -5.555_555_555_555_555)]
1583 #[case(15, -5.555_555_555_555_555)]
1584 fn test_f64_to_fixed_i128(#[case] precision: u8, #[case] value: f64) {
1585 if precision > FIXED_PRECISION {
1587 return;
1588 }
1589
1590 let result = f64_to_fixed_i128(value, precision);
1591
1592 let pow1 = 10_i128.pow(u32::from(precision));
1594 let pow2 = 10_i128.pow(u32::from(FIXED_PRECISION - precision));
1595 let rounded = (value * pow1 as f64).round() as i128;
1596 let expected = rounded * pow2;
1597
1598 assert_eq!(
1599 result, expected,
1600 "Failed for precision {precision}, value {value}: got {result}, expected {expected}"
1601 );
1602 }
1603
1604 #[rstest]
1605 #[case(0, 5.555_555_555_555_555)]
1606 #[case(1, 5.555_555_555_555_555)]
1607 #[case(2, 5.555_555_555_555_555)]
1608 #[case(3, 5.555_555_555_555_555)]
1609 #[case(4, 5.555_555_555_555_555)]
1610 #[case(5, 5.555_555_555_555_555)]
1611 #[case(6, 5.555_555_555_555_555)]
1612 #[case(7, 5.555_555_555_555_555)]
1613 #[case(8, 5.555_555_555_555_555)]
1614 #[case(9, 5.555_555_555_555_555)]
1615 #[case(10, 5.555_555_555_555_555)]
1616 #[case(11, 5.555_555_555_555_555)]
1617 #[case(12, 5.555_555_555_555_555)]
1618 #[case(13, 5.555_555_555_555_555)]
1619 #[case(14, 5.555_555_555_555_555)]
1620 #[case(15, 5.555_555_555_555_555)]
1621 #[case(16, 5.555_555_555_555_555)]
1622 fn test_f64_to_fixed_u64(#[case] precision: u8, #[case] value: f64) {
1623 if precision > FIXED_PRECISION {
1625 return;
1626 }
1627
1628 let result = f64_to_fixed_u128(value, precision);
1629
1630 let pow1 = 10_u128.pow(u32::from(precision));
1632 let pow2 = 10_u128.pow(u32::from(FIXED_PRECISION - precision));
1633 let rounded = (value * pow1 as f64).round() as u128;
1634 let expected = rounded * pow2;
1635
1636 assert_eq!(
1637 result, expected,
1638 "Failed for precision {precision}, value {value}: got {result}, expected {expected}"
1639 );
1640 }
1641
1642 #[rstest]
1643 fn test_fixed_i128_to_f64(
1644 #[values(1, -1, 2, -2, 10, -10, 100, -100, 1_000, -1_000, -10_000, -100_000)] value: i128,
1645 ) {
1646 assert_eq!(fixed_i128_to_f64(value), value as f64 / FIXED_SCALAR);
1647 }
1648
1649 #[rstest]
1650 fn test_fixed_u128_to_f64(
1651 #[values(
1652 0,
1653 1,
1654 2,
1655 3,
1656 10,
1657 100,
1658 1_000,
1659 10_000,
1660 100_000,
1661 1_000_000,
1662 10_000_000,
1663 100_000_000,
1664 1_000_000_000,
1665 10_000_000_000,
1666 100_000_000_000,
1667 1_000_000_000_000,
1668 10_000_000_000_000,
1669 100_000_000_000_000,
1670 1_000_000_000_000_000,
1671 10_000_000_000_000_000,
1672 100_000_000_000_000_000,
1673 1_000_000_000_000_000_000,
1674 10_000_000_000_000_000_000,
1675 100_000_000_000_000_000_000
1676 )]
1677 value: u128,
1678 ) {
1679 let result = fixed_u128_to_f64(value);
1680 assert_eq!(result, (value as f64) / FIXED_SCALAR);
1681 }
1682
1683 #[rstest]
1688 #[case(0, 0)] #[case(0, 10_000_000_000_000_000)] #[case(0, 1_200_000_000_000_000_000)] #[case(8, 12_345_678_900_000_000)] #[case(15, 1_234_567_890_123_450)] fn test_check_fixed_raw_u128_valid(#[case] precision: u8, #[case] raw: u128) {
1694 assert!(check_fixed_raw_u128(raw, precision).is_ok());
1695 }
1696
1697 #[rstest]
1698 #[case(0, 1)] #[case(0, 9_999_999_999_999_999)] #[case(0, 10_000_000_000_000_001)] #[case(8, 12_345_678_900_000_001)] #[case(15, 1_234_567_890_123_451)] fn test_check_fixed_raw_u128_invalid(#[case] precision: u8, #[case] raw: u128) {
1704 assert!(check_fixed_raw_u128(raw, precision).is_err());
1705 }
1706
1707 #[rstest]
1708 fn test_check_fixed_raw_u128_at_max_precision() {
1709 assert!(check_fixed_raw_u128(0, FIXED_PRECISION).is_ok());
1711 assert!(check_fixed_raw_u128(1, FIXED_PRECISION).is_ok());
1712 assert!(check_fixed_raw_u128(123_456_789, FIXED_PRECISION).is_ok());
1713 assert!(check_fixed_raw_u128(u128::MAX, FIXED_PRECISION).is_ok());
1714 }
1715
1716 #[rstest]
1717 #[case(0, 0)]
1718 #[case(0, 10_000_000_000_000_000)]
1719 #[case(0, -10_000_000_000_000_000)]
1720 #[case(8, 12_345_678_900_000_000)]
1721 #[case(8, -12_345_678_900_000_000)]
1722 fn test_check_fixed_raw_i128_valid(#[case] precision: u8, #[case] raw: i128) {
1723 assert!(check_fixed_raw_i128(raw, precision).is_ok());
1724 }
1725
1726 #[rstest]
1727 #[case(0, 1)]
1728 #[case(0, -1)]
1729 #[case(0, 9_999_999_999_999_999)]
1730 #[case(0, -9_999_999_999_999_999)]
1731 fn test_check_fixed_raw_i128_invalid(#[case] precision: u8, #[case] raw: i128) {
1732 assert!(check_fixed_raw_i128(raw, precision).is_err());
1733 }
1734
1735 #[rstest]
1736 fn test_check_fixed_raw_i128_at_max_precision() {
1737 assert!(check_fixed_raw_i128(0, FIXED_PRECISION).is_ok());
1738 assert!(check_fixed_raw_i128(1, FIXED_PRECISION).is_ok());
1739 assert!(check_fixed_raw_i128(-1, FIXED_PRECISION).is_ok());
1740 assert!(check_fixed_raw_i128(i128::MAX, FIXED_PRECISION).is_ok());
1741 assert!(check_fixed_raw_i128(i128::MIN, FIXED_PRECISION).is_ok());
1742 }
1743
1744 #[rstest]
1745 #[should_panic(expected = "Overflow when scaling f64 to fixed-point i128")]
1746 fn test_f64_to_fixed_i128_overflow_panics() {
1747 let _ = f64_to_fixed_i128(1e30, 0);
1748 }
1749
1750 #[rstest]
1751 #[should_panic(expected = "Overflow when scaling f64 to fixed-point u128")]
1752 fn test_f64_to_fixed_u128_overflow_panics() {
1753 let _ = f64_to_fixed_u128(1e30, 0);
1754 }
1755}
1756
1757#[cfg(not(feature = "high-precision"))]
1758#[cfg(test)]
1759mod tests {
1760 use nautilus_core::approx_eq;
1761 use rstest::rstest;
1762
1763 use super::*;
1764
1765 #[rstest]
1766 fn test_correct_raw_rounds_half_away_from_zero() {
1767 let precision = FIXED_PRECISION - 1;
1768
1769 assert_eq!(correct_raw_u128(20, precision), 20);
1770 assert_eq!(correct_raw_u128(14, precision), 10);
1771 assert_eq!(correct_raw_u128(15, precision), 20);
1772 assert_eq!(correct_raw_u64(14, precision), 10);
1773 assert_eq!(correct_raw_u64(15, precision), 20);
1774 assert_eq!(correct_raw_i128(15, precision), 20);
1775 assert_eq!(correct_raw_i128(-14, precision), -10);
1776 assert_eq!(correct_raw_i128(-15, precision), -20);
1777 assert_eq!(correct_raw_i64(15, precision), 20);
1778 assert_eq!(correct_raw_i64(-14, precision), -10);
1779 assert_eq!(correct_raw_i64(-15, precision), -20);
1780 }
1781
1782 #[rstest]
1783 fn test_f64_fixed_u128_round_trip() {
1784 let raw = f64_to_fixed_u128(1.5, 1);
1785
1786 assert_eq!(raw, 15 * 10_u128.pow(u32::from(FIXED_PRECISION - 1)));
1787 assert_eq!(fixed_u128_to_f64(raw), 1.5);
1788 }
1789
1790 #[rstest]
1791 fn test_mantissa_exponent_to_fixed_i128_allows_max_scale_factor() {
1792 let exponent = i8::try_from(38 - FIXED_PRECISION).unwrap();
1793
1794 assert_eq!(
1795 mantissa_exponent_to_fixed_i128(1, exponent, 0).unwrap(),
1796 10_i128.pow(38)
1797 );
1798 assert_eq!(
1799 mantissa_exponent_to_fixed_i128(1, exponent + 1, 0)
1800 .unwrap_err()
1801 .to_string(),
1802 format!(
1803 "Exponent {} produces scale factor 10^39 which exceeds i128 range",
1804 exponent + 1
1805 )
1806 );
1807 }
1808
1809 #[rstest]
1810 fn test_raw_scales_match_requires_equal_effective_scale() {
1811 assert!(raw_scales_match(0, FIXED_PRECISION));
1812 assert!(raw_scales_match(FIXED_PRECISION + 1, FIXED_PRECISION + 1));
1813 assert!(!raw_scales_match(FIXED_PRECISION, FIXED_PRECISION + 1));
1814 }
1815
1816 #[rstest]
1817 fn test_canonical_raw_trims_native_scale_trailing_zeros() {
1818 assert_eq!(
1819 canonical_raw(0_u128, FIXED_PRECISION + 2),
1820 (0, FIXED_PRECISION)
1821 );
1822 assert_eq!(
1823 canonical_raw(1_200_u128, FIXED_PRECISION + 2),
1824 (12, FIXED_PRECISION)
1825 );
1826 assert_eq!(
1827 canonical_raw(12_000_u128, FIXED_PRECISION + 2),
1828 (120, FIXED_PRECISION)
1829 );
1830 assert_eq!(
1831 canonical_raw(1_205_u128, FIXED_PRECISION + 2),
1832 (1_205, FIXED_PRECISION + 2)
1833 );
1834 assert_eq!(
1835 canonical_raw(5_u128, FIXED_PRECISION - 1),
1836 (5, FIXED_PRECISION)
1837 );
1838 }
1839
1840 #[rstest]
1841 fn test_compare_raw_zero_operands_are_equal_across_scales() {
1842 assert_eq!(compare_raw(0_u128, u8::MAX, 0_u128, 2), Ordering::Equal);
1843 }
1844
1845 #[rstest]
1846 fn test_precision_boundaries() {
1847 assert!(check_fixed_precision(0).is_ok());
1848 assert!(check_fixed_precision(FIXED_PRECISION).is_ok());
1849 assert!(check_fixed_precision(FIXED_PRECISION + 1).is_err());
1850 }
1851
1852 #[rstest]
1853 #[case(0.0)]
1854 #[case(1.0)]
1855 #[case(-1.0)]
1856 fn test_basic_roundtrip(#[case] value: f64) {
1857 for precision in 0..=FIXED_PRECISION {
1858 let fixed = f64_to_fixed_i64(value, precision);
1859 let result = fixed_i64_to_f64(fixed);
1860 assert!(approx_eq!(f64, value, result, epsilon = 0.001));
1861 }
1862 }
1863
1864 #[rstest]
1865 #[case(1_000_000.0)]
1866 #[case(-1_000_000.0)]
1867 fn test_large_value_roundtrip(#[case] value: f64) {
1868 for precision in 0..=FIXED_PRECISION {
1869 let fixed = f64_to_fixed_i64(value, precision);
1870 let result = fixed_i64_to_f64(fixed);
1871 assert!(approx_eq!(f64, value, result, epsilon = 0.000_1));
1872 }
1873 }
1874
1875 #[rstest]
1876 #[case(0, 123_456.0, 123_456_000_000_000)]
1877 #[case(0, 123_456.7, 123_457_000_000_000)]
1878 #[case(1, 123_456.7, 123_456_700_000_000)]
1879 #[case(2, 123_456.78, 123_456_780_000_000)]
1880 #[case(8, 123_456.123_456_78, 123_456_123_456_780)]
1881 #[case(9, 123_456.123_456_789, 123_456_123_456_789)]
1882 fn test_precision_specific_values(
1883 #[case] precision: u8,
1884 #[case] value: f64,
1885 #[case] expected: i64,
1886 ) {
1887 assert_eq!(f64_to_fixed_i64(value, precision), expected);
1888 }
1889
1890 #[rstest]
1891 #[case(0.0)]
1892 #[case(1.0)]
1893 #[case(1_000_000.0)]
1894 fn test_unsigned_basic_roundtrip(#[case] value: f64) {
1895 for precision in 0..=FIXED_PRECISION {
1896 let fixed = f64_to_fixed_u64(value, precision);
1897 let result = fixed_u64_to_f64(fixed);
1898 assert!(approx_eq!(f64, value, result, epsilon = 0.001));
1899 }
1900 }
1901
1902 #[rstest]
1903 #[case(0, 1.4, 1.0)]
1904 #[case(0, 1.5, 2.0)]
1905 #[case(0, 1.6, 2.0)]
1906 #[case(1, 1.44, 1.4)]
1907 #[case(1, 1.45, 1.5)]
1908 #[case(1, 1.46, 1.5)]
1909 #[case(2, 1.444, 1.44)]
1910 #[case(2, 1.445, 1.45)]
1911 #[case(2, 1.446, 1.45)]
1912 fn test_rounding(#[case] precision: u8, #[case] input: f64, #[case] expected: f64) {
1913 let fixed = f64_to_fixed_i128(input, precision);
1914 assert!(approx_eq!(
1915 f64,
1916 fixed_i128_to_f64(fixed),
1917 expected,
1918 epsilon = 0.000_000_001
1919 ));
1920 }
1921
1922 #[rstest]
1923 fn test_special_values() {
1924 assert_eq!(f64_to_fixed_i128(0.0, FIXED_PRECISION), 0);
1926 assert_eq!(f64_to_fixed_i128(-0.0, FIXED_PRECISION), 0);
1927
1928 let smallest_positive = 1.0 / FIXED_SCALAR;
1930 let fixed_smallest = f64_to_fixed_i128(smallest_positive, FIXED_PRECISION);
1931 assert_eq!(fixed_smallest, 1);
1932
1933 let large_int = 1_000_000_000.0;
1935 let fixed_large = f64_to_fixed_i128(large_int, 0);
1936 assert_eq!(fixed_i128_to_f64(fixed_large), large_int);
1937 }
1938
1939 #[rstest]
1940 #[case(0)]
1941 #[case(FIXED_PRECISION)]
1942 fn test_valid_precision(#[case] precision: u8) {
1943 let result = check_fixed_precision(precision);
1944 assert!(result.is_ok());
1945 }
1946
1947 #[rstest]
1948 fn test_invalid_precision() {
1949 let precision = FIXED_PRECISION + 1;
1950 let result = check_fixed_precision(precision);
1951 assert!(result.is_err());
1952 }
1953
1954 #[rstest]
1955 #[case(0, 0.0)]
1956 #[case(1, 1.0)]
1957 #[case(1, 1.1)]
1958 #[case(9, 0.000_000_001)]
1959 #[case(0, -0.0)]
1960 #[case(1, -1.0)]
1961 #[case(1, -1.1)]
1962 #[case(9, -0.000_000_001)]
1963 fn test_f64_to_fixed_i64_to_fixed(#[case] precision: u8, #[case] value: f64) {
1964 let fixed = f64_to_fixed_i64(value, precision);
1965 let result = fixed_i64_to_f64(fixed);
1966 assert_eq!(result, value);
1967 }
1968
1969 #[rstest]
1970 #[case(0, 0.0)]
1971 #[case(1, 1.0)]
1972 #[case(1, 1.1)]
1973 #[case(9, 0.000_000_001)]
1974 fn test_f64_to_fixed_u64_to_fixed(#[case] precision: u8, #[case] value: f64) {
1975 let fixed = f64_to_fixed_u64(value, precision);
1976 let result = fixed_u64_to_f64(fixed);
1977 assert_eq!(result, value);
1978 }
1979
1980 #[rstest]
1981 #[case(0, 123_456.0, 123_456_000_000_000)]
1982 #[case(0, 123_456.7, 123_457_000_000_000)]
1983 #[case(0, 123_456.4, 123_456_000_000_000)]
1984 #[case(1, 123_456.0, 123_456_000_000_000)]
1985 #[case(1, 123_456.7, 123_456_700_000_000)]
1986 #[case(1, 123_456.4, 123_456_400_000_000)]
1987 #[case(2, 123_456.0, 123_456_000_000_000)]
1988 #[case(2, 123_456.7, 123_456_700_000_000)]
1989 #[case(2, 123_456.4, 123_456_400_000_000)]
1990 fn test_f64_to_fixed_i64_with_precision(
1991 #[case] precision: u8,
1992 #[case] value: f64,
1993 #[case] expected: i64,
1994 ) {
1995 assert_eq!(f64_to_fixed_i64(value, precision), expected);
1996 }
1997
1998 #[rstest]
1999 #[case(0, 5.5, 6_000_000_000)]
2000 #[case(1, 5.55, 5_600_000_000)]
2001 #[case(2, 5.555, 5_560_000_000)]
2002 #[case(3, 5.5555, 5_556_000_000)]
2003 #[case(4, 5.55555, 5_555_600_000)]
2004 #[case(5, 5.555_555, 5_555_560_000)]
2005 #[case(6, 5.555_555_5, 5_555_556_000)]
2006 #[case(7, 5.555_555_55, 5_555_555_600)]
2007 #[case(8, 5.555_555_555, 5_555_555_560)]
2008 #[case(9, 5.555_555_555_5, 5_555_555_556)]
2009 #[case(0, -5.5, -6_000_000_000)]
2010 #[case(1, -5.55, -5_600_000_000)]
2011 #[case(2, -5.555, -5_560_000_000)]
2012 #[case(3, -5.5555, -5_556_000_000)]
2013 #[case(4, -5.55555, -5_555_600_000)]
2014 #[case(5, -5.555_555, -5_555_560_000)]
2015 #[case(6, -5.555_555_5, -5_555_556_000)]
2016 #[case(7, -5.555_555_55, -5_555_555_600)]
2017 #[case(8, -5.555_555_555, -5_555_555_560)]
2018 #[case(9, -5.555_555_555_5, -5_555_555_556)]
2019 fn test_f64_to_fixed_i64(#[case] precision: u8, #[case] value: f64, #[case] expected: i64) {
2020 assert_eq!(f64_to_fixed_i64(value, precision), expected);
2021 }
2022
2023 #[rstest]
2024 #[case(0, 5.5, 6_000_000_000)]
2025 #[case(1, 5.55, 5_600_000_000)]
2026 #[case(2, 5.555, 5_560_000_000)]
2027 #[case(3, 5.5555, 5_556_000_000)]
2028 #[case(4, 5.55555, 5_555_600_000)]
2029 #[case(5, 5.555_555, 5_555_560_000)]
2030 #[case(6, 5.555_555_5, 5_555_556_000)]
2031 #[case(7, 5.555_555_55, 5_555_555_600)]
2032 #[case(8, 5.555_555_555, 5_555_555_560)]
2033 #[case(9, 5.555_555_555_5, 5_555_555_556)]
2034 fn test_f64_to_fixed_u64(#[case] precision: u8, #[case] value: f64, #[case] expected: u64) {
2035 assert_eq!(f64_to_fixed_u64(value, precision), expected);
2036 }
2037
2038 #[rstest]
2039 fn test_fixed_i64_to_f64(
2040 #[values(1, -1, 2, -2, 10, -10, 100, -100, 1_000, -1_000)] value: i64,
2041 ) {
2042 assert_eq!(fixed_i64_to_f64(value), value as f64 / FIXED_SCALAR);
2043 }
2044
2045 #[rstest]
2046 fn test_fixed_u64_to_f64(
2047 #[values(
2048 0,
2049 1,
2050 2,
2051 3,
2052 10,
2053 100,
2054 1_000,
2055 10_000,
2056 100_000,
2057 1_000_000,
2058 10_000_000,
2059 100_000_000,
2060 1_000_000_000,
2061 10_000_000_000,
2062 100_000_000_000,
2063 1_000_000_000_000,
2064 10_000_000_000_000,
2065 100_000_000_000_000,
2066 1_000_000_000_000_000
2067 )]
2068 value: u64,
2069 ) {
2070 let result = fixed_u64_to_f64(value);
2071 assert_eq!(result, (value as f64) / FIXED_SCALAR);
2072 }
2073
2074 #[rstest]
2075 #[case(0, 0)] #[case(0, 1_000_000_000)] #[case(0, 120_000_000_000)] #[case(2, 123_450_000_000)] #[case(8, 1_234_567_890)] fn test_check_fixed_raw_u64_valid(#[case] precision: u8, #[case] raw: u64) {
2081 assert!(check_fixed_raw_u64(raw, precision).is_ok());
2082 }
2083
2084 #[rstest]
2085 #[case(0, 1)] #[case(0, 999_999_999)] #[case(0, 1_000_000_001)] #[case(0, 119_582_001_968_421_736)] #[case(2, 123_456_789_000)] #[case(8, 1_234_567_891)] fn test_check_fixed_raw_u64_invalid(#[case] precision: u8, #[case] raw: u64) {
2092 assert!(check_fixed_raw_u64(raw, precision).is_err());
2093 }
2094
2095 #[rstest]
2096 fn test_check_fixed_raw_u64_at_max_precision() {
2097 assert!(check_fixed_raw_u64(0, FIXED_PRECISION).is_ok());
2099 assert!(check_fixed_raw_u64(1, FIXED_PRECISION).is_ok());
2100 assert!(check_fixed_raw_u64(123_456_789, FIXED_PRECISION).is_ok());
2101 assert!(check_fixed_raw_u64(u64::MAX, FIXED_PRECISION).is_ok());
2102 }
2103
2104 #[rstest]
2105 #[case(0, 0)]
2106 #[case(0, 1_000_000_000)]
2107 #[case(0, -1_000_000_000)]
2108 #[case(2, 123_450_000_000)]
2109 #[case(2, -123_450_000_000)]
2110 fn test_check_fixed_raw_i64_valid(#[case] precision: u8, #[case] raw: i64) {
2111 assert!(check_fixed_raw_i64(raw, precision).is_ok());
2112 }
2113
2114 #[rstest]
2115 #[case(0, 1)]
2116 #[case(0, -1)]
2117 #[case(0, 999_999_999)]
2118 #[case(0, -999_999_999)]
2119 fn test_check_fixed_raw_i64_invalid(#[case] precision: u8, #[case] raw: i64) {
2120 assert!(check_fixed_raw_i64(raw, precision).is_err());
2121 }
2122
2123 #[rstest]
2124 fn test_check_fixed_raw_i64_at_max_precision() {
2125 assert!(check_fixed_raw_i64(0, FIXED_PRECISION).is_ok());
2126 assert!(check_fixed_raw_i64(1, FIXED_PRECISION).is_ok());
2127 assert!(check_fixed_raw_i64(-1, FIXED_PRECISION).is_ok());
2128 assert!(check_fixed_raw_i64(i64::MAX, FIXED_PRECISION).is_ok());
2129 assert!(check_fixed_raw_i64(i64::MIN, FIXED_PRECISION).is_ok());
2130 }
2131
2132 #[rstest]
2133 #[should_panic(expected = "Overflow when scaling f64 to fixed-point i64")]
2134 fn test_f64_to_fixed_i64_overflow_panics() {
2135 let _ = f64_to_fixed_i64(2e18, 0);
2136 }
2137
2138 #[rstest]
2139 #[should_panic(expected = "Overflow when scaling f64 to fixed-point u64")]
2140 fn test_f64_to_fixed_u64_overflow_panics() {
2141 let _ = f64_to_fixed_u64(2e19, 0);
2142 }
2143}
2144
2145#[cfg(test)]
2146mod bankers_round_tests {
2147 use std::str::FromStr;
2148
2149 use rstest::rstest;
2150 use rust_decimal::{Decimal, RoundingStrategy};
2151
2152 use super::*;
2153
2154 #[rstest]
2155 #[case(0, 0, 0)]
2157 #[case(1, 0, 1)]
2158 #[case(5, 0, 5)]
2159 #[case(99, 0, 99)]
2160 #[case(-7, 0, -7)]
2161 #[case(12345, 39, 0)]
2163 #[case(i128::from(i64::MAX), 100, 0)]
2164 #[case(-99999, 50, 0)]
2165 #[case(15, 1, 2)] #[case(25, 1, 2)] #[case(35, 1, 4)] #[case(45, 1, 4)] #[case(55, 1, 6)] #[case(65, 1, 6)] #[case(75, 1, 8)] #[case(85, 1, 8)] #[case(95, 1, 10)] #[case(105, 1, 10)] #[case(14, 1, 1)] #[case(16, 1, 2)] #[case(24, 1, 2)] #[case(26, 1, 3)] #[case(11, 1, 1)] #[case(19, 1, 2)] #[case(150, 2, 2)] #[case(250, 2, 2)] #[case(350, 2, 4)] #[case(450, 2, 4)] #[case(550, 2, 6)] #[case(1050, 2, 10)] #[case(1150, 2, 12)] #[case(149, 2, 1)] #[case(151, 2, 2)] #[case(199, 2, 2)] #[case(101, 2, 1)] #[case(1500, 3, 2)] #[case(2500, 3, 2)] #[case(3500, 3, 4)] #[case(10500, 3, 10)] #[case(11500, 3, 12)] #[case(1499, 3, 1)] #[case(1501, 3, 2)] #[case(-15, 1, -2)] #[case(-25, 1, -2)] #[case(-35, 1, -4)] #[case(-45, 1, -4)] #[case(-55, 1, -6)] #[case(-65, 1, -6)] #[case(-150, 2, -2)] #[case(-250, 2, -2)] #[case(-350, 2, -4)] #[case(-14, 1, -1)] #[case(-16, 1, -2)] #[case(-24, 1, -2)] #[case(-26, 1, -3)] #[case(0, 1, 0)]
2222 #[case(0, 2, 0)]
2223 #[case(0, 5, 0)]
2224 #[case(123_456_789, 3, 123_457)] #[case(123_456_500, 3, 123_456)] #[case(123_457_500, 3, 123_458)] #[case(100_005, 1, 10_000)] #[case(100_015, 1, 10_002)] #[case(999_999_999_999_999_995, 1, 100_000_000_000_000_000)]
2232 #[case(1_000_000_000_000_000_005, 1, 100_000_000_000_000_000)]
2233 fn test_bankers_round(#[case] mantissa: i128, #[case] excess: u32, #[case] expected: i128) {
2234 assert_eq!(
2235 bankers_round(mantissa, excess),
2236 expected,
2237 "bankers_round({mantissa}, {excess}) expected {expected}"
2238 );
2239 }
2240
2241 #[rstest]
2243 #[case(15, 1)]
2244 #[case(25, 1)]
2245 #[case(35, 1)]
2246 #[case(150, 2)]
2247 #[case(250, 2)]
2248 #[case(1500, 3)]
2249 #[case(2500, 3)]
2250 #[case(123_456_789, 3)]
2251 #[case(14, 1)]
2252 #[case(16, 1)]
2253 fn test_bankers_round_negative_symmetry(#[case] mantissa: i128, #[case] excess: u32) {
2254 assert_eq!(
2255 bankers_round(-mantissa, excess),
2256 -bankers_round(mantissa, excess),
2257 "Negative symmetry failed for mantissa={mantissa}, excess={excess}"
2258 );
2259 }
2260
2261 #[rstest]
2263 #[case("1.005", 2, "1.00")] #[case("1.015", 2, "1.02")] #[case("1.025", 2, "1.02")] #[case("1.035", 2, "1.04")] #[case("1.045", 2, "1.04")] #[case("2.5", 0, "2")] #[case("3.5", 0, "4")] #[case("-2.5", 0, "-2")]
2271 #[case("-3.5", 0, "-4")]
2272 #[case("123.456", 2, "123.46")]
2273 #[case("123.455", 2, "123.46")] #[case("123.445", 2, "123.44")] fn test_bankers_round_matches_decimal(
2276 #[case] input: &str,
2277 #[case] target_precision: u8,
2278 #[case] expected: &str,
2279 ) {
2280 let dec = Decimal::from_str(input).unwrap();
2281 let expected_dec = Decimal::from_str(expected).unwrap();
2282
2283 let decimal_rounded = dec.round_dp_with_strategy(
2284 u32::from(target_precision),
2285 RoundingStrategy::MidpointNearestEven,
2286 );
2287 assert_eq!(
2288 decimal_rounded, expected_dec,
2289 "Decimal rounding sanity check failed for {input}"
2290 );
2291
2292 let mantissa = dec.mantissa();
2293 let scale = dec.scale() as u8;
2294 let excess = u32::from(scale.saturating_sub(target_precision));
2295 if excess > 0 {
2296 let rounded = bankers_round(mantissa, excess);
2297
2298 let expected_mantissa = expected_dec.mantissa();
2300 let expected_scale = expected_dec.scale() as u8;
2301 let scale_diff = u32::from(target_precision.saturating_sub(expected_scale));
2302 let normalized_expected = expected_mantissa * 10i128.pow(scale_diff);
2303
2304 assert_eq!(
2305 rounded, normalized_expected,
2306 "bankers_round disagrees with Decimal for {input} at precision {target_precision}"
2307 );
2308 }
2309 }
2310}
2311
2312#[cfg(test)]
2313mod correct_raw_tests {
2314 use rstest::rstest;
2315
2316 use super::*;
2317
2318 #[rstest]
2322 #[case(0, 0)]
2323 #[case(10, 10)] #[case(14, 10)] #[case(15, 20)] #[case(16, 20)] #[case(u64::MAX, u64::MAX - 5)] fn test_correct_raw_u64(#[case] raw: u64, #[case] expected: u64) {
2329 assert_eq!(correct_raw_u64(raw, FIXED_PRECISION - 1), expected);
2330 }
2331
2332 #[rstest]
2333 #[case(0, 0)]
2334 #[case(14, 10)]
2335 #[case(15, 20)]
2336 #[case(-14, -10)] #[case(-15, -20)] #[case(-16, -20)] #[case(i64::MAX, i64::MAX - 7)] #[case(i64::MIN, i64::MIN + 8)] fn test_correct_raw_i64(#[case] raw: i64, #[case] expected: i64) {
2342 assert_eq!(correct_raw_i64(raw, FIXED_PRECISION - 1), expected);
2343 }
2344
2345 #[rstest]
2346 #[case(0, 0)]
2347 #[case(14, 10)]
2348 #[case(15, 20)]
2349 #[case(u128::MAX, u128::MAX - 5)] fn test_correct_raw_u128(#[case] raw: u128, #[case] expected: u128) {
2351 assert_eq!(correct_raw_u128(raw, FIXED_PRECISION - 1), expected);
2352 }
2353
2354 #[rstest]
2355 #[case(0, 0)]
2356 #[case(14, 10)]
2357 #[case(15, 20)]
2358 #[case(-15, -20)]
2359 #[case(i128::MAX, i128::MAX - 7)] #[case(i128::MIN, i128::MIN + 8)] fn test_correct_raw_i128(#[case] raw: i128, #[case] expected: i128) {
2362 assert_eq!(correct_raw_i128(raw, FIXED_PRECISION - 1), expected);
2363 }
2364
2365 #[rstest]
2366 fn test_correct_raw_identity_at_max_precision() {
2367 assert_eq!(correct_raw_u64(12_345, FIXED_PRECISION), 12_345);
2368 assert_eq!(correct_raw_i64(-12_345, FIXED_PRECISION), -12_345);
2369 assert_eq!(correct_raw_u128(12_345, FIXED_PRECISION), 12_345);
2370 assert_eq!(correct_raw_i128(-12_345, FIXED_PRECISION), -12_345);
2371 }
2372}
2373
2374#[cfg(test)]
2375mod checked_mul_div_tests {
2376 #[cfg(feature = "defi")]
2377 use alloy_primitives::U256;
2378 use proptest::{prelude::*, test_runner::Config as ProptestConfig};
2379 use rstest::rstest;
2380
2381 use super::{FIXED_SCALAR_RAW, checked_mul_div_fixed};
2382 use crate::types::quantity::QuantityRaw;
2383
2384 #[rstest]
2385 fn test_checked_mul_div_fixed_exact_boundaries() {
2386 let scalar = FIXED_SCALAR_RAW;
2387
2388 assert_eq!(checked_mul_div_fixed(0, QuantityRaw::MAX), Some(0));
2389 assert_eq!(checked_mul_div_fixed(QuantityRaw::MAX, 0), Some(0));
2390 assert_eq!(checked_mul_div_fixed(scalar, scalar), Some(scalar));
2391 assert_eq!(
2392 checked_mul_div_fixed(scalar - 1, scalar - 1),
2393 Some(scalar - 2)
2394 );
2395 assert_eq!(
2396 checked_mul_div_fixed(scalar + 1, scalar + 1),
2397 Some(scalar + 2)
2398 );
2399 assert_eq!(
2400 checked_mul_div_fixed(QuantityRaw::MAX, scalar),
2401 Some(QuantityRaw::MAX)
2402 );
2403 assert_eq!(
2404 checked_mul_div_fixed(scalar, QuantityRaw::MAX),
2405 Some(QuantityRaw::MAX)
2406 );
2407 assert_eq!(checked_mul_div_fixed(QuantityRaw::MAX, scalar + 1), None);
2408 assert_eq!(checked_mul_div_fixed(scalar + 1, QuantityRaw::MAX), None);
2409 }
2410
2411 #[cfg(not(feature = "high-precision"))]
2412 proptest! {
2413 #![proptest_config(ProptestConfig::with_cases(4_096))]
2414
2415 #[rstest]
2416 fn prop_checked_mul_div_fixed_matches_u128_full_range(
2417 lhs in any::<QuantityRaw>(),
2418 rhs in any::<QuantityRaw>(),
2419 ) {
2420 let expected = u128::from(lhs)
2421 .checked_mul(u128::from(rhs))
2422 .map(|product| product / u128::from(FIXED_SCALAR_RAW))
2423 .and_then(|result| QuantityRaw::try_from(result).ok());
2424
2425 prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), expected);
2426 }
2427
2428 #[rstest]
2429 fn prop_checked_mul_div_fixed_matches_u128_final_fit(
2430 (lhs, rhs) in standard_final_fit_strategy(),
2431 ) {
2432 let expected =
2433 u128::from(lhs) * u128::from(rhs) / u128::from(FIXED_SCALAR_RAW);
2434 let expected = QuantityRaw::try_from(expected).expect("strategy result fits u64");
2435
2436 prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), Some(expected));
2437 }
2438
2439 #[rstest]
2440 fn prop_checked_mul_div_fixed_avoids_u64_phantom_overflow(
2441 (lhs, rhs, expected) in standard_phantom_overflow_strategy(),
2442 ) {
2443 prop_assert!(lhs.checked_mul(rhs).is_none());
2444 prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), Some(expected));
2445 }
2446 }
2447
2448 #[cfg(feature = "high-precision")]
2449 proptest! {
2450 #![proptest_config(ProptestConfig::with_cases(4_096))]
2451
2452 #[rstest]
2453 fn prop_checked_mul_div_fixed_matches_u128_ordinary(
2454 (lhs, rhs) in high_precision_ordinary_strategy(),
2455 ) {
2456 let expected = lhs
2457 .checked_mul(rhs)
2458 .expect("ordinary strategy product fits u128")
2459 / FIXED_SCALAR_RAW;
2460
2461 prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), Some(expected));
2462 }
2463
2464 #[rstest]
2465 fn prop_checked_mul_div_fixed_avoids_u128_phantom_overflow(
2466 (lhs, rhs, expected) in high_precision_phantom_overflow_strategy(),
2467 ) {
2468 prop_assert!(lhs.checked_mul(rhs).is_none());
2469 prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), Some(expected));
2470 }
2471
2472 #[rstest]
2473 fn prop_checked_mul_div_fixed_handles_remainders_after_u128_overflow(
2474 rhs in high_precision_remainder_overflow_strategy(),
2475 ) {
2476 let lhs = 2 * FIXED_SCALAR_RAW - 1;
2477 let expected = 2 * rhs - rhs.div_ceil(FIXED_SCALAR_RAW);
2478
2479 prop_assert!(lhs.checked_mul(rhs).is_none());
2480 prop_assert_ne!(lhs % FIXED_SCALAR_RAW, 0);
2481 prop_assert_ne!(rhs % FIXED_SCALAR_RAW, 0);
2482 prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), Some(expected));
2483 prop_assert_eq!(checked_mul_div_fixed(rhs, lhs), Some(expected));
2484 }
2485
2486 #[rstest]
2487 fn prop_checked_mul_div_fixed_is_commutative(
2488 lhs in any::<QuantityRaw>(),
2489 rhs in any::<QuantityRaw>(),
2490 ) {
2491 prop_assert_eq!(
2492 checked_mul_div_fixed(lhs, rhs),
2493 checked_mul_div_fixed(rhs, lhs)
2494 );
2495 }
2496 }
2497
2498 #[cfg(feature = "defi")]
2499 proptest! {
2500 #![proptest_config(ProptestConfig::with_cases(4_096))]
2501
2502 #[rstest]
2503 fn prop_checked_mul_div_raw_matches_u256_full_range(
2504 lhs in any::<QuantityRaw>(),
2505 rhs in any::<QuantityRaw>(),
2506 precision in 16_u32..=18,
2507 ) {
2508 let scalar = 10_u128.pow(precision);
2509 let expected = U256::from(lhs) * U256::from(rhs) / U256::from(scalar);
2510 let expected = QuantityRaw::try_from(expected).ok();
2511 prop_assert_eq!(super::checked_mul_div_raw(lhs, rhs, scalar), expected);
2512 }
2513
2514 #[rstest]
2515 fn prop_checked_mul_div_fixed_matches_u256_full_range(
2516 lhs in any::<QuantityRaw>(),
2517 rhs in any::<QuantityRaw>(),
2518 ) {
2519 let expected = U256::from(lhs)
2520 .checked_mul(U256::from(rhs))
2521 .expect("u128 product fits U256")
2522 / U256::from(FIXED_SCALAR_RAW);
2523 let expected = QuantityRaw::try_from(expected).ok();
2524
2525 prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), expected);
2526 }
2527 }
2528
2529 #[cfg(not(feature = "high-precision"))]
2530 fn standard_final_fit_strategy() -> impl Strategy<Value = (QuantityRaw, QuantityRaw)> {
2531 let scalar = FIXED_SCALAR_RAW;
2532
2533 (0_u64..=1_000, 0_u64..=1_000, 0_u64..scalar, 0_u64..scalar).prop_map(
2534 move |(lhs_whole, rhs_whole, lhs_remainder, rhs_remainder)| {
2535 (
2536 lhs_whole * scalar + lhs_remainder,
2537 rhs_whole * scalar + rhs_remainder,
2538 )
2539 },
2540 )
2541 }
2542
2543 #[cfg(not(feature = "high-precision"))]
2544 fn standard_phantom_overflow_strategy()
2545 -> impl Strategy<Value = (QuantityRaw, QuantityRaw, QuantityRaw)> {
2546 let scalar = FIXED_SCALAR_RAW;
2547
2548 (8_000_000_000_u64..=9_000_000_000, 0_u64..scalar).prop_map(
2549 move |(lhs_whole, rhs_remainder)| {
2550 let lhs = lhs_whole * scalar;
2551 let rhs = scalar + rhs_remainder;
2552 (lhs, rhs, lhs_whole * rhs)
2553 },
2554 )
2555 }
2556
2557 #[cfg(feature = "high-precision")]
2558 fn high_precision_ordinary_strategy() -> impl Strategy<Value = (QuantityRaw, QuantityRaw)> {
2559 let scalar = FIXED_SCALAR_RAW;
2560
2561 (
2562 0_u128..=1_000,
2563 0_u128..=1_000,
2564 0_u128..scalar,
2565 0_u128..scalar,
2566 )
2567 .prop_map(
2568 move |(lhs_whole, rhs_whole, lhs_remainder, rhs_remainder)| {
2569 (
2570 lhs_whole * scalar + lhs_remainder,
2571 rhs_whole * scalar + rhs_remainder,
2572 )
2573 },
2574 )
2575 }
2576
2577 #[cfg(feature = "high-precision")]
2578 fn high_precision_phantom_overflow_strategy()
2579 -> impl Strategy<Value = (QuantityRaw, QuantityRaw, QuantityRaw)> {
2580 let scalar = FIXED_SCALAR_RAW;
2581
2582 (10_000_u128..=1_000_000, 1_000_u128..=10_000, 0_u128..scalar).prop_map(
2583 move |(lhs_whole, rhs_whole, rhs_remainder)| {
2584 let lhs = lhs_whole * scalar;
2585 let rhs = rhs_whole * scalar + rhs_remainder;
2586 (lhs, rhs, lhs_whole * rhs)
2587 },
2588 )
2589 }
2590
2591 #[cfg(feature = "high-precision")]
2592 fn high_precision_remainder_overflow_strategy() -> impl Strategy<Value = QuantityRaw> {
2593 let lhs = 2 * FIXED_SCALAR_RAW - 1;
2594 let min = QuantityRaw::MAX / lhs + 1;
2595
2596 (min..=QuantityRaw::MAX / 2).prop_filter("rhs remainder is nonzero", |rhs| {
2597 rhs % FIXED_SCALAR_RAW != 0
2598 })
2599 }
2600}