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
215#[inline]
226#[must_use]
227pub fn raw_scales_match(a: u8, b: u8) -> bool {
228 a == b || a.max(b) <= FIXED_PRECISION
229}
230
231#[inline]
233#[must_use]
234pub(crate) fn raw_scale(precision: u8) -> u128 {
235 10_u128.pow(u32::from(precision.max(FIXED_PRECISION)))
236}
237
238#[must_use]
240pub(crate) fn canonical_raw(raw: impl Into<u128>, precision: u8) -> (u128, u8) {
241 let mut raw = raw.into();
242 let mut precision = if raw == 0 {
243 FIXED_PRECISION
244 } else {
245 precision.max(FIXED_PRECISION)
246 };
247
248 while precision > FIXED_PRECISION && raw % 10 == 0 {
249 raw /= 10;
250 precision -= 1;
251 }
252
253 (raw, precision)
254}
255
256#[inline]
257#[must_use]
258pub(crate) fn compare_raw_signed(
259 lhs: PriceRaw,
260 lhs_precision: u8,
261 rhs: PriceRaw,
262 rhs_precision: u8,
263) -> Ordering {
264 if raw_scales_match(lhs_precision, rhs_precision) {
265 return lhs.cmp(&rhs);
266 }
267
268 lhs.signum().cmp(&rhs.signum()).then_with(|| {
269 let ordering = compare_raw(
270 lhs.unsigned_abs(),
271 lhs_precision,
272 rhs.unsigned_abs(),
273 rhs_precision,
274 );
275
276 if lhs < 0 {
277 ordering.reverse()
278 } else {
279 ordering
280 }
281 })
282}
283
284#[inline]
285#[must_use]
286pub(crate) fn compare_raw(
287 lhs: impl Into<u128>,
288 lhs_precision: u8,
289 rhs: impl Into<u128>,
290 rhs_precision: u8,
291) -> Ordering {
292 let lhs = lhs.into();
293 let rhs = rhs.into();
294
295 if (lhs == 0 && rhs == 0) || raw_scales_match(lhs_precision, rhs_precision) {
297 return lhs.cmp(&rhs);
298 }
299
300 let lhs_scale = raw_scale(lhs_precision);
301 let rhs_scale = raw_scale(rhs_precision);
302 let scale = lhs_scale.max(rhs_scale);
303
304 (lhs / lhs_scale).cmp(&(rhs / rhs_scale)).then_with(|| {
306 let lhs_fraction = (lhs % lhs_scale) * (scale / lhs_scale);
307 let rhs_fraction = (rhs % rhs_scale) * (scale / rhs_scale);
308 lhs_fraction.cmp(&rhs_fraction)
309 })
310}
311
312#[must_use]
321pub(crate) fn scaled_raw_to_decimal(scaled_raw: i128, precision: u8) -> Decimal {
322 let scale = u32::from(precision);
323
324 Decimal::try_from_i128_with_scale(scaled_raw, scale).unwrap_or_else(|_| {
325 let divisor = 10_i128.pow(scale);
326
327 Decimal::from(scaled_raw / divisor)
328 + Decimal::from_i128_with_scale(scaled_raw % divisor, scale)
329 })
330}
331
332pub(crate) fn format_scaled_i128(raw: i128, precision: u8) -> String {
333 let sign = if raw < 0 { "-" } else { "" };
334 format!(
335 "{sign}{}",
336 format_scaled_u128(raw.unsigned_abs(), precision)
337 )
338}
339
340pub(crate) fn parse_decimal_mantissa(value: &str) -> Result<(i128, u8), String> {
342 let (negative, unsigned) = value
343 .strip_prefix('-')
344 .map_or((false, value), |value| (true, value));
345 let unsigned = if negative {
346 unsigned
347 } else {
348 unsigned.strip_prefix('+').unwrap_or(unsigned)
349 };
350 let (whole, fraction) = unsigned.split_once('.').unwrap_or((unsigned, ""));
351 if fraction.contains('.') {
352 return Err(format!("Invalid decimal value '{value}'"));
353 }
354 let digits = format!("{whole}{fraction}");
355 if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
356 return Err(format!("Invalid decimal value '{value}'"));
357 }
358 let precision = u8::try_from(fraction.len())
359 .map_err(|_| format!("Decimal value '{value}' has too many fractional digits"))?;
360 let mut mantissa = 0_i128;
361 for digit in digits.bytes().map(|byte| i128::from(byte - b'0')) {
362 mantissa = if negative {
363 mantissa
364 .checked_mul(10)
365 .and_then(|value| value.checked_sub(digit))
366 } else {
367 mantissa
368 .checked_mul(10)
369 .and_then(|value| value.checked_add(digit))
370 }
371 .ok_or_else(|| format!("Decimal value '{value}' exceeds i128 range"))?;
372 }
373 Ok((mantissa, precision))
374}
375
376pub(crate) fn format_scaled_u128(raw: u128, precision: u8) -> String {
377 if precision == 0 {
378 return raw.to_string();
379 }
380
381 let scale = 10_u128.pow(u32::from(precision));
382 format!(
383 "{}.{:0>width$}",
384 raw / scale,
385 raw % scale,
386 width = usize::from(precision),
387 )
388}
389
390#[must_use]
394pub(crate) fn checked_mul_div_fixed(lhs: QuantityRaw, rhs: QuantityRaw) -> Option<QuantityRaw> {
395 checked_mul_div_raw(lhs, rhs, FIXED_SCALAR_RAW)
396}
397
398#[must_use]
401pub(crate) fn checked_mul_div_raw(
402 lhs: QuantityRaw,
403 rhs: QuantityRaw,
404 scalar: QuantityRaw,
405) -> Option<QuantityRaw> {
406 let lhs_whole = lhs / scalar;
407 let lhs_remainder = lhs % scalar;
408 let rhs_whole = rhs / scalar;
409 let rhs_remainder = rhs % scalar;
410
411 lhs_whole
412 .checked_mul(rhs)
413 .and_then(|whole| {
414 lhs_remainder
415 .checked_mul(rhs_whole)
416 .and_then(|mixed| whole.checked_add(mixed))
417 })
418 .and_then(|whole_and_mixed| {
419 lhs_remainder
420 .checked_mul(rhs_remainder)
421 .map(|fractional| fractional / scalar)
422 .and_then(|fractional| whole_and_mixed.checked_add(fractional))
423 })
424}
425
426const _: () = {
427 assert!(FIXED_SCALAR_RAW > 0);
428 assert!((FIXED_SCALAR_RAW as f64).to_bits() == FIXED_SCALAR.to_bits());
429 let max_remainder = FIXED_SCALAR_RAW - 1;
430 assert!(max_remainder.checked_mul(max_remainder).is_some());
431};
432
433#[inline(always)]
443fn should_skip_validation(precision: u8) -> bool {
444 #[cfg(not(feature = "defi"))]
445 debug_assert!(
446 precision <= FIXED_PRECISION,
447 "precision {precision} exceeds FIXED_PRECISION {FIXED_PRECISION}: \
448 raw value validation is not possible at this precision"
449 );
450
451 precision >= FIXED_PRECISION
452}
453
454#[cold]
456fn invalid_raw_error(
457 raw: impl Display,
458 precision: u8,
459 remainder: impl Display,
460 scale: impl Display,
461) -> anyhow::Error {
462 anyhow::anyhow!(
463 "Invalid fixed-point raw value {raw} for precision {precision}: \
464 remainder {remainder} when divided by scale {scale}. \
465 Raw value should be a multiple of {scale}. \
466 This indicates data corruption or incorrect precision/scaling upstream"
467 )
468}
469
470#[inline(always)]
500pub fn check_fixed_raw_u128(raw: u128, precision: u8) -> anyhow::Result<()> {
501 if should_skip_validation(precision) {
502 return Ok(());
503 }
504
505 let exp = usize::from(FIXED_PRECISION - precision);
506 let scale = u128::from(POWERS_OF_10[exp]);
507 let remainder = raw % scale;
508
509 if remainder != 0 {
510 return Err(invalid_raw_error(raw, precision, remainder, scale));
511 }
512
513 Ok(())
514}
515
516#[inline(always)]
525pub fn check_fixed_raw_u64(raw: u64, precision: u8) -> anyhow::Result<()> {
526 if should_skip_validation(precision) {
527 return Ok(());
528 }
529
530 let exp = usize::from(FIXED_PRECISION - precision);
531 let scale = POWERS_OF_10[exp];
532 let remainder = raw % scale;
533
534 if remainder != 0 {
535 return Err(invalid_raw_error(raw, precision, remainder, scale));
536 }
537
538 Ok(())
539}
540
541#[inline(always)]
571pub fn check_fixed_raw_i128(raw: i128, precision: u8) -> anyhow::Result<()> {
572 if should_skip_validation(precision) {
573 return Ok(());
574 }
575
576 let exp = usize::from(FIXED_PRECISION - precision);
577 let scale = i128::from(POWERS_OF_10[exp]);
578 let remainder = raw % scale;
579
580 if remainder != 0 {
581 return Err(invalid_raw_error(raw, precision, remainder, scale));
582 }
583
584 Ok(())
585}
586
587#[inline(always)]
596pub fn check_fixed_raw_i64(raw: i64, precision: u8) -> anyhow::Result<()> {
597 if should_skip_validation(precision) {
598 return Ok(());
599 }
600
601 let exp = usize::from(FIXED_PRECISION - precision);
602 let scale = POWERS_OF_10[exp].cast_signed();
603 let remainder = raw % scale;
604
605 if remainder != 0 {
606 return Err(invalid_raw_error(raw, precision, remainder, scale));
607 }
608
609 Ok(())
610}
611
612#[must_use]
629pub fn correct_raw_u128(raw: u128, precision: u8) -> u128 {
630 if precision >= FIXED_PRECISION {
631 return raw;
632 }
633 let exp = usize::from(FIXED_PRECISION - precision);
634 let scale = u128::from(POWERS_OF_10[exp]);
635 let half_scale = scale / 2;
636 let remainder = raw % scale;
637 if remainder == 0 {
638 raw
639 } else if remainder >= half_scale {
640 raw.checked_add(scale - remainder)
641 .unwrap_or(raw - remainder)
642 } else {
643 raw - remainder
644 }
645}
646
647#[must_use]
655pub fn correct_raw_u64(raw: u64, precision: u8) -> u64 {
656 if precision >= FIXED_PRECISION {
657 return raw;
658 }
659 let exp = usize::from(FIXED_PRECISION - precision);
660 let scale = POWERS_OF_10[exp];
661 let half_scale = scale / 2;
662 let remainder = raw % scale;
663 if remainder == 0 {
664 raw
665 } else if remainder >= half_scale {
666 raw.checked_add(scale - remainder)
667 .unwrap_or(raw - remainder)
668 } else {
669 raw - remainder
670 }
671}
672
673#[must_use]
681pub fn correct_raw_i128(raw: i128, precision: u8) -> i128 {
682 if precision >= FIXED_PRECISION {
683 return raw;
684 }
685 let exp = usize::from(FIXED_PRECISION - precision);
686 let scale = i128::from(POWERS_OF_10[exp]);
687 let half_scale = scale / 2;
688 let remainder = raw % scale;
689 if remainder == 0 {
690 raw
691 } else if raw >= 0 {
692 if remainder >= half_scale {
693 raw.checked_add(scale - remainder)
694 .unwrap_or(raw - remainder)
695 } else {
696 raw - remainder
697 }
698 } else {
699 if remainder.abs() >= half_scale {
701 raw.checked_sub(scale + remainder)
702 .unwrap_or(raw - remainder)
703 } else {
704 raw - remainder
705 }
706 }
707}
708
709#[must_use]
717pub fn correct_raw_i64(raw: i64, precision: u8) -> i64 {
718 if precision >= FIXED_PRECISION {
719 return raw;
720 }
721 let exp = usize::from(FIXED_PRECISION - precision);
722 let scale = POWERS_OF_10[exp].cast_signed();
723 let half_scale = scale / 2;
724 let remainder = raw % scale;
725 if remainder == 0 {
726 raw
727 } else if raw >= 0 {
728 if remainder >= half_scale {
729 raw.checked_add(scale - remainder)
730 .unwrap_or(raw - remainder)
731 } else {
732 raw - remainder
733 }
734 } else {
735 if remainder.abs() >= half_scale {
737 raw.checked_sub(scale + remainder)
738 .unwrap_or(raw - remainder)
739 } else {
740 raw - remainder
741 }
742 }
743}
744
745#[must_use]
751#[inline]
752pub fn correct_price_raw(raw: PriceRaw, precision: u8) -> PriceRaw {
753 #[cfg(feature = "high-precision")]
754 {
755 correct_raw_i128(raw, precision)
756 }
757 #[cfg(not(feature = "high-precision"))]
758 {
759 correct_raw_i64(raw, precision)
760 }
761}
762
763#[must_use]
769#[inline]
770pub fn correct_quantity_raw(raw: QuantityRaw, precision: u8) -> QuantityRaw {
771 #[cfg(feature = "high-precision")]
772 {
773 correct_raw_u128(raw, precision)
774 }
775 #[cfg(not(feature = "high-precision"))]
776 {
777 correct_raw_u64(raw, precision)
778 }
779}
780
781#[must_use]
786#[inline]
787pub fn bankers_round(mantissa: i128, excess: u32) -> i128 {
788 if excess == 0 {
789 return mantissa;
790 }
791
792 if excess >= 39 {
794 return 0;
795 }
796
797 let divisor = 10i128.pow(excess);
798 let quotient = mantissa / divisor;
799 let remainder = mantissa % divisor;
800 let half = divisor / 2;
801
802 if remainder.abs() > half || (remainder.abs() == half && quotient % 2 != 0) {
803 quotient + mantissa.signum()
804 } else {
805 quotient
806 }
807}
808
809pub fn mantissa_exponent_to_fixed_i128(
824 mantissa: i128,
825 exponent: i8,
826 precision: u8,
827) -> CorrectnessResult<i128> {
828 check_fixed_precision(precision)?;
829
830 let precision_i16 = i16::from(precision);
831 let target_scale = i16::from(FIXED_PRECISION).max(precision_i16);
832 let frac_digits = -i16::from(exponent);
833
834 let mantissa = if frac_digits > precision_i16 {
835 let excess = u32::from((frac_digits - precision_i16).cast_unsigned());
836 bankers_round(mantissa, excess)
837 } else {
838 mantissa
839 };
840
841 let scale_after_rounding = frac_digits.min(precision_i16);
842 let scale_exp = target_scale - scale_after_rounding;
843 if scale_exp > 38 {
844 return Err(CorrectnessError::PredicateViolation {
845 message: format!(
846 "Exponent {exponent} produces scale factor 10^{scale_exp} which exceeds i128 range"
847 ),
848 });
849 }
850
851 if scale_exp >= 0 {
852 mantissa.checked_mul(10i128.pow(u32::from(scale_exp.cast_unsigned())))
853 } else {
854 Some(mantissa / 10i128.pow(u32::from((-scale_exp).cast_unsigned())))
855 }
856 .ok_or_else(|| CorrectnessError::PredicateViolation {
857 message: "Overflow when scaling mantissa to fixed precision".to_string(),
858 })
859}
860
861pub(crate) fn mantissa_exponent_to_raw_checked<R>(
862 mantissa: i128,
863 exponent: i8,
864 precision: u8,
865 context: &'static str,
866 raw_type_name: &'static str,
867 value_type_name: &'static str,
868) -> CorrectnessResult<R>
869where
870 R: TryFrom<i128>,
871{
872 check_fixed_precision(precision)?;
873
874 let raw_i128 = if mantissa == 0 {
875 0
876 } else {
877 mantissa_exponent_to_fixed_i128(mantissa, exponent, precision).map_err(|_| {
878 CorrectnessError::PredicateViolation {
879 message: format!(
880 "Overflow in {context} (mantissa={mantissa}, exponent={exponent}, precision={precision})"
881 ),
882 }
883 })?
884 };
885
886 raw_i128
887 .try_into()
888 .map_err(|_| CorrectnessError::PredicateViolation {
889 message: format!(
890 "Raw value {raw_i128} exceeds {raw_type_name} range for {value_type_name}"
891 ),
892 })
893}
894
895#[must_use]
912#[expect(
913 clippy::cast_precision_loss,
914 clippy::cast_possible_truncation,
915 reason = "f64 to fixed-point conversion is inherently lossy; callers validate range and finiteness"
916)]
917pub fn f64_to_fixed_i64(value: f64, precision: u8) -> i64 {
918 check_fixed_precision(precision).expect_display(FAILED);
919 let pow1 = 10_i64.pow(u32::from(precision));
920 let pow2 = 10_i64.pow(u32::from(FIXED_PRECISION - precision));
921 let rounded = (value * pow1 as f64).round() as i64;
922 rounded
923 .checked_mul(pow2)
924 .expect("Overflow when scaling f64 to fixed-point i64")
925}
926
927#[must_use]
937#[expect(
938 clippy::cast_precision_loss,
939 clippy::cast_possible_truncation,
940 reason = "f64 to fixed-point conversion is inherently lossy; callers validate range and finiteness"
941)]
942pub fn f64_to_fixed_i128(value: f64, precision: u8) -> i128 {
943 check_fixed_precision(precision).expect_display(FAILED);
944 let pow1 = 10_i128.pow(u32::from(precision));
945 let pow2 = 10_i128.pow(u32::from(FIXED_PRECISION - precision));
946 let rounded = (value * pow1 as f64).round() as i128;
947 rounded
948 .checked_mul(pow2)
949 .expect("Overflow when scaling f64 to fixed-point i128")
950}
951
952#[must_use]
962#[expect(
963 clippy::cast_precision_loss,
964 clippy::cast_possible_truncation,
965 clippy::cast_sign_loss,
966 reason = "f64 to fixed-point conversion is inherently lossy; callers validate range and finiteness"
967)]
968pub fn f64_to_fixed_u64(value: f64, precision: u8) -> u64 {
969 check_fixed_precision(precision).expect_display(FAILED);
970 let pow1 = 10_u64.pow(u32::from(precision));
971 let pow2 = 10_u64.pow(u32::from(FIXED_PRECISION - precision));
972 let rounded = (value * pow1 as f64).round() as u64;
973 rounded
974 .checked_mul(pow2)
975 .expect("Overflow when scaling f64 to fixed-point u64")
976}
977
978#[must_use]
988#[expect(
989 clippy::cast_precision_loss,
990 clippy::cast_possible_truncation,
991 clippy::cast_sign_loss,
992 reason = "f64 to fixed-point conversion is inherently lossy; callers validate range and finiteness"
993)]
994pub fn f64_to_fixed_u128(value: f64, precision: u8) -> u128 {
995 check_fixed_precision(precision).expect_display(FAILED);
996 let pow1 = 10_u128.pow(u32::from(precision));
997 let pow2 = 10_u128.pow(u32::from(FIXED_PRECISION - precision));
998 let rounded = (value * pow1 as f64).round() as u128;
999 rounded
1000 .checked_mul(pow2)
1001 .expect("Overflow when scaling f64 to fixed-point u128")
1002}
1003
1004#[must_use]
1006#[expect(
1007 clippy::cast_precision_loss,
1008 reason = "i64 to f64 is inherently lossy above 2^53; accepted for float interop"
1009)]
1010pub fn fixed_i64_to_f64(value: i64) -> f64 {
1011 (value as f64) / FIXED_SCALAR
1012}
1013
1014#[must_use]
1016#[expect(
1017 clippy::cast_precision_loss,
1018 reason = "i128 to f64 is inherently lossy above 2^53; accepted for float interop"
1019)]
1020pub fn fixed_i128_to_f64(value: i128) -> f64 {
1021 (value as f64) / FIXED_SCALAR
1022}
1023
1024#[must_use]
1026#[expect(
1027 clippy::cast_precision_loss,
1028 reason = "u64 to f64 is inherently lossy above 2^53; accepted for float interop"
1029)]
1030pub fn fixed_u64_to_f64(value: u64) -> f64 {
1031 (value as f64) / FIXED_SCALAR
1032}
1033
1034#[must_use]
1036#[expect(
1037 clippy::cast_precision_loss,
1038 reason = "u128 to f64 is inherently lossy above 2^53; accepted for float interop"
1039)]
1040pub fn fixed_u128_to_f64(value: u128) -> f64 {
1041 (value as f64) / FIXED_SCALAR
1042}
1043
1044#[cfg(feature = "high-precision")]
1045#[cfg(test)]
1046mod tests {
1047 use nautilus_core::approx_eq;
1048 use rstest::rstest;
1049
1050 use super::*;
1051
1052 #[rstest]
1053 fn test_correct_raw_rounds_half_away_from_zero() {
1054 let precision = FIXED_PRECISION - 1;
1055
1056 assert_eq!(correct_raw_u128(20, precision), 20);
1057 assert_eq!(correct_raw_u128(14, precision), 10);
1058 assert_eq!(correct_raw_u128(15, precision), 20);
1059 assert_eq!(correct_raw_u64(14, precision), 10);
1060 assert_eq!(correct_raw_u64(15, precision), 20);
1061 assert_eq!(correct_raw_i128(15, precision), 20);
1062 assert_eq!(correct_raw_i128(-14, precision), -10);
1063 assert_eq!(correct_raw_i128(-15, precision), -20);
1064 assert_eq!(correct_raw_i64(15, precision), 20);
1065 assert_eq!(correct_raw_i64(-14, precision), -10);
1066 assert_eq!(correct_raw_i64(-15, precision), -20);
1067 }
1068
1069 #[rstest]
1070 fn test_f64_fixed_u128_round_trip() {
1071 let raw = f64_to_fixed_u128(1.5, 1);
1072
1073 assert_eq!(raw, 15 * 10_u128.pow(u32::from(FIXED_PRECISION - 1)));
1074 assert_eq!(fixed_u128_to_f64(raw), 1.5);
1075 }
1076
1077 #[rstest]
1078 fn test_mantissa_exponent_to_fixed_i128_allows_max_scale_factor() {
1079 let exponent = i8::try_from(38 - FIXED_PRECISION).unwrap();
1080
1081 assert_eq!(
1082 mantissa_exponent_to_fixed_i128(1, exponent, 0).unwrap(),
1083 10_i128.pow(38)
1084 );
1085 assert_eq!(
1086 mantissa_exponent_to_fixed_i128(1, exponent + 1, 0)
1087 .unwrap_err()
1088 .to_string(),
1089 format!(
1090 "Exponent {} produces scale factor 10^39 which exceeds i128 range",
1091 exponent + 1
1092 )
1093 );
1094 }
1095
1096 #[rstest]
1097 fn test_raw_scales_match_requires_equal_effective_scale() {
1098 assert!(raw_scales_match(0, FIXED_PRECISION));
1099 assert!(raw_scales_match(FIXED_PRECISION + 1, FIXED_PRECISION + 1));
1100 assert!(!raw_scales_match(FIXED_PRECISION, FIXED_PRECISION + 1));
1101 }
1102
1103 #[rstest]
1104 fn test_canonical_raw_trims_native_scale_trailing_zeros() {
1105 assert_eq!(
1106 canonical_raw(0_u128, FIXED_PRECISION + 2),
1107 (0, FIXED_PRECISION)
1108 );
1109 assert_eq!(
1110 canonical_raw(1_200_u128, FIXED_PRECISION + 2),
1111 (12, FIXED_PRECISION)
1112 );
1113 assert_eq!(
1114 canonical_raw(12_000_u128, FIXED_PRECISION + 2),
1115 (120, FIXED_PRECISION)
1116 );
1117 assert_eq!(
1118 canonical_raw(1_205_u128, FIXED_PRECISION + 2),
1119 (1_205, FIXED_PRECISION + 2)
1120 );
1121 assert_eq!(
1122 canonical_raw(5_u128, FIXED_PRECISION - 1),
1123 (5, FIXED_PRECISION)
1124 );
1125 }
1126
1127 #[rstest]
1128 fn test_compare_raw_zero_operands_are_equal_across_scales() {
1129 assert_eq!(compare_raw(0_u128, u8::MAX, 0_u128, 2), Ordering::Equal);
1130 }
1131
1132 #[rstest]
1133 #[case("1.00", 100, 2)]
1134 #[case("+1.00", 100, 2)]
1135 #[case("-1.00", -100, 2)]
1136 #[case("-0.00", 0, 2)]
1137 fn test_parse_decimal_mantissa_sign(
1138 #[case] input: &str,
1139 #[case] mantissa: i128,
1140 #[case] precision: u8,
1141 ) {
1142 assert_eq!(parse_decimal_mantissa(input), Ok((mantissa, precision)));
1143 }
1144
1145 #[rstest]
1146 #[case("-+1.00")]
1147 #[case("+-1.00")]
1148 #[case("--1.00")]
1149 #[case("++1.00")]
1150 #[case("-+0.00")]
1151 fn test_parse_decimal_mantissa_rejects_multiple_signs(#[case] input: &str) {
1152 assert_eq!(
1153 parse_decimal_mantissa(input),
1154 Err(format!("Invalid decimal value '{input}'")),
1155 );
1156 assert!(input.parse::<crate::types::Price>().is_err());
1157 assert!(input.parse::<crate::types::Quantity>().is_err());
1158 assert!(
1159 format!("{input} USD")
1160 .parse::<crate::types::Money>()
1161 .is_err()
1162 );
1163 }
1164
1165 #[rstest]
1166 #[case(i128::MIN)]
1167 #[case(i128::MAX)]
1168 fn test_parse_decimal_mantissa_integer_limits(#[case] value: i128) {
1169 assert_eq!(parse_decimal_mantissa(&value.to_string()), Ok((value, 0)));
1170 }
1171
1172 #[rstest]
1173 #[case("170141183460469231731687303715884105728")]
1174 #[case("-170141183460469231731687303715884105729")]
1175 fn test_parse_decimal_mantissa_overflow(#[case] input: &str) {
1176 assert_eq!(
1177 parse_decimal_mantissa(input),
1178 Err(format!("Decimal value '{input}' exceeds i128 range")),
1179 );
1180 }
1181
1182 #[rstest]
1183 #[case(".5", 5, 1)]
1184 #[case("-.5", -5, 1)]
1185 #[case("1.", 1, 0)]
1186 #[case("0001.0200", 10200, 4)]
1187 fn test_parse_decimal_mantissa_syntax(
1188 #[case] input: &str,
1189 #[case] mantissa: i128,
1190 #[case] precision: u8,
1191 ) {
1192 assert_eq!(parse_decimal_mantissa(input), Ok((mantissa, precision)));
1193 }
1194
1195 #[rstest]
1196 #[case("")]
1197 #[case(".")]
1198 #[case("+")]
1199 #[case("-")]
1200 #[case("1.2.3")]
1201 #[case(" 1")]
1202 #[case("1 ")]
1203 #[case("1 2")]
1204 #[case("1")]
1205 fn test_parse_decimal_mantissa_invalid_syntax(#[case] input: &str) {
1206 assert_eq!(
1207 parse_decimal_mantissa(input),
1208 Err(format!("Invalid decimal value '{input}'")),
1209 );
1210 }
1211
1212 #[rstest]
1213 fn test_parse_decimal_mantissa_fraction_length_limit() {
1214 let accepted = format!("0.{}", "0".repeat(255));
1215 let rejected = format!("{accepted}0");
1216
1217 assert_eq!(parse_decimal_mantissa(&accepted), Ok((0, 255)));
1218 assert_eq!(
1219 parse_decimal_mantissa(&rejected),
1220 Err(format!(
1221 "Decimal value '{rejected}' has too many fractional digits"
1222 )),
1223 );
1224 }
1225
1226 #[rstest]
1227 fn test_decimal_string_domain_precision_limit() {
1228 use crate::types::{Price, Quantity};
1229
1230 #[cfg(feature = "defi")]
1231 let precision = crate::defi::WEI_PRECISION;
1232 #[cfg(not(feature = "defi"))]
1233 let precision = FIXED_PRECISION;
1234 let accepted = format!("0.{}1", "0".repeat(usize::from(precision - 1)));
1235 let rejected = format!("{accepted}0");
1236 let price = accepted.parse::<Price>().unwrap();
1237 let quantity = accepted.parse::<Quantity>().unwrap();
1238
1239 assert_eq!(price.raw, 1);
1240 assert_eq!(price.precision, precision);
1241 assert_eq!(quantity.raw, 1);
1242 assert_eq!(quantity.precision, precision);
1243 assert!(rejected.parse::<Price>().is_err());
1244 assert!(rejected.parse::<Quantity>().is_err());
1245 }
1246
1247 #[rstest]
1248 #[case(0, 0, "0")]
1249 #[case(125, 2, "1.25")]
1250 #[case(-1234, 2, "-12.34")]
1251 #[case(1, 16, "0.0000000000000001")]
1252 #[case(-1, 16, "-0.0000000000000001")]
1253 #[case(1_000_000_000_000_000_000, 18, "1.000000000000000000")]
1254 fn test_scaled_raw_to_decimal_matches_plain_conversion(
1255 #[case] raw: i128,
1256 #[case] precision: u8,
1257 #[case] expected: &str,
1258 ) {
1259 let plain = Decimal::from_i128_with_scale(raw, u32::from(precision));
1260 let result = scaled_raw_to_decimal(raw, precision);
1261
1262 assert_eq!(result, plain);
1263 assert_eq!(result.scale(), plain.scale());
1264 assert_eq!(result.to_string(), expected);
1265 }
1266
1267 #[rstest]
1268 #[case(80_000_000_000_000_000_000_000_000_000, 16, "8000000000000")]
1269 #[case(340_282_366_920_930_000_000_000_000_000, 16, "34028236692093")]
1270 #[case(170_141_183_460_460_000_000_000_000_000, 16, "17014118346046")]
1271 #[case(-170_141_183_460_460_000_000_000_000_000, 16, "-17014118346046")]
1272 #[case(
1275 80_000_000_000_000_005_000_000_000_000,
1276 16,
1277 "8000000000000.000500000000000"
1278 )]
1279 #[case(-80_000_000_000_000_005_000_000_000_000, 16, "-8000000000000.000500000000000")]
1280 #[case(
1281 80_000_000_000_000_000_000_000_000_001,
1282 16,
1283 "8000000000000.000000000000000"
1284 )]
1285 #[case(
1286 80_000_000_000_000_000_250_000_000_000,
1287 18,
1288 "80000000000.00000025000000000"
1289 )]
1290 #[case(-80_000_000_000_000_000_250_000_000_000, 18, "-80000000000.00000025000000000")]
1291 fn test_scaled_raw_to_decimal_beyond_mantissa_rounds_rather_than_panics(
1292 #[case] raw: i128,
1293 #[case] precision: u8,
1294 #[case] expected: &str,
1295 ) {
1296 assert_eq!(scaled_raw_to_decimal(raw, precision).to_string(), expected);
1300 }
1301
1302 #[cfg(not(feature = "defi"))]
1303 #[rstest]
1304 fn test_precision_boundaries() {
1305 assert!(check_fixed_precision(0).is_ok());
1306 assert!(check_fixed_precision(FIXED_PRECISION).is_ok());
1307 assert!(check_fixed_precision(FIXED_PRECISION + 1).is_err());
1308 }
1309
1310 #[cfg(feature = "defi")]
1311 #[rstest]
1312 fn test_precision_boundaries() {
1313 use crate::defi::WEI_PRECISION;
1314
1315 assert!(check_fixed_precision(0).is_ok());
1316 assert!(check_fixed_precision(WEI_PRECISION).is_ok());
1317 assert!(check_fixed_precision(WEI_PRECISION + 1).is_err());
1318 }
1319
1320 #[rstest]
1321 #[case(0.0)]
1322 #[case(1.0)]
1323 #[case(-1.0)]
1324 fn test_basic_roundtrip(#[case] value: f64) {
1325 for precision in 0..=FIXED_PRECISION {
1326 let fixed = f64_to_fixed_i128(value, precision);
1327 let result = fixed_i128_to_f64(fixed);
1328 assert!(approx_eq!(f64, value, result, epsilon = 0.001));
1329 }
1330 }
1331
1332 #[rstest]
1333 #[case(1_000_000.0)]
1334 #[case(-1_000_000.0)]
1335 fn test_large_value_roundtrip(#[case] value: f64) {
1336 for precision in 0..=FIXED_PRECISION {
1337 let fixed = f64_to_fixed_i128(value, precision);
1338 let result = fixed_i128_to_f64(fixed);
1339 assert!(approx_eq!(f64, value, result, epsilon = 0.000_1));
1340 }
1341 }
1342
1343 #[rstest]
1344 #[case(0, 123_456.0)]
1345 #[case(0, 123_456.7)]
1346 #[case(1, 123_456.7)]
1347 #[case(2, 123_456.78)]
1348 #[case(8, 123_456.123_456_78)]
1349 fn test_precision_specific_values_basic(#[case] precision: u8, #[case] value: f64) {
1350 let result = f64_to_fixed_i128(value, precision);
1351 let back_converted = fixed_i128_to_f64(result);
1352 let scale = 10.0_f64.powi(i32::from(precision));
1354 let expected_rounded = (value * scale).round() / scale;
1355 assert!((back_converted - expected_rounded).abs() < 1e-10);
1356 }
1357
1358 #[rstest]
1359 fn test_max_precision_values() {
1360 let test_value = 123_456.123_456_789;
1362 let result = f64_to_fixed_i128(test_value, FIXED_PRECISION);
1363 let back_converted = fixed_i128_to_f64(result);
1364 assert!((back_converted - test_value).abs() < 1e-6);
1366 }
1367
1368 #[rstest]
1369 #[case(0.0)]
1370 #[case(1.0)]
1371 #[case(1_000_000.0)]
1372 fn test_unsigned_basic_roundtrip(#[case] value: f64) {
1373 for precision in 0..=FIXED_PRECISION {
1374 let fixed = f64_to_fixed_u128(value, precision);
1375 let result = fixed_u128_to_f64(fixed);
1376 assert!(approx_eq!(f64, value, result, epsilon = 0.001));
1377 }
1378 }
1379
1380 #[rstest]
1381 #[case(0)]
1382 #[case(FIXED_PRECISION)]
1383 fn test_valid_precision(#[case] precision: u8) {
1384 let result = check_fixed_precision(precision);
1385 assert!(result.is_ok());
1386 }
1387
1388 #[cfg(not(feature = "defi"))]
1389 #[rstest]
1390 fn test_invalid_precision() {
1391 let precision = FIXED_PRECISION + 1;
1392 let result = check_fixed_precision(precision);
1393 assert!(result.is_err());
1394 }
1395
1396 #[cfg(feature = "defi")]
1397 #[rstest]
1398 fn test_invalid_precision() {
1399 use crate::defi::WEI_PRECISION;
1400 let precision = WEI_PRECISION + 1;
1401 let result = check_fixed_precision(precision);
1402 assert!(result.is_err());
1403 }
1404
1405 #[cfg(not(feature = "defi"))]
1406 #[rstest]
1407 fn test_check_fixed_precision_returns_typed_error_with_stable_display() {
1408 let error = check_fixed_precision(FIXED_PRECISION + 1).unwrap_err();
1409
1410 assert_eq!(
1411 error,
1412 CorrectnessError::PredicateViolation {
1413 message: format!(
1414 "`precision` exceeded maximum `FIXED_PRECISION` ({FIXED_PRECISION}), was {}",
1415 FIXED_PRECISION + 1
1416 ),
1417 }
1418 );
1419 assert_eq!(
1420 error.to_string(),
1421 format!(
1422 "`precision` exceeded maximum `FIXED_PRECISION` ({FIXED_PRECISION}), was {}",
1423 FIXED_PRECISION + 1
1424 )
1425 );
1426 }
1427
1428 #[cfg(feature = "defi")]
1429 #[rstest]
1430 fn test_check_fixed_precision_returns_typed_error_with_stable_display() {
1431 use crate::defi::WEI_PRECISION;
1432
1433 let error = check_fixed_precision(WEI_PRECISION + 1).unwrap_err();
1434
1435 assert_eq!(
1436 error,
1437 CorrectnessError::PredicateViolation {
1438 message: format!(
1439 "`precision` exceeded maximum `WEI_PRECISION` (18), was {}",
1440 WEI_PRECISION + 1
1441 ),
1442 }
1443 );
1444 assert_eq!(
1445 error.to_string(),
1446 format!(
1447 "`precision` exceeded maximum `WEI_PRECISION` (18), was {}",
1448 WEI_PRECISION + 1
1449 )
1450 );
1451 }
1452
1453 #[rstest]
1454 #[case(0, 0.0)]
1455 #[case(1, 1.0)]
1456 #[case(1, 1.1)]
1457 #[case(9, 0.000_000_001)]
1458 #[case(16, 0.000_000_000_000_000_1)]
1459 #[case(0, -0.0)]
1460 #[case(1, -1.0)]
1461 #[case(1, -1.1)]
1462 #[case(9, -0.000_000_001)]
1463 #[case(16, -0.000_000_000_000_000_1)]
1464 fn test_f64_to_fixed_i128_to_fixed(#[case] precision: u8, #[case] value: f64) {
1465 let fixed = f64_to_fixed_i128(value, precision);
1466 let result = fixed_i128_to_f64(fixed);
1467 assert_eq!(result, value);
1468 }
1469
1470 #[rstest]
1471 #[case(0, 0.0)]
1472 #[case(1, 1.0)]
1473 #[case(1, 1.1)]
1474 #[case(9, 0.000_000_001)]
1475 #[case(16, 0.000_000_000_000_000_1)]
1476 fn test_f64_to_fixed_u128_to_fixed(#[case] precision: u8, #[case] value: f64) {
1477 let fixed = f64_to_fixed_u128(value, precision);
1478 let result = fixed_u128_to_f64(fixed);
1479 assert_eq!(result, value);
1480 }
1481
1482 #[rstest]
1483 #[case(0, 123_456.0)]
1484 #[case(0, 123_456.7)]
1485 #[case(0, 123_456.4)]
1486 #[case(1, 123_456.0)]
1487 #[case(1, 123_456.7)]
1488 #[case(1, 123_456.4)]
1489 #[case(2, 123_456.0)]
1490 #[case(2, 123_456.7)]
1491 #[case(2, 123_456.4)]
1492 fn test_f64_to_fixed_i128_with_precision(#[case] precision: u8, #[case] value: f64) {
1493 let result = f64_to_fixed_i128(value, precision);
1494
1495 let pow1 = 10_i128.pow(u32::from(precision));
1497 let pow2 = 10_i128.pow(u32::from(FIXED_PRECISION - precision));
1498 let rounded = (value * pow1 as f64).round() as i128;
1499 let expected = rounded * pow2;
1500
1501 assert_eq!(
1502 result, expected,
1503 "Failed for precision {precision}, value {value}: got {result}, expected {expected}"
1504 );
1505 }
1506
1507 #[rstest]
1508 #[case(0, 5.555_555_555_555_555)]
1509 #[case(1, 5.555_555_555_555_555)]
1510 #[case(2, 5.555_555_555_555_555)]
1511 #[case(3, 5.555_555_555_555_555)]
1512 #[case(4, 5.555_555_555_555_555)]
1513 #[case(5, 5.555_555_555_555_555)]
1514 #[case(6, 5.555_555_555_555_555)]
1515 #[case(7, 5.555_555_555_555_555)]
1516 #[case(8, 5.555_555_555_555_555)]
1517 #[case(9, 5.555_555_555_555_555)]
1518 #[case(10, 5.555_555_555_555_555)]
1519 #[case(11, 5.555_555_555_555_555)]
1520 #[case(12, 5.555_555_555_555_555)]
1521 #[case(13, 5.555_555_555_555_555)]
1522 #[case(14, 5.555_555_555_555_555)]
1523 #[case(15, 5.555_555_555_555_555)]
1524 #[case(0, -5.555_555_555_555_555)]
1525 #[case(1, -5.555_555_555_555_555)]
1526 #[case(2, -5.555_555_555_555_555)]
1527 #[case(3, -5.555_555_555_555_555)]
1528 #[case(4, -5.555_555_555_555_555)]
1529 #[case(5, -5.555_555_555_555_555)]
1530 #[case(6, -5.555_555_555_555_555)]
1531 #[case(7, -5.555_555_555_555_555)]
1532 #[case(8, -5.555_555_555_555_555)]
1533 #[case(9, -5.555_555_555_555_555)]
1534 #[case(10, -5.555_555_555_555_555)]
1535 #[case(11, -5.555_555_555_555_555)]
1536 #[case(12, -5.555_555_555_555_555)]
1537 #[case(13, -5.555_555_555_555_555)]
1538 #[case(14, -5.555_555_555_555_555)]
1539 #[case(15, -5.555_555_555_555_555)]
1540 fn test_f64_to_fixed_i128(#[case] precision: u8, #[case] value: f64) {
1541 if precision > FIXED_PRECISION {
1543 return;
1544 }
1545
1546 let result = f64_to_fixed_i128(value, precision);
1547
1548 let pow1 = 10_i128.pow(u32::from(precision));
1550 let pow2 = 10_i128.pow(u32::from(FIXED_PRECISION - precision));
1551 let rounded = (value * pow1 as f64).round() as i128;
1552 let expected = rounded * pow2;
1553
1554 assert_eq!(
1555 result, expected,
1556 "Failed for precision {precision}, value {value}: got {result}, expected {expected}"
1557 );
1558 }
1559
1560 #[rstest]
1561 #[case(0, 5.555_555_555_555_555)]
1562 #[case(1, 5.555_555_555_555_555)]
1563 #[case(2, 5.555_555_555_555_555)]
1564 #[case(3, 5.555_555_555_555_555)]
1565 #[case(4, 5.555_555_555_555_555)]
1566 #[case(5, 5.555_555_555_555_555)]
1567 #[case(6, 5.555_555_555_555_555)]
1568 #[case(7, 5.555_555_555_555_555)]
1569 #[case(8, 5.555_555_555_555_555)]
1570 #[case(9, 5.555_555_555_555_555)]
1571 #[case(10, 5.555_555_555_555_555)]
1572 #[case(11, 5.555_555_555_555_555)]
1573 #[case(12, 5.555_555_555_555_555)]
1574 #[case(13, 5.555_555_555_555_555)]
1575 #[case(14, 5.555_555_555_555_555)]
1576 #[case(15, 5.555_555_555_555_555)]
1577 #[case(16, 5.555_555_555_555_555)]
1578 fn test_f64_to_fixed_u64(#[case] precision: u8, #[case] value: f64) {
1579 if precision > FIXED_PRECISION {
1581 return;
1582 }
1583
1584 let result = f64_to_fixed_u128(value, precision);
1585
1586 let pow1 = 10_u128.pow(u32::from(precision));
1588 let pow2 = 10_u128.pow(u32::from(FIXED_PRECISION - precision));
1589 let rounded = (value * pow1 as f64).round() as u128;
1590 let expected = rounded * pow2;
1591
1592 assert_eq!(
1593 result, expected,
1594 "Failed for precision {precision}, value {value}: got {result}, expected {expected}"
1595 );
1596 }
1597
1598 #[rstest]
1599 fn test_fixed_i128_to_f64(
1600 #[values(1, -1, 2, -2, 10, -10, 100, -100, 1_000, -1_000, -10_000, -100_000)] value: i128,
1601 ) {
1602 assert_eq!(fixed_i128_to_f64(value), value as f64 / FIXED_SCALAR);
1603 }
1604
1605 #[rstest]
1606 fn test_fixed_u128_to_f64(
1607 #[values(
1608 0,
1609 1,
1610 2,
1611 3,
1612 10,
1613 100,
1614 1_000,
1615 10_000,
1616 100_000,
1617 1_000_000,
1618 10_000_000,
1619 100_000_000,
1620 1_000_000_000,
1621 10_000_000_000,
1622 100_000_000_000,
1623 1_000_000_000_000,
1624 10_000_000_000_000,
1625 100_000_000_000_000,
1626 1_000_000_000_000_000,
1627 10_000_000_000_000_000,
1628 100_000_000_000_000_000,
1629 1_000_000_000_000_000_000,
1630 10_000_000_000_000_000_000,
1631 100_000_000_000_000_000_000
1632 )]
1633 value: u128,
1634 ) {
1635 let result = fixed_u128_to_f64(value);
1636 assert_eq!(result, (value as f64) / FIXED_SCALAR);
1637 }
1638
1639 #[rstest]
1644 #[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) {
1650 assert!(check_fixed_raw_u128(raw, precision).is_ok());
1651 }
1652
1653 #[rstest]
1654 #[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) {
1660 assert!(check_fixed_raw_u128(raw, precision).is_err());
1661 }
1662
1663 #[rstest]
1664 fn test_check_fixed_raw_u128_at_max_precision() {
1665 assert!(check_fixed_raw_u128(0, FIXED_PRECISION).is_ok());
1667 assert!(check_fixed_raw_u128(1, FIXED_PRECISION).is_ok());
1668 assert!(check_fixed_raw_u128(123_456_789, FIXED_PRECISION).is_ok());
1669 assert!(check_fixed_raw_u128(u128::MAX, FIXED_PRECISION).is_ok());
1670 }
1671
1672 #[rstest]
1673 #[case(0, 0)]
1674 #[case(0, 10_000_000_000_000_000)]
1675 #[case(0, -10_000_000_000_000_000)]
1676 #[case(8, 12_345_678_900_000_000)]
1677 #[case(8, -12_345_678_900_000_000)]
1678 fn test_check_fixed_raw_i128_valid(#[case] precision: u8, #[case] raw: i128) {
1679 assert!(check_fixed_raw_i128(raw, precision).is_ok());
1680 }
1681
1682 #[rstest]
1683 #[case(0, 1)]
1684 #[case(0, -1)]
1685 #[case(0, 9_999_999_999_999_999)]
1686 #[case(0, -9_999_999_999_999_999)]
1687 fn test_check_fixed_raw_i128_invalid(#[case] precision: u8, #[case] raw: i128) {
1688 assert!(check_fixed_raw_i128(raw, precision).is_err());
1689 }
1690
1691 #[rstest]
1692 fn test_check_fixed_raw_i128_at_max_precision() {
1693 assert!(check_fixed_raw_i128(0, FIXED_PRECISION).is_ok());
1694 assert!(check_fixed_raw_i128(1, FIXED_PRECISION).is_ok());
1695 assert!(check_fixed_raw_i128(-1, FIXED_PRECISION).is_ok());
1696 assert!(check_fixed_raw_i128(i128::MAX, FIXED_PRECISION).is_ok());
1697 assert!(check_fixed_raw_i128(i128::MIN, FIXED_PRECISION).is_ok());
1698 }
1699
1700 #[rstest]
1701 #[should_panic(expected = "Overflow when scaling f64 to fixed-point i128")]
1702 fn test_f64_to_fixed_i128_overflow_panics() {
1703 let _ = f64_to_fixed_i128(1e30, 0);
1704 }
1705
1706 #[rstest]
1707 #[should_panic(expected = "Overflow when scaling f64 to fixed-point u128")]
1708 fn test_f64_to_fixed_u128_overflow_panics() {
1709 let _ = f64_to_fixed_u128(1e30, 0);
1710 }
1711}
1712
1713#[cfg(not(feature = "high-precision"))]
1714#[cfg(test)]
1715mod tests {
1716 use nautilus_core::approx_eq;
1717 use rstest::rstest;
1718
1719 use super::*;
1720
1721 #[rstest]
1722 fn test_correct_raw_rounds_half_away_from_zero() {
1723 let precision = FIXED_PRECISION - 1;
1724
1725 assert_eq!(correct_raw_u128(20, precision), 20);
1726 assert_eq!(correct_raw_u128(14, precision), 10);
1727 assert_eq!(correct_raw_u128(15, precision), 20);
1728 assert_eq!(correct_raw_u64(14, precision), 10);
1729 assert_eq!(correct_raw_u64(15, precision), 20);
1730 assert_eq!(correct_raw_i128(15, precision), 20);
1731 assert_eq!(correct_raw_i128(-14, precision), -10);
1732 assert_eq!(correct_raw_i128(-15, precision), -20);
1733 assert_eq!(correct_raw_i64(15, precision), 20);
1734 assert_eq!(correct_raw_i64(-14, precision), -10);
1735 assert_eq!(correct_raw_i64(-15, precision), -20);
1736 }
1737
1738 #[rstest]
1739 fn test_f64_fixed_u128_round_trip() {
1740 let raw = f64_to_fixed_u128(1.5, 1);
1741
1742 assert_eq!(raw, 15 * 10_u128.pow(u32::from(FIXED_PRECISION - 1)));
1743 assert_eq!(fixed_u128_to_f64(raw), 1.5);
1744 }
1745
1746 #[rstest]
1747 fn test_mantissa_exponent_to_fixed_i128_allows_max_scale_factor() {
1748 let exponent = i8::try_from(38 - FIXED_PRECISION).unwrap();
1749
1750 assert_eq!(
1751 mantissa_exponent_to_fixed_i128(1, exponent, 0).unwrap(),
1752 10_i128.pow(38)
1753 );
1754 assert_eq!(
1755 mantissa_exponent_to_fixed_i128(1, exponent + 1, 0)
1756 .unwrap_err()
1757 .to_string(),
1758 format!(
1759 "Exponent {} produces scale factor 10^39 which exceeds i128 range",
1760 exponent + 1
1761 )
1762 );
1763 }
1764
1765 #[rstest]
1766 fn test_raw_scales_match_requires_equal_effective_scale() {
1767 assert!(raw_scales_match(0, FIXED_PRECISION));
1768 assert!(raw_scales_match(FIXED_PRECISION + 1, FIXED_PRECISION + 1));
1769 assert!(!raw_scales_match(FIXED_PRECISION, FIXED_PRECISION + 1));
1770 }
1771
1772 #[rstest]
1773 fn test_canonical_raw_trims_native_scale_trailing_zeros() {
1774 assert_eq!(
1775 canonical_raw(0_u128, FIXED_PRECISION + 2),
1776 (0, FIXED_PRECISION)
1777 );
1778 assert_eq!(
1779 canonical_raw(1_200_u128, FIXED_PRECISION + 2),
1780 (12, FIXED_PRECISION)
1781 );
1782 assert_eq!(
1783 canonical_raw(12_000_u128, FIXED_PRECISION + 2),
1784 (120, FIXED_PRECISION)
1785 );
1786 assert_eq!(
1787 canonical_raw(1_205_u128, FIXED_PRECISION + 2),
1788 (1_205, FIXED_PRECISION + 2)
1789 );
1790 assert_eq!(
1791 canonical_raw(5_u128, FIXED_PRECISION - 1),
1792 (5, FIXED_PRECISION)
1793 );
1794 }
1795
1796 #[rstest]
1797 fn test_compare_raw_zero_operands_are_equal_across_scales() {
1798 assert_eq!(compare_raw(0_u128, u8::MAX, 0_u128, 2), Ordering::Equal);
1799 }
1800
1801 #[rstest]
1802 fn test_precision_boundaries() {
1803 assert!(check_fixed_precision(0).is_ok());
1804 assert!(check_fixed_precision(FIXED_PRECISION).is_ok());
1805 assert!(check_fixed_precision(FIXED_PRECISION + 1).is_err());
1806 }
1807
1808 #[rstest]
1809 #[case(0.0)]
1810 #[case(1.0)]
1811 #[case(-1.0)]
1812 fn test_basic_roundtrip(#[case] value: f64) {
1813 for precision in 0..=FIXED_PRECISION {
1814 let fixed = f64_to_fixed_i64(value, precision);
1815 let result = fixed_i64_to_f64(fixed);
1816 assert!(approx_eq!(f64, value, result, epsilon = 0.001));
1817 }
1818 }
1819
1820 #[rstest]
1821 #[case(1_000_000.0)]
1822 #[case(-1_000_000.0)]
1823 fn test_large_value_roundtrip(#[case] value: f64) {
1824 for precision in 0..=FIXED_PRECISION {
1825 let fixed = f64_to_fixed_i64(value, precision);
1826 let result = fixed_i64_to_f64(fixed);
1827 assert!(approx_eq!(f64, value, result, epsilon = 0.000_1));
1828 }
1829 }
1830
1831 #[rstest]
1832 #[case(0, 123_456.0, 123_456_000_000_000)]
1833 #[case(0, 123_456.7, 123_457_000_000_000)]
1834 #[case(1, 123_456.7, 123_456_700_000_000)]
1835 #[case(2, 123_456.78, 123_456_780_000_000)]
1836 #[case(8, 123_456.123_456_78, 123_456_123_456_780)]
1837 #[case(9, 123_456.123_456_789, 123_456_123_456_789)]
1838 fn test_precision_specific_values(
1839 #[case] precision: u8,
1840 #[case] value: f64,
1841 #[case] expected: i64,
1842 ) {
1843 assert_eq!(f64_to_fixed_i64(value, precision), expected);
1844 }
1845
1846 #[rstest]
1847 #[case(0.0)]
1848 #[case(1.0)]
1849 #[case(1_000_000.0)]
1850 fn test_unsigned_basic_roundtrip(#[case] value: f64) {
1851 for precision in 0..=FIXED_PRECISION {
1852 let fixed = f64_to_fixed_u64(value, precision);
1853 let result = fixed_u64_to_f64(fixed);
1854 assert!(approx_eq!(f64, value, result, epsilon = 0.001));
1855 }
1856 }
1857
1858 #[rstest]
1859 #[case(0, 1.4, 1.0)]
1860 #[case(0, 1.5, 2.0)]
1861 #[case(0, 1.6, 2.0)]
1862 #[case(1, 1.44, 1.4)]
1863 #[case(1, 1.45, 1.5)]
1864 #[case(1, 1.46, 1.5)]
1865 #[case(2, 1.444, 1.44)]
1866 #[case(2, 1.445, 1.45)]
1867 #[case(2, 1.446, 1.45)]
1868 fn test_rounding(#[case] precision: u8, #[case] input: f64, #[case] expected: f64) {
1869 let fixed = f64_to_fixed_i128(input, precision);
1870 assert!(approx_eq!(
1871 f64,
1872 fixed_i128_to_f64(fixed),
1873 expected,
1874 epsilon = 0.000_000_001
1875 ));
1876 }
1877
1878 #[rstest]
1879 fn test_special_values() {
1880 assert_eq!(f64_to_fixed_i128(0.0, FIXED_PRECISION), 0);
1882 assert_eq!(f64_to_fixed_i128(-0.0, FIXED_PRECISION), 0);
1883
1884 let smallest_positive = 1.0 / FIXED_SCALAR;
1886 let fixed_smallest = f64_to_fixed_i128(smallest_positive, FIXED_PRECISION);
1887 assert_eq!(fixed_smallest, 1);
1888
1889 let large_int = 1_000_000_000.0;
1891 let fixed_large = f64_to_fixed_i128(large_int, 0);
1892 assert_eq!(fixed_i128_to_f64(fixed_large), large_int);
1893 }
1894
1895 #[rstest]
1896 #[case(0)]
1897 #[case(FIXED_PRECISION)]
1898 fn test_valid_precision(#[case] precision: u8) {
1899 let result = check_fixed_precision(precision);
1900 assert!(result.is_ok());
1901 }
1902
1903 #[rstest]
1904 fn test_invalid_precision() {
1905 let precision = FIXED_PRECISION + 1;
1906 let result = check_fixed_precision(precision);
1907 assert!(result.is_err());
1908 }
1909
1910 #[rstest]
1911 #[case(0, 0.0)]
1912 #[case(1, 1.0)]
1913 #[case(1, 1.1)]
1914 #[case(9, 0.000_000_001)]
1915 #[case(0, -0.0)]
1916 #[case(1, -1.0)]
1917 #[case(1, -1.1)]
1918 #[case(9, -0.000_000_001)]
1919 fn test_f64_to_fixed_i64_to_fixed(#[case] precision: u8, #[case] value: f64) {
1920 let fixed = f64_to_fixed_i64(value, precision);
1921 let result = fixed_i64_to_f64(fixed);
1922 assert_eq!(result, value);
1923 }
1924
1925 #[rstest]
1926 #[case(0, 0.0)]
1927 #[case(1, 1.0)]
1928 #[case(1, 1.1)]
1929 #[case(9, 0.000_000_001)]
1930 fn test_f64_to_fixed_u64_to_fixed(#[case] precision: u8, #[case] value: f64) {
1931 let fixed = f64_to_fixed_u64(value, precision);
1932 let result = fixed_u64_to_f64(fixed);
1933 assert_eq!(result, value);
1934 }
1935
1936 #[rstest]
1937 #[case(0, 123_456.0, 123_456_000_000_000)]
1938 #[case(0, 123_456.7, 123_457_000_000_000)]
1939 #[case(0, 123_456.4, 123_456_000_000_000)]
1940 #[case(1, 123_456.0, 123_456_000_000_000)]
1941 #[case(1, 123_456.7, 123_456_700_000_000)]
1942 #[case(1, 123_456.4, 123_456_400_000_000)]
1943 #[case(2, 123_456.0, 123_456_000_000_000)]
1944 #[case(2, 123_456.7, 123_456_700_000_000)]
1945 #[case(2, 123_456.4, 123_456_400_000_000)]
1946 fn test_f64_to_fixed_i64_with_precision(
1947 #[case] precision: u8,
1948 #[case] value: f64,
1949 #[case] expected: i64,
1950 ) {
1951 assert_eq!(f64_to_fixed_i64(value, precision), expected);
1952 }
1953
1954 #[rstest]
1955 #[case(0, 5.5, 6_000_000_000)]
1956 #[case(1, 5.55, 5_600_000_000)]
1957 #[case(2, 5.555, 5_560_000_000)]
1958 #[case(3, 5.5555, 5_556_000_000)]
1959 #[case(4, 5.55555, 5_555_600_000)]
1960 #[case(5, 5.555_555, 5_555_560_000)]
1961 #[case(6, 5.555_555_5, 5_555_556_000)]
1962 #[case(7, 5.555_555_55, 5_555_555_600)]
1963 #[case(8, 5.555_555_555, 5_555_555_560)]
1964 #[case(9, 5.555_555_555_5, 5_555_555_556)]
1965 #[case(0, -5.5, -6_000_000_000)]
1966 #[case(1, -5.55, -5_600_000_000)]
1967 #[case(2, -5.555, -5_560_000_000)]
1968 #[case(3, -5.5555, -5_556_000_000)]
1969 #[case(4, -5.55555, -5_555_600_000)]
1970 #[case(5, -5.555_555, -5_555_560_000)]
1971 #[case(6, -5.555_555_5, -5_555_556_000)]
1972 #[case(7, -5.555_555_55, -5_555_555_600)]
1973 #[case(8, -5.555_555_555, -5_555_555_560)]
1974 #[case(9, -5.555_555_555_5, -5_555_555_556)]
1975 fn test_f64_to_fixed_i64(#[case] precision: u8, #[case] value: f64, #[case] expected: i64) {
1976 assert_eq!(f64_to_fixed_i64(value, precision), expected);
1977 }
1978
1979 #[rstest]
1980 #[case(0, 5.5, 6_000_000_000)]
1981 #[case(1, 5.55, 5_600_000_000)]
1982 #[case(2, 5.555, 5_560_000_000)]
1983 #[case(3, 5.5555, 5_556_000_000)]
1984 #[case(4, 5.55555, 5_555_600_000)]
1985 #[case(5, 5.555_555, 5_555_560_000)]
1986 #[case(6, 5.555_555_5, 5_555_556_000)]
1987 #[case(7, 5.555_555_55, 5_555_555_600)]
1988 #[case(8, 5.555_555_555, 5_555_555_560)]
1989 #[case(9, 5.555_555_555_5, 5_555_555_556)]
1990 fn test_f64_to_fixed_u64(#[case] precision: u8, #[case] value: f64, #[case] expected: u64) {
1991 assert_eq!(f64_to_fixed_u64(value, precision), expected);
1992 }
1993
1994 #[rstest]
1995 fn test_fixed_i64_to_f64(
1996 #[values(1, -1, 2, -2, 10, -10, 100, -100, 1_000, -1_000)] value: i64,
1997 ) {
1998 assert_eq!(fixed_i64_to_f64(value), value as f64 / FIXED_SCALAR);
1999 }
2000
2001 #[rstest]
2002 fn test_fixed_u64_to_f64(
2003 #[values(
2004 0,
2005 1,
2006 2,
2007 3,
2008 10,
2009 100,
2010 1_000,
2011 10_000,
2012 100_000,
2013 1_000_000,
2014 10_000_000,
2015 100_000_000,
2016 1_000_000_000,
2017 10_000_000_000,
2018 100_000_000_000,
2019 1_000_000_000_000,
2020 10_000_000_000_000,
2021 100_000_000_000_000,
2022 1_000_000_000_000_000
2023 )]
2024 value: u64,
2025 ) {
2026 let result = fixed_u64_to_f64(value);
2027 assert_eq!(result, (value as f64) / FIXED_SCALAR);
2028 }
2029
2030 #[rstest]
2031 #[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) {
2037 assert!(check_fixed_raw_u64(raw, precision).is_ok());
2038 }
2039
2040 #[rstest]
2041 #[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) {
2048 assert!(check_fixed_raw_u64(raw, precision).is_err());
2049 }
2050
2051 #[rstest]
2052 fn test_check_fixed_raw_u64_at_max_precision() {
2053 assert!(check_fixed_raw_u64(0, FIXED_PRECISION).is_ok());
2055 assert!(check_fixed_raw_u64(1, FIXED_PRECISION).is_ok());
2056 assert!(check_fixed_raw_u64(123_456_789, FIXED_PRECISION).is_ok());
2057 assert!(check_fixed_raw_u64(u64::MAX, FIXED_PRECISION).is_ok());
2058 }
2059
2060 #[rstest]
2061 #[case(0, 0)]
2062 #[case(0, 1_000_000_000)]
2063 #[case(0, -1_000_000_000)]
2064 #[case(2, 123_450_000_000)]
2065 #[case(2, -123_450_000_000)]
2066 fn test_check_fixed_raw_i64_valid(#[case] precision: u8, #[case] raw: i64) {
2067 assert!(check_fixed_raw_i64(raw, precision).is_ok());
2068 }
2069
2070 #[rstest]
2071 #[case(0, 1)]
2072 #[case(0, -1)]
2073 #[case(0, 999_999_999)]
2074 #[case(0, -999_999_999)]
2075 fn test_check_fixed_raw_i64_invalid(#[case] precision: u8, #[case] raw: i64) {
2076 assert!(check_fixed_raw_i64(raw, precision).is_err());
2077 }
2078
2079 #[rstest]
2080 fn test_check_fixed_raw_i64_at_max_precision() {
2081 assert!(check_fixed_raw_i64(0, FIXED_PRECISION).is_ok());
2082 assert!(check_fixed_raw_i64(1, FIXED_PRECISION).is_ok());
2083 assert!(check_fixed_raw_i64(-1, FIXED_PRECISION).is_ok());
2084 assert!(check_fixed_raw_i64(i64::MAX, FIXED_PRECISION).is_ok());
2085 assert!(check_fixed_raw_i64(i64::MIN, FIXED_PRECISION).is_ok());
2086 }
2087
2088 #[rstest]
2089 #[should_panic(expected = "Overflow when scaling f64 to fixed-point i64")]
2090 fn test_f64_to_fixed_i64_overflow_panics() {
2091 let _ = f64_to_fixed_i64(2e18, 0);
2092 }
2093
2094 #[rstest]
2095 #[should_panic(expected = "Overflow when scaling f64 to fixed-point u64")]
2096 fn test_f64_to_fixed_u64_overflow_panics() {
2097 let _ = f64_to_fixed_u64(2e19, 0);
2098 }
2099}
2100
2101#[cfg(test)]
2102mod bankers_round_tests {
2103 use std::str::FromStr;
2104
2105 use rstest::rstest;
2106 use rust_decimal::{Decimal, RoundingStrategy};
2107
2108 use super::*;
2109
2110 #[rstest]
2111 #[case(0, 0, 0)]
2113 #[case(1, 0, 1)]
2114 #[case(5, 0, 5)]
2115 #[case(99, 0, 99)]
2116 #[case(-7, 0, -7)]
2117 #[case(12345, 39, 0)]
2119 #[case(i128::from(i64::MAX), 100, 0)]
2120 #[case(-99999, 50, 0)]
2121 #[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)]
2178 #[case(0, 2, 0)]
2179 #[case(0, 5, 0)]
2180 #[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)]
2188 #[case(1_000_000_000_000_000_005, 1, 100_000_000_000_000_000)]
2189 fn test_bankers_round(#[case] mantissa: i128, #[case] excess: u32, #[case] expected: i128) {
2190 assert_eq!(
2191 bankers_round(mantissa, excess),
2192 expected,
2193 "bankers_round({mantissa}, {excess}) expected {expected}"
2194 );
2195 }
2196
2197 #[rstest]
2199 #[case(15, 1)]
2200 #[case(25, 1)]
2201 #[case(35, 1)]
2202 #[case(150, 2)]
2203 #[case(250, 2)]
2204 #[case(1500, 3)]
2205 #[case(2500, 3)]
2206 #[case(123_456_789, 3)]
2207 #[case(14, 1)]
2208 #[case(16, 1)]
2209 fn test_bankers_round_negative_symmetry(#[case] mantissa: i128, #[case] excess: u32) {
2210 assert_eq!(
2211 bankers_round(-mantissa, excess),
2212 -bankers_round(mantissa, excess),
2213 "Negative symmetry failed for mantissa={mantissa}, excess={excess}"
2214 );
2215 }
2216
2217 #[rstest]
2219 #[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")]
2227 #[case("-3.5", 0, "-4")]
2228 #[case("123.456", 2, "123.46")]
2229 #[case("123.455", 2, "123.46")] #[case("123.445", 2, "123.44")] fn test_bankers_round_matches_decimal(
2232 #[case] input: &str,
2233 #[case] target_precision: u8,
2234 #[case] expected: &str,
2235 ) {
2236 let dec = Decimal::from_str(input).unwrap();
2237 let expected_dec = Decimal::from_str(expected).unwrap();
2238
2239 let decimal_rounded = dec.round_dp_with_strategy(
2240 u32::from(target_precision),
2241 RoundingStrategy::MidpointNearestEven,
2242 );
2243 assert_eq!(
2244 decimal_rounded, expected_dec,
2245 "Decimal rounding sanity check failed for {input}"
2246 );
2247
2248 let mantissa = dec.mantissa();
2249 let scale = dec.scale() as u8;
2250 let excess = u32::from(scale.saturating_sub(target_precision));
2251 if excess > 0 {
2252 let rounded = bankers_round(mantissa, excess);
2253
2254 let expected_mantissa = expected_dec.mantissa();
2256 let expected_scale = expected_dec.scale() as u8;
2257 let scale_diff = u32::from(target_precision.saturating_sub(expected_scale));
2258 let normalized_expected = expected_mantissa * 10i128.pow(scale_diff);
2259
2260 assert_eq!(
2261 rounded, normalized_expected,
2262 "bankers_round disagrees with Decimal for {input} at precision {target_precision}"
2263 );
2264 }
2265 }
2266}
2267
2268#[cfg(test)]
2269mod correct_raw_tests {
2270 use rstest::rstest;
2271
2272 use super::*;
2273
2274 #[rstest]
2278 #[case(0, 0)]
2279 #[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) {
2285 assert_eq!(correct_raw_u64(raw, FIXED_PRECISION - 1), expected);
2286 }
2287
2288 #[rstest]
2289 #[case(0, 0)]
2290 #[case(14, 10)]
2291 #[case(15, 20)]
2292 #[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) {
2298 assert_eq!(correct_raw_i64(raw, FIXED_PRECISION - 1), expected);
2299 }
2300
2301 #[rstest]
2302 #[case(0, 0)]
2303 #[case(14, 10)]
2304 #[case(15, 20)]
2305 #[case(u128::MAX, u128::MAX - 5)] fn test_correct_raw_u128(#[case] raw: u128, #[case] expected: u128) {
2307 assert_eq!(correct_raw_u128(raw, FIXED_PRECISION - 1), expected);
2308 }
2309
2310 #[rstest]
2311 #[case(0, 0)]
2312 #[case(14, 10)]
2313 #[case(15, 20)]
2314 #[case(-15, -20)]
2315 #[case(i128::MAX, i128::MAX - 7)] #[case(i128::MIN, i128::MIN + 8)] fn test_correct_raw_i128(#[case] raw: i128, #[case] expected: i128) {
2318 assert_eq!(correct_raw_i128(raw, FIXED_PRECISION - 1), expected);
2319 }
2320
2321 #[rstest]
2322 fn test_correct_raw_identity_at_max_precision() {
2323 assert_eq!(correct_raw_u64(12_345, FIXED_PRECISION), 12_345);
2324 assert_eq!(correct_raw_i64(-12_345, FIXED_PRECISION), -12_345);
2325 assert_eq!(correct_raw_u128(12_345, FIXED_PRECISION), 12_345);
2326 assert_eq!(correct_raw_i128(-12_345, FIXED_PRECISION), -12_345);
2327 }
2328}
2329
2330#[cfg(test)]
2331mod checked_mul_div_tests {
2332 #[cfg(feature = "defi")]
2333 use alloy_primitives::U256;
2334 use proptest::{prelude::*, test_runner::Config as ProptestConfig};
2335 use rstest::rstest;
2336
2337 use super::{FIXED_SCALAR_RAW, checked_mul_div_fixed};
2338 use crate::types::quantity::QuantityRaw;
2339
2340 #[rstest]
2341 fn test_checked_mul_div_fixed_exact_boundaries() {
2342 let scalar = FIXED_SCALAR_RAW;
2343
2344 assert_eq!(checked_mul_div_fixed(0, QuantityRaw::MAX), Some(0));
2345 assert_eq!(checked_mul_div_fixed(QuantityRaw::MAX, 0), Some(0));
2346 assert_eq!(checked_mul_div_fixed(scalar, scalar), Some(scalar));
2347 assert_eq!(
2348 checked_mul_div_fixed(scalar - 1, scalar - 1),
2349 Some(scalar - 2)
2350 );
2351 assert_eq!(
2352 checked_mul_div_fixed(scalar + 1, scalar + 1),
2353 Some(scalar + 2)
2354 );
2355 assert_eq!(
2356 checked_mul_div_fixed(QuantityRaw::MAX, scalar),
2357 Some(QuantityRaw::MAX)
2358 );
2359 assert_eq!(
2360 checked_mul_div_fixed(scalar, QuantityRaw::MAX),
2361 Some(QuantityRaw::MAX)
2362 );
2363 assert_eq!(checked_mul_div_fixed(QuantityRaw::MAX, scalar + 1), None);
2364 assert_eq!(checked_mul_div_fixed(scalar + 1, QuantityRaw::MAX), None);
2365 }
2366
2367 #[cfg(not(feature = "high-precision"))]
2368 proptest! {
2369 #![proptest_config(ProptestConfig::with_cases(4_096))]
2370
2371 #[rstest]
2372 fn prop_checked_mul_div_fixed_matches_u128_full_range(
2373 lhs in any::<QuantityRaw>(),
2374 rhs in any::<QuantityRaw>(),
2375 ) {
2376 let expected = u128::from(lhs)
2377 .checked_mul(u128::from(rhs))
2378 .map(|product| product / u128::from(FIXED_SCALAR_RAW))
2379 .and_then(|result| QuantityRaw::try_from(result).ok());
2380
2381 prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), expected);
2382 }
2383
2384 #[rstest]
2385 fn prop_checked_mul_div_fixed_matches_u128_final_fit(
2386 (lhs, rhs) in standard_final_fit_strategy(),
2387 ) {
2388 let expected =
2389 u128::from(lhs) * u128::from(rhs) / u128::from(FIXED_SCALAR_RAW);
2390 let expected = QuantityRaw::try_from(expected).expect("strategy result fits u64");
2391
2392 prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), Some(expected));
2393 }
2394
2395 #[rstest]
2396 fn prop_checked_mul_div_fixed_avoids_u64_phantom_overflow(
2397 (lhs, rhs, expected) in standard_phantom_overflow_strategy(),
2398 ) {
2399 prop_assert!(lhs.checked_mul(rhs).is_none());
2400 prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), Some(expected));
2401 }
2402 }
2403
2404 #[cfg(feature = "high-precision")]
2405 proptest! {
2406 #![proptest_config(ProptestConfig::with_cases(4_096))]
2407
2408 #[rstest]
2409 fn prop_checked_mul_div_fixed_matches_u128_ordinary(
2410 (lhs, rhs) in high_precision_ordinary_strategy(),
2411 ) {
2412 let expected = lhs
2413 .checked_mul(rhs)
2414 .expect("ordinary strategy product fits u128")
2415 / FIXED_SCALAR_RAW;
2416
2417 prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), Some(expected));
2418 }
2419
2420 #[rstest]
2421 fn prop_checked_mul_div_fixed_avoids_u128_phantom_overflow(
2422 (lhs, rhs, expected) in high_precision_phantom_overflow_strategy(),
2423 ) {
2424 prop_assert!(lhs.checked_mul(rhs).is_none());
2425 prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), Some(expected));
2426 }
2427
2428 #[rstest]
2429 fn prop_checked_mul_div_fixed_handles_remainders_after_u128_overflow(
2430 rhs in high_precision_remainder_overflow_strategy(),
2431 ) {
2432 let lhs = 2 * FIXED_SCALAR_RAW - 1;
2433 let expected = 2 * rhs - rhs.div_ceil(FIXED_SCALAR_RAW);
2434
2435 prop_assert!(lhs.checked_mul(rhs).is_none());
2436 prop_assert_ne!(lhs % FIXED_SCALAR_RAW, 0);
2437 prop_assert_ne!(rhs % FIXED_SCALAR_RAW, 0);
2438 prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), Some(expected));
2439 prop_assert_eq!(checked_mul_div_fixed(rhs, lhs), Some(expected));
2440 }
2441
2442 #[rstest]
2443 fn prop_checked_mul_div_fixed_is_commutative(
2444 lhs in any::<QuantityRaw>(),
2445 rhs in any::<QuantityRaw>(),
2446 ) {
2447 prop_assert_eq!(
2448 checked_mul_div_fixed(lhs, rhs),
2449 checked_mul_div_fixed(rhs, lhs)
2450 );
2451 }
2452 }
2453
2454 #[cfg(feature = "defi")]
2455 proptest! {
2456 #![proptest_config(ProptestConfig::with_cases(4_096))]
2457
2458 #[rstest]
2459 fn prop_checked_mul_div_raw_matches_u256_full_range(
2460 lhs in any::<QuantityRaw>(),
2461 rhs in any::<QuantityRaw>(),
2462 precision in 16_u32..=18,
2463 ) {
2464 let scalar = 10_u128.pow(precision);
2465 let expected = U256::from(lhs) * U256::from(rhs) / U256::from(scalar);
2466 let expected = QuantityRaw::try_from(expected).ok();
2467 prop_assert_eq!(super::checked_mul_div_raw(lhs, rhs, scalar), expected);
2468 }
2469
2470 #[rstest]
2471 fn prop_checked_mul_div_fixed_matches_u256_full_range(
2472 lhs in any::<QuantityRaw>(),
2473 rhs in any::<QuantityRaw>(),
2474 ) {
2475 let expected = U256::from(lhs)
2476 .checked_mul(U256::from(rhs))
2477 .expect("u128 product fits U256")
2478 / U256::from(FIXED_SCALAR_RAW);
2479 let expected = QuantityRaw::try_from(expected).ok();
2480
2481 prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), expected);
2482 }
2483 }
2484
2485 #[cfg(not(feature = "high-precision"))]
2486 fn standard_final_fit_strategy() -> impl Strategy<Value = (QuantityRaw, QuantityRaw)> {
2487 let scalar = FIXED_SCALAR_RAW;
2488
2489 (0_u64..=1_000, 0_u64..=1_000, 0_u64..scalar, 0_u64..scalar).prop_map(
2490 move |(lhs_whole, rhs_whole, lhs_remainder, rhs_remainder)| {
2491 (
2492 lhs_whole * scalar + lhs_remainder,
2493 rhs_whole * scalar + rhs_remainder,
2494 )
2495 },
2496 )
2497 }
2498
2499 #[cfg(not(feature = "high-precision"))]
2500 fn standard_phantom_overflow_strategy()
2501 -> impl Strategy<Value = (QuantityRaw, QuantityRaw, QuantityRaw)> {
2502 let scalar = FIXED_SCALAR_RAW;
2503
2504 (8_000_000_000_u64..=9_000_000_000, 0_u64..scalar).prop_map(
2505 move |(lhs_whole, rhs_remainder)| {
2506 let lhs = lhs_whole * scalar;
2507 let rhs = scalar + rhs_remainder;
2508 (lhs, rhs, lhs_whole * rhs)
2509 },
2510 )
2511 }
2512
2513 #[cfg(feature = "high-precision")]
2514 fn high_precision_ordinary_strategy() -> impl Strategy<Value = (QuantityRaw, QuantityRaw)> {
2515 let scalar = FIXED_SCALAR_RAW;
2516
2517 (
2518 0_u128..=1_000,
2519 0_u128..=1_000,
2520 0_u128..scalar,
2521 0_u128..scalar,
2522 )
2523 .prop_map(
2524 move |(lhs_whole, rhs_whole, lhs_remainder, rhs_remainder)| {
2525 (
2526 lhs_whole * scalar + lhs_remainder,
2527 rhs_whole * scalar + rhs_remainder,
2528 )
2529 },
2530 )
2531 }
2532
2533 #[cfg(feature = "high-precision")]
2534 fn high_precision_phantom_overflow_strategy()
2535 -> impl Strategy<Value = (QuantityRaw, QuantityRaw, QuantityRaw)> {
2536 let scalar = FIXED_SCALAR_RAW;
2537
2538 (10_000_u128..=1_000_000, 1_000_u128..=10_000, 0_u128..scalar).prop_map(
2539 move |(lhs_whole, rhs_whole, rhs_remainder)| {
2540 let lhs = lhs_whole * scalar;
2541 let rhs = rhs_whole * scalar + rhs_remainder;
2542 (lhs, rhs, lhs_whole * rhs)
2543 },
2544 )
2545 }
2546
2547 #[cfg(feature = "high-precision")]
2548 fn high_precision_remainder_overflow_strategy() -> impl Strategy<Value = QuantityRaw> {
2549 let lhs = 2 * FIXED_SCALAR_RAW - 1;
2550 let min = QuantityRaw::MAX / lhs + 1;
2551
2552 (min..=QuantityRaw::MAX / 2).prop_filter("rhs remainder is nonzero", |rhs| {
2553 rhs % FIXED_SCALAR_RAW != 0
2554 })
2555 }
2556}