use crate::Decimal;
use crate::decimal::{DEFAULT_DIVISION_SCALE, MAX_SUPPORTED_SCALE, ParseDecimalErrorReason};
use candid::{decode_one, encode_one};
use proptest::prelude::*;
use std::str::FromStr;
fn assert_decimal_parse_reason(input: &str, reason: ParseDecimalErrorReason) {
let err = Decimal::from_str(input).expect_err("decimal input should reject");
assert_eq!(err.reason(), reason);
assert_eq!(err.to_string(), "decimal parse error");
}
#[test]
fn decimal_candid_roundtrip() {
let cases = [
"0",
"1",
"-1",
"42.5",
"1234567890.123456789",
"0.00000001",
"1000000000000000000.000000000000000001",
];
for s in cases {
let d1 = Decimal::from_str(s).expect("parse decimal");
let bytes = encode_one(d1).expect("candid encode");
let d2: Decimal = decode_one(&bytes).expect("candid decode to Decimal");
assert_eq!(d2, d1, "roundtrip mismatch for {s}");
let wire_str: String = decode_one(&bytes).expect("candid decode to String");
assert_eq!(wire_str, d1.to_string(), "wire text mismatch for {s}");
}
}
#[test]
fn decimal_division_is_fixed_scale_and_rounded() {
let one = Decimal::new(1, 0);
let third = one / Decimal::new(3, 0);
let sixth = one / Decimal::new(6, 0);
let neg_sixth = Decimal::new(-1, 0) / Decimal::new(6, 0);
assert_eq!(third.to_string(), "0.333333333333333333");
assert_eq!(sixth.to_string(), "0.166666666666666667");
assert_eq!(neg_sixth.to_string(), "-0.166666666666666667");
}
#[test]
fn decimal_div_by_zero_returns_zero() {
let value = Decimal::new(123, 2);
assert_eq!(value / Decimal::ZERO, Decimal::ZERO);
}
#[test]
fn decimal_operator_completion_preserves_saturating_semantics() {
let mut remainder = Decimal::new(17, 0);
remainder %= Decimal::new(5, 0);
assert_eq!(remainder, Decimal::new(2, 0));
let product: Decimal = [Decimal::new(2, 0), Decimal::new(3, 0)]
.into_iter()
.product();
assert_eq!(product, Decimal::new(6, 0));
assert_eq!(
std::iter::empty::<Decimal>().product::<Decimal>(),
Decimal::new(1, 0)
);
assert_eq!(-Decimal::new(25, 1), Decimal::new(-25, 1));
let minimum = Decimal::from_i128_with_scale(i128::MIN, 0);
assert_eq!((-minimum).mantissa(), i128::MAX);
}
#[test]
fn decimal_parse_rejects_mantissa_overflow_without_float_fallback() {
let too_large = "340282366920938463463374607431768211456";
assert_decimal_parse_reason(too_large, ParseDecimalErrorReason::MantissaOverflow);
}
#[test]
fn decimal_parse_rejects_exponent_notation() {
assert_decimal_parse_reason("1e3", ParseDecimalErrorReason::ExponentNotationUnsupported);
assert_decimal_parse_reason("1E3", ParseDecimalErrorReason::ExponentNotationUnsupported);
}
#[test]
fn decimal_parse_rejects_invalid_significand_and_digits_with_reason_codes() {
assert_decimal_parse_reason("", ParseDecimalErrorReason::Empty);
assert_decimal_parse_reason(".", ParseDecimalErrorReason::InvalidSignificand);
assert_decimal_parse_reason("1.2.3", ParseDecimalErrorReason::InvalidSignificand);
assert_decimal_parse_reason("abc", ParseDecimalErrorReason::InvalidDigits);
assert_decimal_parse_reason("1.x", ParseDecimalErrorReason::InvalidDigits);
}
#[test]
fn decimal_try_new_rejects_scale_over_max() {
assert!(Decimal::try_new(1, MAX_SUPPORTED_SCALE).is_some());
assert!(Decimal::try_new(1, MAX_SUPPORTED_SCALE + 1).is_none());
}
#[test]
fn decimal_new_panics_on_scale_over_max() {
assert!(
std::panic::catch_unwind(|| {
let _ = Decimal::new(1, MAX_SUPPORTED_SCALE + 1);
})
.is_err(),
"scale over max should panic",
);
}
#[test]
fn decimal_new_unchecked_is_internal_invariant_bypass() {
let d = Decimal::new_unchecked(1, MAX_SUPPORTED_SCALE + 1);
assert_eq!(d.scale(), MAX_SUPPORTED_SCALE + 1);
}
#[test]
fn decimal_try_from_i128_with_scale_rejects_unrepresentable_scale() {
assert_eq!(
Decimal::try_from_i128_with_scale(1, MAX_SUPPORTED_SCALE + 1),
None,
);
}
#[test]
fn decimal_from_i128_with_scale_panics_on_unrepresentable_scale() {
assert!(
std::panic::catch_unwind(|| {
let _ = Decimal::from_i128_with_scale(1, MAX_SUPPORTED_SCALE + 1);
})
.is_err(),
"unrepresentable scale should panic",
);
}
#[test]
fn decimal_add_overflow_saturates() {
let max = Decimal::from_i128_with_scale(i128::MAX, 0);
let min = Decimal::from_i128_with_scale(i128::MIN, 0);
assert_eq!((max + Decimal::new(1, 0)).mantissa(), i128::MAX);
assert_eq!((min + Decimal::new(-1, 0)).mantissa(), i128::MIN);
}
#[test]
fn decimal_mul_overflow_saturates() {
let positive = Decimal::from_i128_with_scale(i128::MAX / 2 + 1, 0);
let negative = Decimal::from_i128_with_scale(i128::MIN, 0);
assert_eq!((positive * Decimal::new(2, 0)).mantissa(), i128::MAX);
assert_eq!((negative * Decimal::new(2, 0)).mantissa(), i128::MIN);
}
#[test]
fn decimal_division_sign_scale_matrix() {
let sign_cases = [
(1i128, 1i128, false),
(1i128, -1i128, true),
(-1i128, 1i128, true),
(-1i128, -1i128, false),
];
let scales = [0u32, 1u32, 8u32, 18u32];
for (lhs_sign, rhs_sign, expected_negative) in sign_cases {
for lhs_scale in scales {
for rhs_scale in scales {
let lhs = Decimal::from_i128_with_scale(lhs_sign * 25, lhs_scale);
let rhs = Decimal::from_i128_with_scale(rhs_sign * 5, rhs_scale);
let out = lhs / rhs;
assert!(
out.scale() <= DEFAULT_DIVISION_SCALE,
"lhs={lhs:?}, rhs={rhs:?}, out={out:?}"
);
assert!(
!out.is_zero(),
"division matrix should not produce zero for non-zero operands"
);
assert_eq!(
out.is_sign_negative(),
expected_negative,
"lhs={lhs:?}, rhs={rhs:?}, out={out:?}"
);
}
}
}
}
proptest! {
#[test]
fn decimal_add_saturation_boundary_property(
lhs_m in any::<i128>(),
rhs_m in any::<i128>(),
lhs_scale in 0u32..=18,
rhs_scale in 0u32..=18,
) {
let lhs = Decimal::from_i128_with_scale(lhs_m, lhs_scale);
let rhs = Decimal::from_i128_with_scale(rhs_m, rhs_scale);
let out = lhs + rhs;
let target_scale = lhs_scale.max(rhs_scale);
prop_assert_eq!(
out.scale(),
target_scale,
"addition result scale must stay on max operand scale"
);
if let Some(exact) = lhs.checked_add(rhs) {
prop_assert_eq!(out, exact);
} else {
prop_assert!(
out.mantissa() == i128::MAX
|| out.mantissa() == i128::MIN
|| out.mantissa() == 0,
"overflow path must saturate deterministically"
);
}
}
#[test]
fn decimal_division_non_zero_sign_property(
lhs_m in any::<i128>().prop_filter("lhs non-zero", |v| *v != 0),
rhs_m in any::<i128>().prop_filter("rhs non-zero", |v| *v != 0),
lhs_scale in 0u32..=18,
rhs_scale in 0u32..=18,
) {
let lhs = Decimal::from_i128_with_scale(lhs_m, lhs_scale);
let rhs = Decimal::from_i128_with_scale(rhs_m, rhs_scale);
let out = lhs / rhs;
prop_assert!(out.scale() <= DEFAULT_DIVISION_SCALE);
if !out.is_zero() {
prop_assert_eq!(
out.is_sign_negative(),
lhs.is_sign_negative() ^ rhs.is_sign_negative(),
"non-zero quotient sign must follow operand signs"
);
}
}
}