1use std::{borrow::Cow, 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
372 let digits = whole.bytes().chain(fraction.bytes());
373 if (whole.is_empty() && fraction.is_empty())
374 || !digits.clone().all(|byte| byte.is_ascii_digit())
375 {
376 return Err(format!("Invalid decimal value '{value}'"));
377 }
378 let precision = u8::try_from(fraction.len())
379 .map_err(|_| format!("Decimal value '{value}' has too many fractional digits"))?;
380 let mut mantissa = 0_i128;
381 for digit in digits.map(|byte| i128::from(byte - b'0')) {
382 mantissa = if negative {
383 mantissa
384 .checked_mul(10)
385 .and_then(|value| value.checked_sub(digit))
386 } else {
387 mantissa
388 .checked_mul(10)
389 .and_then(|value| value.checked_add(digit))
390 }
391 .ok_or_else(|| format!("Decimal value '{value}' exceeds i128 range"))?;
392 }
393 Ok((mantissa, precision))
394}
395
396pub(super) fn remove_underscores(value: &str) -> Cow<'_, str> {
397 if value.contains('_') {
398 Cow::Owned(value.replace('_', ""))
399 } else {
400 Cow::Borrowed(value)
401 }
402}
403
404pub(crate) fn format_scaled_u128(raw: u128, precision: u8) -> String {
405 if precision == 0 {
406 return raw.to_string();
407 }
408
409 let scale = 10_u128.pow(u32::from(precision));
410 format!(
411 "{}.{:0>width$}",
412 raw / scale,
413 raw % scale,
414 width = usize::from(precision),
415 )
416}
417
418#[must_use]
422pub(crate) fn checked_mul_div_fixed(lhs: QuantityRaw, rhs: QuantityRaw) -> Option<QuantityRaw> {
423 checked_mul_div_raw(lhs, rhs, FIXED_SCALAR_RAW)
424}
425
426#[must_use]
429pub(crate) fn checked_mul_div_raw(
430 lhs: QuantityRaw,
431 rhs: QuantityRaw,
432 scalar: QuantityRaw,
433) -> Option<QuantityRaw> {
434 let lhs_whole = lhs / scalar;
435 let lhs_remainder = lhs % scalar;
436 let rhs_whole = rhs / scalar;
437 let rhs_remainder = rhs % scalar;
438
439 lhs_whole
440 .checked_mul(rhs)
441 .and_then(|whole| {
442 lhs_remainder
443 .checked_mul(rhs_whole)
444 .and_then(|mixed| whole.checked_add(mixed))
445 })
446 .and_then(|whole_and_mixed| {
447 lhs_remainder
448 .checked_mul(rhs_remainder)
449 .map(|fractional| fractional / scalar)
450 .and_then(|fractional| whole_and_mixed.checked_add(fractional))
451 })
452}
453
454const _: () = {
455 assert!(FIXED_SCALAR_RAW > 0);
456 assert!((FIXED_SCALAR_RAW as f64).to_bits() == FIXED_SCALAR.to_bits());
457 let max_remainder = FIXED_SCALAR_RAW - 1;
458 assert!(max_remainder.checked_mul(max_remainder).is_some());
459};
460
461#[inline(always)]
471fn should_skip_validation(precision: u8) -> bool {
472 #[cfg(not(feature = "defi"))]
473 debug_assert!(
474 precision <= FIXED_PRECISION,
475 "precision {precision} exceeds FIXED_PRECISION {FIXED_PRECISION}: \
476 raw value validation is not possible at this precision"
477 );
478
479 precision >= FIXED_PRECISION
480}
481
482#[cold]
484fn invalid_raw_error(
485 raw: impl Display,
486 precision: u8,
487 remainder: impl Display,
488 scale: impl Display,
489) -> anyhow::Error {
490 anyhow::anyhow!(
491 "Invalid fixed-point raw value {raw} for precision {precision}: \
492 remainder {remainder} when divided by scale {scale}. \
493 Raw value should be a multiple of {scale}. \
494 This indicates data corruption or incorrect precision/scaling upstream"
495 )
496}
497
498#[inline(always)]
528pub fn check_fixed_raw_u128(raw: u128, precision: u8) -> anyhow::Result<()> {
529 if should_skip_validation(precision) {
530 return Ok(());
531 }
532
533 let exp = usize::from(FIXED_PRECISION - precision);
534 let scale = u128::from(POWERS_OF_10[exp]);
535 let remainder = raw % scale;
536
537 if remainder != 0 {
538 return Err(invalid_raw_error(raw, precision, remainder, scale));
539 }
540
541 Ok(())
542}
543
544#[inline(always)]
553pub fn check_fixed_raw_u64(raw: u64, precision: u8) -> anyhow::Result<()> {
554 if should_skip_validation(precision) {
555 return Ok(());
556 }
557
558 let exp = usize::from(FIXED_PRECISION - precision);
559 let scale = POWERS_OF_10[exp];
560 let remainder = raw % scale;
561
562 if remainder != 0 {
563 return Err(invalid_raw_error(raw, precision, remainder, scale));
564 }
565
566 Ok(())
567}
568
569#[inline(always)]
599pub fn check_fixed_raw_i128(raw: i128, precision: u8) -> anyhow::Result<()> {
600 if should_skip_validation(precision) {
601 return Ok(());
602 }
603
604 let exp = usize::from(FIXED_PRECISION - precision);
605 let scale = i128::from(POWERS_OF_10[exp]);
606 let remainder = raw % scale;
607
608 if remainder != 0 {
609 return Err(invalid_raw_error(raw, precision, remainder, scale));
610 }
611
612 Ok(())
613}
614
615#[inline(always)]
624pub fn check_fixed_raw_i64(raw: i64, precision: u8) -> anyhow::Result<()> {
625 if should_skip_validation(precision) {
626 return Ok(());
627 }
628
629 let exp = usize::from(FIXED_PRECISION - precision);
630 let scale = POWERS_OF_10[exp].cast_signed();
631 let remainder = raw % scale;
632
633 if remainder != 0 {
634 return Err(invalid_raw_error(raw, precision, remainder, scale));
635 }
636
637 Ok(())
638}
639
640#[must_use]
657pub fn correct_raw_u128(raw: u128, precision: u8) -> u128 {
658 if precision >= FIXED_PRECISION {
659 return raw;
660 }
661 let exp = usize::from(FIXED_PRECISION - precision);
662 let scale = u128::from(POWERS_OF_10[exp]);
663 let half_scale = scale / 2;
664 let remainder = raw % scale;
665 if remainder == 0 {
666 raw
667 } else if remainder >= half_scale {
668 raw.checked_add(scale - remainder)
669 .unwrap_or(raw - remainder)
670 } else {
671 raw - remainder
672 }
673}
674
675#[must_use]
683pub fn correct_raw_u64(raw: u64, precision: u8) -> u64 {
684 if precision >= FIXED_PRECISION {
685 return raw;
686 }
687 let exp = usize::from(FIXED_PRECISION - precision);
688 let scale = POWERS_OF_10[exp];
689 let half_scale = scale / 2;
690 let remainder = raw % scale;
691 if remainder == 0 {
692 raw
693 } else if remainder >= half_scale {
694 raw.checked_add(scale - remainder)
695 .unwrap_or(raw - remainder)
696 } else {
697 raw - remainder
698 }
699}
700
701#[must_use]
709pub fn correct_raw_i128(raw: i128, precision: u8) -> i128 {
710 if precision >= FIXED_PRECISION {
711 return raw;
712 }
713 let exp = usize::from(FIXED_PRECISION - precision);
714 let scale = i128::from(POWERS_OF_10[exp]);
715 let half_scale = scale / 2;
716 let remainder = raw % scale;
717 if remainder == 0 {
718 raw
719 } else if raw >= 0 {
720 if remainder >= half_scale {
721 raw.checked_add(scale - remainder)
722 .unwrap_or(raw - remainder)
723 } else {
724 raw - remainder
725 }
726 } else {
727 if remainder.abs() >= half_scale {
729 raw.checked_sub(scale + remainder)
730 .unwrap_or(raw - remainder)
731 } else {
732 raw - remainder
733 }
734 }
735}
736
737#[must_use]
745pub fn correct_raw_i64(raw: i64, precision: u8) -> i64 {
746 if precision >= FIXED_PRECISION {
747 return raw;
748 }
749 let exp = usize::from(FIXED_PRECISION - precision);
750 let scale = POWERS_OF_10[exp].cast_signed();
751 let half_scale = scale / 2;
752 let remainder = raw % scale;
753 if remainder == 0 {
754 raw
755 } else if raw >= 0 {
756 if remainder >= half_scale {
757 raw.checked_add(scale - remainder)
758 .unwrap_or(raw - remainder)
759 } else {
760 raw - remainder
761 }
762 } else {
763 if remainder.abs() >= half_scale {
765 raw.checked_sub(scale + remainder)
766 .unwrap_or(raw - remainder)
767 } else {
768 raw - remainder
769 }
770 }
771}
772
773#[must_use]
779#[inline]
780pub fn correct_price_raw(raw: PriceRaw, precision: u8) -> PriceRaw {
781 #[cfg(feature = "high-precision")]
782 {
783 correct_raw_i128(raw, precision)
784 }
785 #[cfg(not(feature = "high-precision"))]
786 {
787 correct_raw_i64(raw, precision)
788 }
789}
790
791#[must_use]
797#[inline]
798pub fn correct_quantity_raw(raw: QuantityRaw, precision: u8) -> QuantityRaw {
799 #[cfg(feature = "high-precision")]
800 {
801 correct_raw_u128(raw, precision)
802 }
803 #[cfg(not(feature = "high-precision"))]
804 {
805 correct_raw_u64(raw, precision)
806 }
807}
808
809#[must_use]
814#[inline]
815pub fn bankers_round(mantissa: i128, excess: u32) -> i128 {
816 if excess == 0 {
817 return mantissa;
818 }
819
820 if excess >= 39 {
822 return 0;
823 }
824
825 let divisor = 10i128.pow(excess);
826 let quotient = mantissa / divisor;
827 let remainder = mantissa % divisor;
828 let half = divisor / 2;
829
830 if remainder.abs() > half || (remainder.abs() == half && quotient % 2 != 0) {
831 quotient + mantissa.signum()
832 } else {
833 quotient
834 }
835}
836
837pub fn mantissa_exponent_to_fixed_i128(
852 mantissa: i128,
853 exponent: i8,
854 precision: u8,
855) -> CorrectnessResult<i128> {
856 check_fixed_precision(precision)?;
857
858 let precision_i16 = i16::from(precision);
859 let target_scale = i16::from(FIXED_PRECISION).max(precision_i16);
860 let frac_digits = -i16::from(exponent);
861
862 let mantissa = if frac_digits > precision_i16 {
863 let excess = u32::from((frac_digits - precision_i16).cast_unsigned());
864 bankers_round(mantissa, excess)
865 } else {
866 mantissa
867 };
868
869 let scale_after_rounding = frac_digits.min(precision_i16);
870 let scale_exp = target_scale - scale_after_rounding;
871 if scale_exp > 38 {
872 return Err(CorrectnessError::PredicateViolation {
873 message: format!(
874 "Exponent {exponent} produces scale factor 10^{scale_exp} which exceeds i128 range"
875 ),
876 });
877 }
878
879 if scale_exp >= 0 {
880 mantissa.checked_mul(10i128.pow(u32::from(scale_exp.cast_unsigned())))
881 } else {
882 Some(mantissa / 10i128.pow(u32::from((-scale_exp).cast_unsigned())))
883 }
884 .ok_or_else(|| CorrectnessError::PredicateViolation {
885 message: "Overflow when scaling mantissa to fixed precision".to_string(),
886 })
887}
888
889pub(crate) fn mantissa_exponent_to_raw_checked<R>(
890 mantissa: i128,
891 exponent: i8,
892 precision: u8,
893 context: &'static str,
894 raw_type_name: &'static str,
895 value_type_name: &'static str,
896) -> CorrectnessResult<R>
897where
898 R: TryFrom<i128>,
899{
900 check_fixed_precision(precision)?;
901
902 let raw_i128 = if mantissa == 0 {
903 0
904 } else {
905 mantissa_exponent_to_fixed_i128(mantissa, exponent, precision).map_err(|_| {
906 CorrectnessError::PredicateViolation {
907 message: format!(
908 "Overflow in {context} (mantissa={mantissa}, exponent={exponent}, precision={precision})"
909 ),
910 }
911 })?
912 };
913
914 raw_i128
915 .try_into()
916 .map_err(|_| CorrectnessError::PredicateViolation {
917 message: format!(
918 "Raw value {raw_i128} exceeds {raw_type_name} range for {value_type_name}"
919 ),
920 })
921}
922
923#[must_use]
940#[expect(
941 clippy::cast_precision_loss,
942 clippy::cast_possible_truncation,
943 reason = "f64 to fixed-point conversion is inherently lossy; callers validate range and finiteness"
944)]
945pub fn f64_to_fixed_i64(value: f64, precision: u8) -> i64 {
946 check_fixed_precision(precision).expect_display(FAILED);
947 let pow1 = 10_i64.pow(u32::from(precision));
948 let pow2 = 10_i64.pow(u32::from(FIXED_PRECISION - precision));
949 let rounded = (value * pow1 as f64).round() as i64;
950 rounded
951 .checked_mul(pow2)
952 .expect("Overflow when scaling f64 to fixed-point i64")
953}
954
955#[must_use]
965#[expect(
966 clippy::cast_precision_loss,
967 clippy::cast_possible_truncation,
968 reason = "f64 to fixed-point conversion is inherently lossy; callers validate range and finiteness"
969)]
970pub fn f64_to_fixed_i128(value: f64, precision: u8) -> i128 {
971 check_fixed_precision(precision).expect_display(FAILED);
972 let pow1 = 10_i128.pow(u32::from(precision));
973 let pow2 = 10_i128.pow(u32::from(FIXED_PRECISION - precision));
974 let rounded = (value * pow1 as f64).round() as i128;
975 rounded
976 .checked_mul(pow2)
977 .expect("Overflow when scaling f64 to fixed-point i128")
978}
979
980#[must_use]
990#[expect(
991 clippy::cast_precision_loss,
992 clippy::cast_possible_truncation,
993 clippy::cast_sign_loss,
994 reason = "f64 to fixed-point conversion is inherently lossy; callers validate range and finiteness"
995)]
996pub fn f64_to_fixed_u64(value: f64, precision: u8) -> u64 {
997 check_fixed_precision(precision).expect_display(FAILED);
998 let pow1 = 10_u64.pow(u32::from(precision));
999 let pow2 = 10_u64.pow(u32::from(FIXED_PRECISION - precision));
1000 let rounded = (value * pow1 as f64).round() as u64;
1001 rounded
1002 .checked_mul(pow2)
1003 .expect("Overflow when scaling f64 to fixed-point u64")
1004}
1005
1006#[must_use]
1016#[expect(
1017 clippy::cast_precision_loss,
1018 clippy::cast_possible_truncation,
1019 clippy::cast_sign_loss,
1020 reason = "f64 to fixed-point conversion is inherently lossy; callers validate range and finiteness"
1021)]
1022pub fn f64_to_fixed_u128(value: f64, precision: u8) -> u128 {
1023 check_fixed_precision(precision).expect_display(FAILED);
1024 let pow1 = 10_u128.pow(u32::from(precision));
1025 let pow2 = 10_u128.pow(u32::from(FIXED_PRECISION - precision));
1026 let rounded = (value * pow1 as f64).round() as u128;
1027 rounded
1028 .checked_mul(pow2)
1029 .expect("Overflow when scaling f64 to fixed-point u128")
1030}
1031
1032#[must_use]
1034#[expect(
1035 clippy::cast_precision_loss,
1036 reason = "i64 to f64 is inherently lossy above 2^53; accepted for float interop"
1037)]
1038pub fn fixed_i64_to_f64(value: i64) -> f64 {
1039 (value as f64) / FIXED_SCALAR
1040}
1041
1042#[must_use]
1044#[expect(
1045 clippy::cast_precision_loss,
1046 reason = "i128 to f64 is inherently lossy above 2^53; accepted for float interop"
1047)]
1048pub fn fixed_i128_to_f64(value: i128) -> f64 {
1049 (value as f64) / FIXED_SCALAR
1050}
1051
1052#[must_use]
1054#[expect(
1055 clippy::cast_precision_loss,
1056 reason = "u64 to f64 is inherently lossy above 2^53; accepted for float interop"
1057)]
1058pub fn fixed_u64_to_f64(value: u64) -> f64 {
1059 (value as f64) / FIXED_SCALAR
1060}
1061
1062#[must_use]
1064#[expect(
1065 clippy::cast_precision_loss,
1066 reason = "u128 to f64 is inherently lossy above 2^53; accepted for float interop"
1067)]
1068pub fn fixed_u128_to_f64(value: u128) -> f64 {
1069 (value as f64) / FIXED_SCALAR
1070}
1071
1072#[cfg(feature = "high-precision")]
1073#[cfg(test)]
1074mod tests {
1075 use nautilus_core::approx_eq;
1076 use rstest::rstest;
1077
1078 use super::*;
1079
1080 #[rstest]
1081 fn test_correct_raw_rounds_half_away_from_zero() {
1082 let precision = FIXED_PRECISION - 1;
1083
1084 assert_eq!(correct_raw_u128(20, precision), 20);
1085 assert_eq!(correct_raw_u128(14, precision), 10);
1086 assert_eq!(correct_raw_u128(15, precision), 20);
1087 assert_eq!(correct_raw_u64(14, precision), 10);
1088 assert_eq!(correct_raw_u64(15, precision), 20);
1089 assert_eq!(correct_raw_i128(15, precision), 20);
1090 assert_eq!(correct_raw_i128(-14, precision), -10);
1091 assert_eq!(correct_raw_i128(-15, precision), -20);
1092 assert_eq!(correct_raw_i64(15, precision), 20);
1093 assert_eq!(correct_raw_i64(-14, precision), -10);
1094 assert_eq!(correct_raw_i64(-15, precision), -20);
1095 }
1096
1097 #[rstest]
1098 fn test_f64_fixed_u128_round_trip() {
1099 let raw = f64_to_fixed_u128(1.5, 1);
1100
1101 assert_eq!(raw, 15 * 10_u128.pow(u32::from(FIXED_PRECISION - 1)));
1102 assert_eq!(fixed_u128_to_f64(raw), 1.5);
1103 }
1104
1105 #[rstest]
1106 fn test_mantissa_exponent_to_fixed_i128_allows_max_scale_factor() {
1107 let exponent = i8::try_from(38 - FIXED_PRECISION).unwrap();
1108
1109 assert_eq!(
1110 mantissa_exponent_to_fixed_i128(1, exponent, 0).unwrap(),
1111 10_i128.pow(38)
1112 );
1113 assert_eq!(
1114 mantissa_exponent_to_fixed_i128(1, exponent + 1, 0)
1115 .unwrap_err()
1116 .to_string(),
1117 format!(
1118 "Exponent {} produces scale factor 10^39 which exceeds i128 range",
1119 exponent + 1
1120 )
1121 );
1122 }
1123
1124 #[rstest]
1125 fn test_raw_scales_match_requires_equal_effective_scale() {
1126 assert!(raw_scales_match(0, FIXED_PRECISION));
1127 assert!(raw_scales_match(FIXED_PRECISION + 1, FIXED_PRECISION + 1));
1128 assert!(!raw_scales_match(FIXED_PRECISION, FIXED_PRECISION + 1));
1129 }
1130
1131 #[rstest]
1132 fn test_canonical_raw_trims_native_scale_trailing_zeros() {
1133 assert_eq!(
1134 canonical_raw(0_u128, FIXED_PRECISION + 2),
1135 (0, FIXED_PRECISION)
1136 );
1137 assert_eq!(
1138 canonical_raw(1_200_u128, FIXED_PRECISION + 2),
1139 (12, FIXED_PRECISION)
1140 );
1141 assert_eq!(
1142 canonical_raw(12_000_u128, FIXED_PRECISION + 2),
1143 (120, FIXED_PRECISION)
1144 );
1145 assert_eq!(
1146 canonical_raw(1_205_u128, FIXED_PRECISION + 2),
1147 (1_205, FIXED_PRECISION + 2)
1148 );
1149 assert_eq!(
1150 canonical_raw(5_u128, FIXED_PRECISION - 1),
1151 (5, FIXED_PRECISION)
1152 );
1153 }
1154
1155 #[rstest]
1156 fn test_compare_raw_zero_operands_are_equal_across_scales() {
1157 assert_eq!(compare_raw(0_u128, u8::MAX, 0_u128, 2), Ordering::Equal);
1158 }
1159
1160 #[rstest]
1161 #[case("")]
1162 #[case("0")]
1163 #[case("-12.3400")]
1164 #[case("+1e9")]
1165 #[case("not decimal")]
1166 fn test_remove_underscores_borrows_clean_values(#[case] input: &str) {
1167 assert!(matches!(remove_underscores(input), Cow::Borrowed(value) if value == input));
1168 }
1169
1170 #[rstest]
1171 #[case("_", "")]
1172 #[case("1_234.56", "1234.56")]
1173 #[case("_1__2_3.4_5_", "123.45")]
1174 #[case("1e_3", "1e3")]
1175 #[case("_-_1_0_._0_", "-10.0")]
1176 fn test_remove_underscores_removes_separators(#[case] input: &str, #[case] expected: &str) {
1177 assert!(matches!(remove_underscores(input), Cow::Owned(value) if value == expected));
1178 }
1179
1180 #[rstest]
1181 #[case("1.00", 100, 2)]
1182 #[case("+1.00", 100, 2)]
1183 #[case("-1.00", -100, 2)]
1184 #[case("-0.00", 0, 2)]
1185 fn test_parse_decimal_mantissa_sign(
1186 #[case] input: &str,
1187 #[case] mantissa: i128,
1188 #[case] precision: u8,
1189 ) {
1190 assert_eq!(parse_decimal_mantissa(input), Ok((mantissa, precision)));
1191 }
1192
1193 #[rstest]
1194 #[case("-+1.00")]
1195 #[case("+-1.00")]
1196 #[case("--1.00")]
1197 #[case("++1.00")]
1198 #[case("-+0.00")]
1199 fn test_parse_decimal_mantissa_rejects_multiple_signs(#[case] input: &str) {
1200 assert_eq!(
1201 parse_decimal_mantissa(input),
1202 Err(format!("Invalid decimal value '{input}'")),
1203 );
1204 assert!(input.parse::<crate::types::Price>().is_err());
1205 assert!(input.parse::<crate::types::Quantity>().is_err());
1206 assert!(
1207 format!("{input} USD")
1208 .parse::<crate::types::Money>()
1209 .is_err()
1210 );
1211 }
1212
1213 #[rstest]
1214 #[case(i128::MIN)]
1215 #[case(i128::MAX)]
1216 fn test_parse_decimal_mantissa_integer_limits(#[case] value: i128) {
1217 assert_eq!(parse_decimal_mantissa(&value.to_string()), Ok((value, 0)));
1218 }
1219
1220 #[rstest]
1221 #[case("170141183460469231731687303715884105728")]
1222 #[case("-170141183460469231731687303715884105729")]
1223 fn test_parse_decimal_mantissa_overflow(#[case] input: &str) {
1224 assert_eq!(
1225 parse_decimal_mantissa(input),
1226 Err(format!("Decimal value '{input}' exceeds i128 range")),
1227 );
1228 }
1229
1230 #[rstest]
1231 #[case(".5", 5, 1)]
1232 #[case("-.5", -5, 1)]
1233 #[case("1.", 1, 0)]
1234 #[case("0001.0200", 10200, 4)]
1235 fn test_parse_decimal_mantissa_syntax(
1236 #[case] input: &str,
1237 #[case] mantissa: i128,
1238 #[case] precision: u8,
1239 ) {
1240 assert_eq!(parse_decimal_mantissa(input), Ok((mantissa, precision)));
1241 }
1242
1243 #[rstest]
1244 #[case("")]
1245 #[case(".")]
1246 #[case("+")]
1247 #[case("-")]
1248 #[case("1.2.3")]
1249 #[case(" 1")]
1250 #[case("1 ")]
1251 #[case("1 2")]
1252 #[case("1")]
1253 fn test_parse_decimal_mantissa_invalid_syntax(#[case] input: &str) {
1254 assert_eq!(
1255 parse_decimal_mantissa(input),
1256 Err(format!("Invalid decimal value '{input}'")),
1257 );
1258 }
1259
1260 #[rstest]
1261 fn test_parse_decimal_mantissa_fraction_length_limit() {
1262 let accepted = format!("0.{}", "0".repeat(255));
1263 let rejected = format!("{accepted}0");
1264
1265 assert_eq!(parse_decimal_mantissa(&accepted), Ok((0, 255)));
1266 assert_eq!(
1267 parse_decimal_mantissa(&rejected),
1268 Err(format!(
1269 "Decimal value '{rejected}' has too many fractional digits"
1270 )),
1271 );
1272 }
1273
1274 #[rstest]
1275 fn test_decimal_string_domain_precision_limit() {
1276 use crate::types::{Price, Quantity};
1277
1278 #[cfg(feature = "defi")]
1279 let precision = crate::defi::WEI_PRECISION;
1280 #[cfg(not(feature = "defi"))]
1281 let precision = FIXED_PRECISION;
1282 let accepted = format!("0.{}1", "0".repeat(usize::from(precision - 1)));
1283 let rejected = format!("{accepted}0");
1284 let price = accepted.parse::<Price>().unwrap();
1285 let quantity = accepted.parse::<Quantity>().unwrap();
1286
1287 assert_eq!(price.raw, 1);
1288 assert_eq!(price.precision, precision);
1289 assert_eq!(quantity.raw, 1);
1290 assert_eq!(quantity.precision, precision);
1291 assert!(rejected.parse::<Price>().is_err());
1292 assert!(rejected.parse::<Quantity>().is_err());
1293 }
1294
1295 #[rstest]
1296 #[case(0, 0, "0")]
1297 #[case(125, 2, "1.25")]
1298 #[case(-1234, 2, "-12.34")]
1299 #[case(1, 16, "0.0000000000000001")]
1300 #[case(-1, 16, "-0.0000000000000001")]
1301 #[case(1_000_000_000_000_000_000, 18, "1.000000000000000000")]
1302 fn test_scaled_raw_to_decimal_matches_plain_conversion(
1303 #[case] raw: i128,
1304 #[case] precision: u8,
1305 #[case] expected: &str,
1306 ) {
1307 let plain = Decimal::from_i128_with_scale(raw, u32::from(precision));
1308 let result = scaled_raw_to_decimal(raw, precision);
1309
1310 assert_eq!(result, plain);
1311 assert_eq!(result.scale(), plain.scale());
1312 assert_eq!(result.to_string(), expected);
1313 }
1314
1315 #[rstest]
1316 #[case(80_000_000_000_000_000_000_000_000_000, 16, "8000000000000")]
1317 #[case(340_282_366_920_930_000_000_000_000_000, 16, "34028236692093")]
1318 #[case(170_141_183_460_460_000_000_000_000_000, 16, "17014118346046")]
1319 #[case(-170_141_183_460_460_000_000_000_000_000, 16, "-17014118346046")]
1320 #[case(
1323 80_000_000_000_000_005_000_000_000_000,
1324 16,
1325 "8000000000000.000500000000000"
1326 )]
1327 #[case(-80_000_000_000_000_005_000_000_000_000, 16, "-8000000000000.000500000000000")]
1328 #[case(
1329 80_000_000_000_000_000_000_000_000_001,
1330 16,
1331 "8000000000000.000000000000000"
1332 )]
1333 #[case(
1334 80_000_000_000_000_000_250_000_000_000,
1335 18,
1336 "80000000000.00000025000000000"
1337 )]
1338 #[case(-80_000_000_000_000_000_250_000_000_000, 18, "-80000000000.00000025000000000")]
1339 fn test_scaled_raw_to_decimal_beyond_mantissa_rounds_rather_than_panics(
1340 #[case] raw: i128,
1341 #[case] precision: u8,
1342 #[case] expected: &str,
1343 ) {
1344 assert_eq!(scaled_raw_to_decimal(raw, precision).to_string(), expected);
1348 }
1349
1350 #[cfg(not(feature = "defi"))]
1351 #[rstest]
1352 fn test_precision_boundaries() {
1353 assert!(check_fixed_precision(0).is_ok());
1354 assert!(check_fixed_precision(FIXED_PRECISION).is_ok());
1355 assert!(check_fixed_precision(FIXED_PRECISION + 1).is_err());
1356 }
1357
1358 #[cfg(feature = "defi")]
1359 #[rstest]
1360 fn test_precision_boundaries() {
1361 use crate::defi::WEI_PRECISION;
1362
1363 assert!(check_fixed_precision(0).is_ok());
1364 assert!(check_fixed_precision(WEI_PRECISION).is_ok());
1365 assert!(check_fixed_precision(WEI_PRECISION + 1).is_err());
1366 }
1367
1368 #[rstest]
1369 #[case(0.0)]
1370 #[case(1.0)]
1371 #[case(-1.0)]
1372 fn test_basic_roundtrip(#[case] value: f64) {
1373 for precision in 0..=FIXED_PRECISION {
1374 let fixed = f64_to_fixed_i128(value, precision);
1375 let result = fixed_i128_to_f64(fixed);
1376 assert!(approx_eq!(f64, value, result, epsilon = 0.001));
1377 }
1378 }
1379
1380 #[rstest]
1381 #[case(1_000_000.0)]
1382 #[case(-1_000_000.0)]
1383 fn test_large_value_roundtrip(#[case] value: f64) {
1384 for precision in 0..=FIXED_PRECISION {
1385 let fixed = f64_to_fixed_i128(value, precision);
1386 let result = fixed_i128_to_f64(fixed);
1387 assert!(approx_eq!(f64, value, result, epsilon = 0.000_1));
1388 }
1389 }
1390
1391 #[rstest]
1392 #[case(0, 123_456.0)]
1393 #[case(0, 123_456.7)]
1394 #[case(1, 123_456.7)]
1395 #[case(2, 123_456.78)]
1396 #[case(8, 123_456.123_456_78)]
1397 fn test_precision_specific_values_basic(#[case] precision: u8, #[case] value: f64) {
1398 let result = f64_to_fixed_i128(value, precision);
1399 let back_converted = fixed_i128_to_f64(result);
1400 let scale = 10.0_f64.powi(i32::from(precision));
1402 let expected_rounded = (value * scale).round() / scale;
1403 assert!((back_converted - expected_rounded).abs() < 1e-10);
1404 }
1405
1406 #[rstest]
1407 fn test_max_precision_values() {
1408 let test_value = 123_456.123_456_789;
1410 let result = f64_to_fixed_i128(test_value, FIXED_PRECISION);
1411 let back_converted = fixed_i128_to_f64(result);
1412 assert!((back_converted - test_value).abs() < 1e-6);
1414 }
1415
1416 #[rstest]
1417 #[case(0.0)]
1418 #[case(1.0)]
1419 #[case(1_000_000.0)]
1420 fn test_unsigned_basic_roundtrip(#[case] value: f64) {
1421 for precision in 0..=FIXED_PRECISION {
1422 let fixed = f64_to_fixed_u128(value, precision);
1423 let result = fixed_u128_to_f64(fixed);
1424 assert!(approx_eq!(f64, value, result, epsilon = 0.001));
1425 }
1426 }
1427
1428 #[rstest]
1429 #[case(0)]
1430 #[case(FIXED_PRECISION)]
1431 fn test_valid_precision(#[case] precision: u8) {
1432 let result = check_fixed_precision(precision);
1433 assert!(result.is_ok());
1434 }
1435
1436 #[cfg(not(feature = "defi"))]
1437 #[rstest]
1438 fn test_invalid_precision() {
1439 let precision = FIXED_PRECISION + 1;
1440 let result = check_fixed_precision(precision);
1441 assert!(result.is_err());
1442 }
1443
1444 #[cfg(feature = "defi")]
1445 #[rstest]
1446 fn test_invalid_precision() {
1447 use crate::defi::WEI_PRECISION;
1448 let precision = WEI_PRECISION + 1;
1449 let result = check_fixed_precision(precision);
1450 assert!(result.is_err());
1451 }
1452
1453 #[rstest]
1454 fn test_check_float_precision_matches_supported_range() {
1455 for precision in 0..=u8::MAX {
1456 let expected = if precision <= MAX_FLOAT_PRECISION {
1457 Ok(())
1458 } else {
1459 Err(CorrectnessError::PredicateViolation {
1460 message: format!(
1461 "Fixed-point precision {precision} exceeds maximum float precision 16"
1462 ),
1463 })
1464 };
1465
1466 assert_eq!(check_float_precision(precision), expected);
1467 }
1468 }
1469
1470 #[rstest]
1471 fn test_check_float_precision_error_display() {
1472 let error = check_float_precision(18).unwrap_err();
1473
1474 assert_eq!(
1475 error.to_string(),
1476 "Fixed-point precision 18 exceeds maximum float precision 16"
1477 );
1478 }
1479
1480 #[cfg(not(feature = "defi"))]
1481 #[rstest]
1482 fn test_check_fixed_precision_returns_typed_error_with_stable_display() {
1483 let error = check_fixed_precision(FIXED_PRECISION + 1).unwrap_err();
1484
1485 assert_eq!(
1486 error,
1487 CorrectnessError::PredicateViolation {
1488 message: format!(
1489 "`precision` exceeded maximum `FIXED_PRECISION` ({FIXED_PRECISION}), was {}",
1490 FIXED_PRECISION + 1
1491 ),
1492 }
1493 );
1494 assert_eq!(
1495 error.to_string(),
1496 format!(
1497 "`precision` exceeded maximum `FIXED_PRECISION` ({FIXED_PRECISION}), was {}",
1498 FIXED_PRECISION + 1
1499 )
1500 );
1501 }
1502
1503 #[cfg(feature = "defi")]
1504 #[rstest]
1505 fn test_check_fixed_precision_returns_typed_error_with_stable_display() {
1506 use crate::defi::WEI_PRECISION;
1507
1508 let error = check_fixed_precision(WEI_PRECISION + 1).unwrap_err();
1509
1510 assert_eq!(
1511 error,
1512 CorrectnessError::PredicateViolation {
1513 message: format!(
1514 "`precision` exceeded maximum `WEI_PRECISION` (18), was {}",
1515 WEI_PRECISION + 1
1516 ),
1517 }
1518 );
1519 assert_eq!(
1520 error.to_string(),
1521 format!(
1522 "`precision` exceeded maximum `WEI_PRECISION` (18), was {}",
1523 WEI_PRECISION + 1
1524 )
1525 );
1526 }
1527
1528 #[rstest]
1529 #[case(0, 0.0)]
1530 #[case(1, 1.0)]
1531 #[case(1, 1.1)]
1532 #[case(9, 0.000_000_001)]
1533 #[case(16, 0.000_000_000_000_000_1)]
1534 #[case(0, -0.0)]
1535 #[case(1, -1.0)]
1536 #[case(1, -1.1)]
1537 #[case(9, -0.000_000_001)]
1538 #[case(16, -0.000_000_000_000_000_1)]
1539 fn test_f64_to_fixed_i128_to_fixed(#[case] precision: u8, #[case] value: f64) {
1540 let fixed = f64_to_fixed_i128(value, precision);
1541 let result = fixed_i128_to_f64(fixed);
1542 assert_eq!(result, value);
1543 }
1544
1545 #[rstest]
1546 #[case(0, 0.0)]
1547 #[case(1, 1.0)]
1548 #[case(1, 1.1)]
1549 #[case(9, 0.000_000_001)]
1550 #[case(16, 0.000_000_000_000_000_1)]
1551 fn test_f64_to_fixed_u128_to_fixed(#[case] precision: u8, #[case] value: f64) {
1552 let fixed = f64_to_fixed_u128(value, precision);
1553 let result = fixed_u128_to_f64(fixed);
1554 assert_eq!(result, value);
1555 }
1556
1557 #[rstest]
1558 #[case(0, 123_456.0)]
1559 #[case(0, 123_456.7)]
1560 #[case(0, 123_456.4)]
1561 #[case(1, 123_456.0)]
1562 #[case(1, 123_456.7)]
1563 #[case(1, 123_456.4)]
1564 #[case(2, 123_456.0)]
1565 #[case(2, 123_456.7)]
1566 #[case(2, 123_456.4)]
1567 fn test_f64_to_fixed_i128_with_precision(#[case] precision: u8, #[case] value: f64) {
1568 let result = f64_to_fixed_i128(value, precision);
1569
1570 let pow1 = 10_i128.pow(u32::from(precision));
1572 let pow2 = 10_i128.pow(u32::from(FIXED_PRECISION - precision));
1573 let rounded = (value * pow1 as f64).round() as i128;
1574 let expected = rounded * pow2;
1575
1576 assert_eq!(
1577 result, expected,
1578 "Failed for precision {precision}, value {value}: got {result}, expected {expected}"
1579 );
1580 }
1581
1582 #[rstest]
1583 #[case(0, 5.555_555_555_555_555)]
1584 #[case(1, 5.555_555_555_555_555)]
1585 #[case(2, 5.555_555_555_555_555)]
1586 #[case(3, 5.555_555_555_555_555)]
1587 #[case(4, 5.555_555_555_555_555)]
1588 #[case(5, 5.555_555_555_555_555)]
1589 #[case(6, 5.555_555_555_555_555)]
1590 #[case(7, 5.555_555_555_555_555)]
1591 #[case(8, 5.555_555_555_555_555)]
1592 #[case(9, 5.555_555_555_555_555)]
1593 #[case(10, 5.555_555_555_555_555)]
1594 #[case(11, 5.555_555_555_555_555)]
1595 #[case(12, 5.555_555_555_555_555)]
1596 #[case(13, 5.555_555_555_555_555)]
1597 #[case(14, 5.555_555_555_555_555)]
1598 #[case(15, 5.555_555_555_555_555)]
1599 #[case(0, -5.555_555_555_555_555)]
1600 #[case(1, -5.555_555_555_555_555)]
1601 #[case(2, -5.555_555_555_555_555)]
1602 #[case(3, -5.555_555_555_555_555)]
1603 #[case(4, -5.555_555_555_555_555)]
1604 #[case(5, -5.555_555_555_555_555)]
1605 #[case(6, -5.555_555_555_555_555)]
1606 #[case(7, -5.555_555_555_555_555)]
1607 #[case(8, -5.555_555_555_555_555)]
1608 #[case(9, -5.555_555_555_555_555)]
1609 #[case(10, -5.555_555_555_555_555)]
1610 #[case(11, -5.555_555_555_555_555)]
1611 #[case(12, -5.555_555_555_555_555)]
1612 #[case(13, -5.555_555_555_555_555)]
1613 #[case(14, -5.555_555_555_555_555)]
1614 #[case(15, -5.555_555_555_555_555)]
1615 fn test_f64_to_fixed_i128(#[case] precision: u8, #[case] value: f64) {
1616 if precision > FIXED_PRECISION {
1618 return;
1619 }
1620
1621 let result = f64_to_fixed_i128(value, precision);
1622
1623 let pow1 = 10_i128.pow(u32::from(precision));
1625 let pow2 = 10_i128.pow(u32::from(FIXED_PRECISION - precision));
1626 let rounded = (value * pow1 as f64).round() as i128;
1627 let expected = rounded * pow2;
1628
1629 assert_eq!(
1630 result, expected,
1631 "Failed for precision {precision}, value {value}: got {result}, expected {expected}"
1632 );
1633 }
1634
1635 #[rstest]
1636 #[case(0, 5.555_555_555_555_555)]
1637 #[case(1, 5.555_555_555_555_555)]
1638 #[case(2, 5.555_555_555_555_555)]
1639 #[case(3, 5.555_555_555_555_555)]
1640 #[case(4, 5.555_555_555_555_555)]
1641 #[case(5, 5.555_555_555_555_555)]
1642 #[case(6, 5.555_555_555_555_555)]
1643 #[case(7, 5.555_555_555_555_555)]
1644 #[case(8, 5.555_555_555_555_555)]
1645 #[case(9, 5.555_555_555_555_555)]
1646 #[case(10, 5.555_555_555_555_555)]
1647 #[case(11, 5.555_555_555_555_555)]
1648 #[case(12, 5.555_555_555_555_555)]
1649 #[case(13, 5.555_555_555_555_555)]
1650 #[case(14, 5.555_555_555_555_555)]
1651 #[case(15, 5.555_555_555_555_555)]
1652 #[case(16, 5.555_555_555_555_555)]
1653 fn test_f64_to_fixed_u64(#[case] precision: u8, #[case] value: f64) {
1654 if precision > FIXED_PRECISION {
1656 return;
1657 }
1658
1659 let result = f64_to_fixed_u128(value, precision);
1660
1661 let pow1 = 10_u128.pow(u32::from(precision));
1663 let pow2 = 10_u128.pow(u32::from(FIXED_PRECISION - precision));
1664 let rounded = (value * pow1 as f64).round() as u128;
1665 let expected = rounded * pow2;
1666
1667 assert_eq!(
1668 result, expected,
1669 "Failed for precision {precision}, value {value}: got {result}, expected {expected}"
1670 );
1671 }
1672
1673 #[rstest]
1674 fn test_fixed_i128_to_f64(
1675 #[values(1, -1, 2, -2, 10, -10, 100, -100, 1_000, -1_000, -10_000, -100_000)] value: i128,
1676 ) {
1677 assert_eq!(fixed_i128_to_f64(value), value as f64 / FIXED_SCALAR);
1678 }
1679
1680 #[rstest]
1681 fn test_fixed_u128_to_f64(
1682 #[values(
1683 0,
1684 1,
1685 2,
1686 3,
1687 10,
1688 100,
1689 1_000,
1690 10_000,
1691 100_000,
1692 1_000_000,
1693 10_000_000,
1694 100_000_000,
1695 1_000_000_000,
1696 10_000_000_000,
1697 100_000_000_000,
1698 1_000_000_000_000,
1699 10_000_000_000_000,
1700 100_000_000_000_000,
1701 1_000_000_000_000_000,
1702 10_000_000_000_000_000,
1703 100_000_000_000_000_000,
1704 1_000_000_000_000_000_000,
1705 10_000_000_000_000_000_000,
1706 100_000_000_000_000_000_000
1707 )]
1708 value: u128,
1709 ) {
1710 let result = fixed_u128_to_f64(value);
1711 assert_eq!(result, (value as f64) / FIXED_SCALAR);
1712 }
1713
1714 #[rstest]
1719 #[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) {
1725 assert!(check_fixed_raw_u128(raw, precision).is_ok());
1726 }
1727
1728 #[rstest]
1729 #[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) {
1735 assert!(check_fixed_raw_u128(raw, precision).is_err());
1736 }
1737
1738 #[rstest]
1739 fn test_check_fixed_raw_u128_at_max_precision() {
1740 assert!(check_fixed_raw_u128(0, FIXED_PRECISION).is_ok());
1742 assert!(check_fixed_raw_u128(1, FIXED_PRECISION).is_ok());
1743 assert!(check_fixed_raw_u128(123_456_789, FIXED_PRECISION).is_ok());
1744 assert!(check_fixed_raw_u128(u128::MAX, FIXED_PRECISION).is_ok());
1745 }
1746
1747 #[rstest]
1748 #[case(0, 0)]
1749 #[case(0, 10_000_000_000_000_000)]
1750 #[case(0, -10_000_000_000_000_000)]
1751 #[case(8, 12_345_678_900_000_000)]
1752 #[case(8, -12_345_678_900_000_000)]
1753 fn test_check_fixed_raw_i128_valid(#[case] precision: u8, #[case] raw: i128) {
1754 assert!(check_fixed_raw_i128(raw, precision).is_ok());
1755 }
1756
1757 #[rstest]
1758 #[case(0, 1)]
1759 #[case(0, -1)]
1760 #[case(0, 9_999_999_999_999_999)]
1761 #[case(0, -9_999_999_999_999_999)]
1762 fn test_check_fixed_raw_i128_invalid(#[case] precision: u8, #[case] raw: i128) {
1763 assert!(check_fixed_raw_i128(raw, precision).is_err());
1764 }
1765
1766 #[rstest]
1767 fn test_check_fixed_raw_i128_at_max_precision() {
1768 assert!(check_fixed_raw_i128(0, FIXED_PRECISION).is_ok());
1769 assert!(check_fixed_raw_i128(1, FIXED_PRECISION).is_ok());
1770 assert!(check_fixed_raw_i128(-1, FIXED_PRECISION).is_ok());
1771 assert!(check_fixed_raw_i128(i128::MAX, FIXED_PRECISION).is_ok());
1772 assert!(check_fixed_raw_i128(i128::MIN, FIXED_PRECISION).is_ok());
1773 }
1774
1775 #[rstest]
1776 #[should_panic(expected = "Overflow when scaling f64 to fixed-point i128")]
1777 fn test_f64_to_fixed_i128_overflow_panics() {
1778 let _ = f64_to_fixed_i128(1e30, 0);
1779 }
1780
1781 #[rstest]
1782 #[should_panic(expected = "Overflow when scaling f64 to fixed-point u128")]
1783 fn test_f64_to_fixed_u128_overflow_panics() {
1784 let _ = f64_to_fixed_u128(1e30, 0);
1785 }
1786}
1787
1788#[cfg(not(feature = "high-precision"))]
1789#[cfg(test)]
1790mod tests {
1791 use nautilus_core::approx_eq;
1792 use rstest::rstest;
1793
1794 use super::*;
1795
1796 #[rstest]
1797 fn test_correct_raw_rounds_half_away_from_zero() {
1798 let precision = FIXED_PRECISION - 1;
1799
1800 assert_eq!(correct_raw_u128(20, precision), 20);
1801 assert_eq!(correct_raw_u128(14, precision), 10);
1802 assert_eq!(correct_raw_u128(15, precision), 20);
1803 assert_eq!(correct_raw_u64(14, precision), 10);
1804 assert_eq!(correct_raw_u64(15, precision), 20);
1805 assert_eq!(correct_raw_i128(15, precision), 20);
1806 assert_eq!(correct_raw_i128(-14, precision), -10);
1807 assert_eq!(correct_raw_i128(-15, precision), -20);
1808 assert_eq!(correct_raw_i64(15, precision), 20);
1809 assert_eq!(correct_raw_i64(-14, precision), -10);
1810 assert_eq!(correct_raw_i64(-15, precision), -20);
1811 }
1812
1813 #[rstest]
1814 fn test_f64_fixed_u128_round_trip() {
1815 let raw = f64_to_fixed_u128(1.5, 1);
1816
1817 assert_eq!(raw, 15 * 10_u128.pow(u32::from(FIXED_PRECISION - 1)));
1818 assert_eq!(fixed_u128_to_f64(raw), 1.5);
1819 }
1820
1821 #[rstest]
1822 fn test_mantissa_exponent_to_fixed_i128_allows_max_scale_factor() {
1823 let exponent = i8::try_from(38 - FIXED_PRECISION).unwrap();
1824
1825 assert_eq!(
1826 mantissa_exponent_to_fixed_i128(1, exponent, 0).unwrap(),
1827 10_i128.pow(38)
1828 );
1829 assert_eq!(
1830 mantissa_exponent_to_fixed_i128(1, exponent + 1, 0)
1831 .unwrap_err()
1832 .to_string(),
1833 format!(
1834 "Exponent {} produces scale factor 10^39 which exceeds i128 range",
1835 exponent + 1
1836 )
1837 );
1838 }
1839
1840 #[rstest]
1841 fn test_raw_scales_match_requires_equal_effective_scale() {
1842 assert!(raw_scales_match(0, FIXED_PRECISION));
1843 assert!(raw_scales_match(FIXED_PRECISION + 1, FIXED_PRECISION + 1));
1844 assert!(!raw_scales_match(FIXED_PRECISION, FIXED_PRECISION + 1));
1845 }
1846
1847 #[rstest]
1848 fn test_canonical_raw_trims_native_scale_trailing_zeros() {
1849 assert_eq!(
1850 canonical_raw(0_u128, FIXED_PRECISION + 2),
1851 (0, FIXED_PRECISION)
1852 );
1853 assert_eq!(
1854 canonical_raw(1_200_u128, FIXED_PRECISION + 2),
1855 (12, FIXED_PRECISION)
1856 );
1857 assert_eq!(
1858 canonical_raw(12_000_u128, FIXED_PRECISION + 2),
1859 (120, FIXED_PRECISION)
1860 );
1861 assert_eq!(
1862 canonical_raw(1_205_u128, FIXED_PRECISION + 2),
1863 (1_205, FIXED_PRECISION + 2)
1864 );
1865 assert_eq!(
1866 canonical_raw(5_u128, FIXED_PRECISION - 1),
1867 (5, FIXED_PRECISION)
1868 );
1869 }
1870
1871 #[rstest]
1872 fn test_compare_raw_zero_operands_are_equal_across_scales() {
1873 assert_eq!(compare_raw(0_u128, u8::MAX, 0_u128, 2), Ordering::Equal);
1874 }
1875
1876 #[rstest]
1877 fn test_precision_boundaries() {
1878 assert!(check_fixed_precision(0).is_ok());
1879 assert!(check_fixed_precision(FIXED_PRECISION).is_ok());
1880 assert!(check_fixed_precision(FIXED_PRECISION + 1).is_err());
1881 }
1882
1883 #[rstest]
1884 #[case(0.0)]
1885 #[case(1.0)]
1886 #[case(-1.0)]
1887 fn test_basic_roundtrip(#[case] value: f64) {
1888 for precision in 0..=FIXED_PRECISION {
1889 let fixed = f64_to_fixed_i64(value, precision);
1890 let result = fixed_i64_to_f64(fixed);
1891 assert!(approx_eq!(f64, value, result, epsilon = 0.001));
1892 }
1893 }
1894
1895 #[rstest]
1896 #[case(1_000_000.0)]
1897 #[case(-1_000_000.0)]
1898 fn test_large_value_roundtrip(#[case] value: f64) {
1899 for precision in 0..=FIXED_PRECISION {
1900 let fixed = f64_to_fixed_i64(value, precision);
1901 let result = fixed_i64_to_f64(fixed);
1902 assert!(approx_eq!(f64, value, result, epsilon = 0.000_1));
1903 }
1904 }
1905
1906 #[rstest]
1907 #[case(0, 123_456.0, 123_456_000_000_000)]
1908 #[case(0, 123_456.7, 123_457_000_000_000)]
1909 #[case(1, 123_456.7, 123_456_700_000_000)]
1910 #[case(2, 123_456.78, 123_456_780_000_000)]
1911 #[case(8, 123_456.123_456_78, 123_456_123_456_780)]
1912 #[case(9, 123_456.123_456_789, 123_456_123_456_789)]
1913 fn test_precision_specific_values(
1914 #[case] precision: u8,
1915 #[case] value: f64,
1916 #[case] expected: i64,
1917 ) {
1918 assert_eq!(f64_to_fixed_i64(value, precision), expected);
1919 }
1920
1921 #[rstest]
1922 #[case(0.0)]
1923 #[case(1.0)]
1924 #[case(1_000_000.0)]
1925 fn test_unsigned_basic_roundtrip(#[case] value: f64) {
1926 for precision in 0..=FIXED_PRECISION {
1927 let fixed = f64_to_fixed_u64(value, precision);
1928 let result = fixed_u64_to_f64(fixed);
1929 assert!(approx_eq!(f64, value, result, epsilon = 0.001));
1930 }
1931 }
1932
1933 #[rstest]
1934 #[case(0, 1.4, 1.0)]
1935 #[case(0, 1.5, 2.0)]
1936 #[case(0, 1.6, 2.0)]
1937 #[case(1, 1.44, 1.4)]
1938 #[case(1, 1.45, 1.5)]
1939 #[case(1, 1.46, 1.5)]
1940 #[case(2, 1.444, 1.44)]
1941 #[case(2, 1.445, 1.45)]
1942 #[case(2, 1.446, 1.45)]
1943 fn test_rounding(#[case] precision: u8, #[case] input: f64, #[case] expected: f64) {
1944 let fixed = f64_to_fixed_i128(input, precision);
1945 assert!(approx_eq!(
1946 f64,
1947 fixed_i128_to_f64(fixed),
1948 expected,
1949 epsilon = 0.000_000_001
1950 ));
1951 }
1952
1953 #[rstest]
1954 fn test_special_values() {
1955 assert_eq!(f64_to_fixed_i128(0.0, FIXED_PRECISION), 0);
1957 assert_eq!(f64_to_fixed_i128(-0.0, FIXED_PRECISION), 0);
1958
1959 let smallest_positive = 1.0 / FIXED_SCALAR;
1961 let fixed_smallest = f64_to_fixed_i128(smallest_positive, FIXED_PRECISION);
1962 assert_eq!(fixed_smallest, 1);
1963
1964 let large_int = 1_000_000_000.0;
1966 let fixed_large = f64_to_fixed_i128(large_int, 0);
1967 assert_eq!(fixed_i128_to_f64(fixed_large), large_int);
1968 }
1969
1970 #[rstest]
1971 #[case(0)]
1972 #[case(FIXED_PRECISION)]
1973 fn test_valid_precision(#[case] precision: u8) {
1974 let result = check_fixed_precision(precision);
1975 assert!(result.is_ok());
1976 }
1977
1978 #[rstest]
1979 fn test_invalid_precision() {
1980 let precision = FIXED_PRECISION + 1;
1981 let result = check_fixed_precision(precision);
1982 assert!(result.is_err());
1983 }
1984
1985 #[rstest]
1986 #[case(0, 0.0)]
1987 #[case(1, 1.0)]
1988 #[case(1, 1.1)]
1989 #[case(9, 0.000_000_001)]
1990 #[case(0, -0.0)]
1991 #[case(1, -1.0)]
1992 #[case(1, -1.1)]
1993 #[case(9, -0.000_000_001)]
1994 fn test_f64_to_fixed_i64_to_fixed(#[case] precision: u8, #[case] value: f64) {
1995 let fixed = f64_to_fixed_i64(value, precision);
1996 let result = fixed_i64_to_f64(fixed);
1997 assert_eq!(result, value);
1998 }
1999
2000 #[rstest]
2001 #[case(0, 0.0)]
2002 #[case(1, 1.0)]
2003 #[case(1, 1.1)]
2004 #[case(9, 0.000_000_001)]
2005 fn test_f64_to_fixed_u64_to_fixed(#[case] precision: u8, #[case] value: f64) {
2006 let fixed = f64_to_fixed_u64(value, precision);
2007 let result = fixed_u64_to_f64(fixed);
2008 assert_eq!(result, value);
2009 }
2010
2011 #[rstest]
2012 #[case(0, 123_456.0, 123_456_000_000_000)]
2013 #[case(0, 123_456.7, 123_457_000_000_000)]
2014 #[case(0, 123_456.4, 123_456_000_000_000)]
2015 #[case(1, 123_456.0, 123_456_000_000_000)]
2016 #[case(1, 123_456.7, 123_456_700_000_000)]
2017 #[case(1, 123_456.4, 123_456_400_000_000)]
2018 #[case(2, 123_456.0, 123_456_000_000_000)]
2019 #[case(2, 123_456.7, 123_456_700_000_000)]
2020 #[case(2, 123_456.4, 123_456_400_000_000)]
2021 fn test_f64_to_fixed_i64_with_precision(
2022 #[case] precision: u8,
2023 #[case] value: f64,
2024 #[case] expected: i64,
2025 ) {
2026 assert_eq!(f64_to_fixed_i64(value, precision), expected);
2027 }
2028
2029 #[rstest]
2030 #[case(0, 5.5, 6_000_000_000)]
2031 #[case(1, 5.55, 5_600_000_000)]
2032 #[case(2, 5.555, 5_560_000_000)]
2033 #[case(3, 5.5555, 5_556_000_000)]
2034 #[case(4, 5.55555, 5_555_600_000)]
2035 #[case(5, 5.555_555, 5_555_560_000)]
2036 #[case(6, 5.555_555_5, 5_555_556_000)]
2037 #[case(7, 5.555_555_55, 5_555_555_600)]
2038 #[case(8, 5.555_555_555, 5_555_555_560)]
2039 #[case(9, 5.555_555_555_5, 5_555_555_556)]
2040 #[case(0, -5.5, -6_000_000_000)]
2041 #[case(1, -5.55, -5_600_000_000)]
2042 #[case(2, -5.555, -5_560_000_000)]
2043 #[case(3, -5.5555, -5_556_000_000)]
2044 #[case(4, -5.55555, -5_555_600_000)]
2045 #[case(5, -5.555_555, -5_555_560_000)]
2046 #[case(6, -5.555_555_5, -5_555_556_000)]
2047 #[case(7, -5.555_555_55, -5_555_555_600)]
2048 #[case(8, -5.555_555_555, -5_555_555_560)]
2049 #[case(9, -5.555_555_555_5, -5_555_555_556)]
2050 fn test_f64_to_fixed_i64(#[case] precision: u8, #[case] value: f64, #[case] expected: i64) {
2051 assert_eq!(f64_to_fixed_i64(value, precision), expected);
2052 }
2053
2054 #[rstest]
2055 #[case(0, 5.5, 6_000_000_000)]
2056 #[case(1, 5.55, 5_600_000_000)]
2057 #[case(2, 5.555, 5_560_000_000)]
2058 #[case(3, 5.5555, 5_556_000_000)]
2059 #[case(4, 5.55555, 5_555_600_000)]
2060 #[case(5, 5.555_555, 5_555_560_000)]
2061 #[case(6, 5.555_555_5, 5_555_556_000)]
2062 #[case(7, 5.555_555_55, 5_555_555_600)]
2063 #[case(8, 5.555_555_555, 5_555_555_560)]
2064 #[case(9, 5.555_555_555_5, 5_555_555_556)]
2065 fn test_f64_to_fixed_u64(#[case] precision: u8, #[case] value: f64, #[case] expected: u64) {
2066 assert_eq!(f64_to_fixed_u64(value, precision), expected);
2067 }
2068
2069 #[rstest]
2070 fn test_fixed_i64_to_f64(
2071 #[values(1, -1, 2, -2, 10, -10, 100, -100, 1_000, -1_000)] value: i64,
2072 ) {
2073 assert_eq!(fixed_i64_to_f64(value), value as f64 / FIXED_SCALAR);
2074 }
2075
2076 #[rstest]
2077 fn test_fixed_u64_to_f64(
2078 #[values(
2079 0,
2080 1,
2081 2,
2082 3,
2083 10,
2084 100,
2085 1_000,
2086 10_000,
2087 100_000,
2088 1_000_000,
2089 10_000_000,
2090 100_000_000,
2091 1_000_000_000,
2092 10_000_000_000,
2093 100_000_000_000,
2094 1_000_000_000_000,
2095 10_000_000_000_000,
2096 100_000_000_000_000,
2097 1_000_000_000_000_000
2098 )]
2099 value: u64,
2100 ) {
2101 let result = fixed_u64_to_f64(value);
2102 assert_eq!(result, (value as f64) / FIXED_SCALAR);
2103 }
2104
2105 #[rstest]
2106 #[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) {
2112 assert!(check_fixed_raw_u64(raw, precision).is_ok());
2113 }
2114
2115 #[rstest]
2116 #[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) {
2123 assert!(check_fixed_raw_u64(raw, precision).is_err());
2124 }
2125
2126 #[rstest]
2127 fn test_check_fixed_raw_u64_at_max_precision() {
2128 assert!(check_fixed_raw_u64(0, FIXED_PRECISION).is_ok());
2130 assert!(check_fixed_raw_u64(1, FIXED_PRECISION).is_ok());
2131 assert!(check_fixed_raw_u64(123_456_789, FIXED_PRECISION).is_ok());
2132 assert!(check_fixed_raw_u64(u64::MAX, FIXED_PRECISION).is_ok());
2133 }
2134
2135 #[rstest]
2136 #[case(0, 0)]
2137 #[case(0, 1_000_000_000)]
2138 #[case(0, -1_000_000_000)]
2139 #[case(2, 123_450_000_000)]
2140 #[case(2, -123_450_000_000)]
2141 fn test_check_fixed_raw_i64_valid(#[case] precision: u8, #[case] raw: i64) {
2142 assert!(check_fixed_raw_i64(raw, precision).is_ok());
2143 }
2144
2145 #[rstest]
2146 #[case(0, 1)]
2147 #[case(0, -1)]
2148 #[case(0, 999_999_999)]
2149 #[case(0, -999_999_999)]
2150 fn test_check_fixed_raw_i64_invalid(#[case] precision: u8, #[case] raw: i64) {
2151 assert!(check_fixed_raw_i64(raw, precision).is_err());
2152 }
2153
2154 #[rstest]
2155 fn test_check_fixed_raw_i64_at_max_precision() {
2156 assert!(check_fixed_raw_i64(0, FIXED_PRECISION).is_ok());
2157 assert!(check_fixed_raw_i64(1, FIXED_PRECISION).is_ok());
2158 assert!(check_fixed_raw_i64(-1, FIXED_PRECISION).is_ok());
2159 assert!(check_fixed_raw_i64(i64::MAX, FIXED_PRECISION).is_ok());
2160 assert!(check_fixed_raw_i64(i64::MIN, FIXED_PRECISION).is_ok());
2161 }
2162
2163 #[rstest]
2164 #[should_panic(expected = "Overflow when scaling f64 to fixed-point i64")]
2165 fn test_f64_to_fixed_i64_overflow_panics() {
2166 let _ = f64_to_fixed_i64(2e18, 0);
2167 }
2168
2169 #[rstest]
2170 #[should_panic(expected = "Overflow when scaling f64 to fixed-point u64")]
2171 fn test_f64_to_fixed_u64_overflow_panics() {
2172 let _ = f64_to_fixed_u64(2e19, 0);
2173 }
2174}
2175
2176#[cfg(test)]
2177mod bankers_round_tests {
2178 use std::str::FromStr;
2179
2180 use rstest::rstest;
2181 use rust_decimal::{Decimal, RoundingStrategy};
2182
2183 use super::*;
2184
2185 #[rstest]
2186 #[case(0, 0, 0)]
2188 #[case(1, 0, 1)]
2189 #[case(5, 0, 5)]
2190 #[case(99, 0, 99)]
2191 #[case(-7, 0, -7)]
2192 #[case(12345, 39, 0)]
2194 #[case(i128::from(i64::MAX), 100, 0)]
2195 #[case(-99999, 50, 0)]
2196 #[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)]
2253 #[case(0, 2, 0)]
2254 #[case(0, 5, 0)]
2255 #[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)]
2263 #[case(1_000_000_000_000_000_005, 1, 100_000_000_000_000_000)]
2264 fn test_bankers_round(#[case] mantissa: i128, #[case] excess: u32, #[case] expected: i128) {
2265 assert_eq!(
2266 bankers_round(mantissa, excess),
2267 expected,
2268 "bankers_round({mantissa}, {excess}) expected {expected}"
2269 );
2270 }
2271
2272 #[rstest]
2274 #[case(15, 1)]
2275 #[case(25, 1)]
2276 #[case(35, 1)]
2277 #[case(150, 2)]
2278 #[case(250, 2)]
2279 #[case(1500, 3)]
2280 #[case(2500, 3)]
2281 #[case(123_456_789, 3)]
2282 #[case(14, 1)]
2283 #[case(16, 1)]
2284 fn test_bankers_round_negative_symmetry(#[case] mantissa: i128, #[case] excess: u32) {
2285 assert_eq!(
2286 bankers_round(-mantissa, excess),
2287 -bankers_round(mantissa, excess),
2288 "Negative symmetry failed for mantissa={mantissa}, excess={excess}"
2289 );
2290 }
2291
2292 #[rstest]
2294 #[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")]
2302 #[case("-3.5", 0, "-4")]
2303 #[case("123.456", 2, "123.46")]
2304 #[case("123.455", 2, "123.46")] #[case("123.445", 2, "123.44")] fn test_bankers_round_matches_decimal(
2307 #[case] input: &str,
2308 #[case] target_precision: u8,
2309 #[case] expected: &str,
2310 ) {
2311 let dec = Decimal::from_str(input).unwrap();
2312 let expected_dec = Decimal::from_str(expected).unwrap();
2313
2314 let decimal_rounded = dec.round_dp_with_strategy(
2315 u32::from(target_precision),
2316 RoundingStrategy::MidpointNearestEven,
2317 );
2318 assert_eq!(
2319 decimal_rounded, expected_dec,
2320 "Decimal rounding sanity check failed for {input}"
2321 );
2322
2323 let mantissa = dec.mantissa();
2324 let scale = dec.scale() as u8;
2325 let excess = u32::from(scale.saturating_sub(target_precision));
2326 if excess > 0 {
2327 let rounded = bankers_round(mantissa, excess);
2328
2329 let expected_mantissa = expected_dec.mantissa();
2331 let expected_scale = expected_dec.scale() as u8;
2332 let scale_diff = u32::from(target_precision.saturating_sub(expected_scale));
2333 let normalized_expected = expected_mantissa * 10i128.pow(scale_diff);
2334
2335 assert_eq!(
2336 rounded, normalized_expected,
2337 "bankers_round disagrees with Decimal for {input} at precision {target_precision}"
2338 );
2339 }
2340 }
2341}
2342
2343#[cfg(test)]
2344mod correct_raw_tests {
2345 use rstest::rstest;
2346
2347 use super::*;
2348
2349 #[rstest]
2353 #[case(0, 0)]
2354 #[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) {
2360 assert_eq!(correct_raw_u64(raw, FIXED_PRECISION - 1), expected);
2361 }
2362
2363 #[rstest]
2364 #[case(0, 0)]
2365 #[case(14, 10)]
2366 #[case(15, 20)]
2367 #[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) {
2373 assert_eq!(correct_raw_i64(raw, FIXED_PRECISION - 1), expected);
2374 }
2375
2376 #[rstest]
2377 #[case(0, 0)]
2378 #[case(14, 10)]
2379 #[case(15, 20)]
2380 #[case(u128::MAX, u128::MAX - 5)] fn test_correct_raw_u128(#[case] raw: u128, #[case] expected: u128) {
2382 assert_eq!(correct_raw_u128(raw, FIXED_PRECISION - 1), expected);
2383 }
2384
2385 #[rstest]
2386 #[case(0, 0)]
2387 #[case(14, 10)]
2388 #[case(15, 20)]
2389 #[case(-15, -20)]
2390 #[case(i128::MAX, i128::MAX - 7)] #[case(i128::MIN, i128::MIN + 8)] fn test_correct_raw_i128(#[case] raw: i128, #[case] expected: i128) {
2393 assert_eq!(correct_raw_i128(raw, FIXED_PRECISION - 1), expected);
2394 }
2395
2396 #[rstest]
2397 fn test_correct_raw_identity_at_max_precision() {
2398 assert_eq!(correct_raw_u64(12_345, FIXED_PRECISION), 12_345);
2399 assert_eq!(correct_raw_i64(-12_345, FIXED_PRECISION), -12_345);
2400 assert_eq!(correct_raw_u128(12_345, FIXED_PRECISION), 12_345);
2401 assert_eq!(correct_raw_i128(-12_345, FIXED_PRECISION), -12_345);
2402 }
2403}
2404
2405#[cfg(test)]
2406mod checked_mul_div_tests {
2407 #[cfg(feature = "defi")]
2408 use alloy_primitives::U256;
2409 use proptest::{prelude::*, test_runner::Config as ProptestConfig};
2410 use rstest::rstest;
2411
2412 use super::{FIXED_SCALAR_RAW, checked_mul_div_fixed};
2413 use crate::types::quantity::QuantityRaw;
2414
2415 #[rstest]
2416 fn test_checked_mul_div_fixed_exact_boundaries() {
2417 let scalar = FIXED_SCALAR_RAW;
2418
2419 assert_eq!(checked_mul_div_fixed(0, QuantityRaw::MAX), Some(0));
2420 assert_eq!(checked_mul_div_fixed(QuantityRaw::MAX, 0), Some(0));
2421 assert_eq!(checked_mul_div_fixed(scalar, scalar), Some(scalar));
2422 assert_eq!(
2423 checked_mul_div_fixed(scalar - 1, scalar - 1),
2424 Some(scalar - 2)
2425 );
2426 assert_eq!(
2427 checked_mul_div_fixed(scalar + 1, scalar + 1),
2428 Some(scalar + 2)
2429 );
2430 assert_eq!(
2431 checked_mul_div_fixed(QuantityRaw::MAX, scalar),
2432 Some(QuantityRaw::MAX)
2433 );
2434 assert_eq!(
2435 checked_mul_div_fixed(scalar, QuantityRaw::MAX),
2436 Some(QuantityRaw::MAX)
2437 );
2438 assert_eq!(checked_mul_div_fixed(QuantityRaw::MAX, scalar + 1), None);
2439 assert_eq!(checked_mul_div_fixed(scalar + 1, QuantityRaw::MAX), None);
2440 }
2441
2442 #[cfg(not(feature = "high-precision"))]
2443 proptest! {
2444 #![proptest_config(ProptestConfig::with_cases(4_096))]
2445
2446 #[rstest]
2447 fn prop_checked_mul_div_fixed_matches_u128_full_range(
2448 lhs in any::<QuantityRaw>(),
2449 rhs in any::<QuantityRaw>(),
2450 ) {
2451 let expected = u128::from(lhs)
2452 .checked_mul(u128::from(rhs))
2453 .map(|product| product / u128::from(FIXED_SCALAR_RAW))
2454 .and_then(|result| QuantityRaw::try_from(result).ok());
2455
2456 prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), expected);
2457 }
2458
2459 #[rstest]
2460 fn prop_checked_mul_div_fixed_matches_u128_final_fit(
2461 (lhs, rhs) in standard_final_fit_strategy(),
2462 ) {
2463 let expected =
2464 u128::from(lhs) * u128::from(rhs) / u128::from(FIXED_SCALAR_RAW);
2465 let expected = QuantityRaw::try_from(expected).expect("strategy result fits u64");
2466
2467 prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), Some(expected));
2468 }
2469
2470 #[rstest]
2471 fn prop_checked_mul_div_fixed_avoids_u64_phantom_overflow(
2472 (lhs, rhs, expected) in standard_phantom_overflow_strategy(),
2473 ) {
2474 prop_assert!(lhs.checked_mul(rhs).is_none());
2475 prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), Some(expected));
2476 }
2477 }
2478
2479 #[cfg(feature = "high-precision")]
2480 proptest! {
2481 #![proptest_config(ProptestConfig::with_cases(4_096))]
2482
2483 #[rstest]
2484 fn prop_checked_mul_div_fixed_matches_u128_ordinary(
2485 (lhs, rhs) in high_precision_ordinary_strategy(),
2486 ) {
2487 let expected = lhs
2488 .checked_mul(rhs)
2489 .expect("ordinary strategy product fits u128")
2490 / FIXED_SCALAR_RAW;
2491
2492 prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), Some(expected));
2493 }
2494
2495 #[rstest]
2496 fn prop_checked_mul_div_fixed_avoids_u128_phantom_overflow(
2497 (lhs, rhs, expected) in high_precision_phantom_overflow_strategy(),
2498 ) {
2499 prop_assert!(lhs.checked_mul(rhs).is_none());
2500 prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), Some(expected));
2501 }
2502
2503 #[rstest]
2504 fn prop_checked_mul_div_fixed_handles_remainders_after_u128_overflow(
2505 rhs in high_precision_remainder_overflow_strategy(),
2506 ) {
2507 let lhs = 2 * FIXED_SCALAR_RAW - 1;
2508 let expected = 2 * rhs - rhs.div_ceil(FIXED_SCALAR_RAW);
2509
2510 prop_assert!(lhs.checked_mul(rhs).is_none());
2511 prop_assert_ne!(lhs % FIXED_SCALAR_RAW, 0);
2512 prop_assert_ne!(rhs % FIXED_SCALAR_RAW, 0);
2513 prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), Some(expected));
2514 prop_assert_eq!(checked_mul_div_fixed(rhs, lhs), Some(expected));
2515 }
2516
2517 #[rstest]
2518 fn prop_checked_mul_div_fixed_is_commutative(
2519 lhs in any::<QuantityRaw>(),
2520 rhs in any::<QuantityRaw>(),
2521 ) {
2522 prop_assert_eq!(
2523 checked_mul_div_fixed(lhs, rhs),
2524 checked_mul_div_fixed(rhs, lhs)
2525 );
2526 }
2527 }
2528
2529 #[cfg(feature = "defi")]
2530 proptest! {
2531 #![proptest_config(ProptestConfig::with_cases(4_096))]
2532
2533 #[rstest]
2534 fn prop_checked_mul_div_raw_matches_u256_full_range(
2535 lhs in any::<QuantityRaw>(),
2536 rhs in any::<QuantityRaw>(),
2537 precision in 16_u32..=18,
2538 ) {
2539 let scalar = 10_u128.pow(precision);
2540 let expected = U256::from(lhs) * U256::from(rhs) / U256::from(scalar);
2541 let expected = QuantityRaw::try_from(expected).ok();
2542 prop_assert_eq!(super::checked_mul_div_raw(lhs, rhs, scalar), expected);
2543 }
2544
2545 #[rstest]
2546 fn prop_checked_mul_div_fixed_matches_u256_full_range(
2547 lhs in any::<QuantityRaw>(),
2548 rhs in any::<QuantityRaw>(),
2549 ) {
2550 let expected = U256::from(lhs)
2551 .checked_mul(U256::from(rhs))
2552 .expect("u128 product fits U256")
2553 / U256::from(FIXED_SCALAR_RAW);
2554 let expected = QuantityRaw::try_from(expected).ok();
2555
2556 prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), expected);
2557 }
2558 }
2559
2560 #[cfg(not(feature = "high-precision"))]
2561 fn standard_final_fit_strategy() -> impl Strategy<Value = (QuantityRaw, QuantityRaw)> {
2562 let scalar = FIXED_SCALAR_RAW;
2563
2564 (0_u64..=1_000, 0_u64..=1_000, 0_u64..scalar, 0_u64..scalar).prop_map(
2565 move |(lhs_whole, rhs_whole, lhs_remainder, rhs_remainder)| {
2566 (
2567 lhs_whole * scalar + lhs_remainder,
2568 rhs_whole * scalar + rhs_remainder,
2569 )
2570 },
2571 )
2572 }
2573
2574 #[cfg(not(feature = "high-precision"))]
2575 fn standard_phantom_overflow_strategy()
2576 -> impl Strategy<Value = (QuantityRaw, QuantityRaw, QuantityRaw)> {
2577 let scalar = FIXED_SCALAR_RAW;
2578
2579 (8_000_000_000_u64..=9_000_000_000, 0_u64..scalar).prop_map(
2580 move |(lhs_whole, rhs_remainder)| {
2581 let lhs = lhs_whole * scalar;
2582 let rhs = scalar + rhs_remainder;
2583 (lhs, rhs, lhs_whole * rhs)
2584 },
2585 )
2586 }
2587
2588 #[cfg(feature = "high-precision")]
2589 fn high_precision_ordinary_strategy() -> impl Strategy<Value = (QuantityRaw, QuantityRaw)> {
2590 let scalar = FIXED_SCALAR_RAW;
2591
2592 (
2593 0_u128..=1_000,
2594 0_u128..=1_000,
2595 0_u128..scalar,
2596 0_u128..scalar,
2597 )
2598 .prop_map(
2599 move |(lhs_whole, rhs_whole, lhs_remainder, rhs_remainder)| {
2600 (
2601 lhs_whole * scalar + lhs_remainder,
2602 rhs_whole * scalar + rhs_remainder,
2603 )
2604 },
2605 )
2606 }
2607
2608 #[cfg(feature = "high-precision")]
2609 fn high_precision_phantom_overflow_strategy()
2610 -> impl Strategy<Value = (QuantityRaw, QuantityRaw, QuantityRaw)> {
2611 let scalar = FIXED_SCALAR_RAW;
2612
2613 (10_000_u128..=1_000_000, 1_000_u128..=10_000, 0_u128..scalar).prop_map(
2614 move |(lhs_whole, rhs_whole, rhs_remainder)| {
2615 let lhs = lhs_whole * scalar;
2616 let rhs = rhs_whole * scalar + rhs_remainder;
2617 (lhs, rhs, lhs_whole * rhs)
2618 },
2619 )
2620 }
2621
2622 #[cfg(feature = "high-precision")]
2623 fn high_precision_remainder_overflow_strategy() -> impl Strategy<Value = QuantityRaw> {
2624 let lhs = 2 * FIXED_SCALAR_RAW - 1;
2625 let min = QuantityRaw::MAX / lhs + 1;
2626
2627 (min..=QuantityRaw::MAX / 2).prop_filter("rhs remainder is nonzero", |rhs| {
2628 rhs % FIXED_SCALAR_RAW != 0
2629 })
2630 }
2631}