tier 0.1.17

Rust configuration library for layered TOML, env, and CLI settings
Documentation
use std::cmp::Ordering;

use serde_json::Number;

const I128_MIN_AS_F64: f64 = -170_141_183_460_469_231_731_687_303_715_884_105_728.0;
const I128_MAX_EXCLUSIVE_AS_F64: f64 = 170_141_183_460_469_231_731_687_303_715_884_105_728.0;

#[derive(Debug, Clone, Copy)]
struct DecimalRational {
    numerator: i128,
    denominator: i128,
}

pub(crate) fn compare_json_numbers(left: &Number, right: &Number) -> Option<Ordering> {
    if let (Some(left), Some(right)) = (json_number_as_i128(left), json_number_as_i128(right)) {
        return Some(left.cmp(&right));
    }
    if let Some(left) = json_number_as_i128(left) {
        return compare_i128_to_f64(left, right.as_f64()?);
    }
    if let Some(right) = json_number_as_i128(right) {
        return compare_i128_to_f64(right, left.as_f64()?).map(Ordering::reverse);
    }

    let left = left.as_f64()?;
    let right = right.as_f64()?;
    if !left.is_finite() || !right.is_finite() {
        return None;
    }
    left.partial_cmp(&right)
}

pub(crate) fn json_number_as_i128(value: &Number) -> Option<i128> {
    if let Some(value) = value.as_i64() {
        Some(i128::from(value))
    } else {
        value.as_u64().map(i128::from)
    }
}

#[cfg(feature = "schema")]
pub(crate) fn json_number_is_integer(value: &Number) -> bool {
    json_number_as_i128(value).is_some()
        || value
            .as_f64()
            .is_some_and(|value| value.is_finite() && value.fract() == 0.0)
}

fn compare_i128_to_f64(integer: i128, float: f64) -> Option<Ordering> {
    if !float.is_finite() {
        return None;
    }
    if let Some(float_integer) = whole_f64_to_i128(float) {
        return Some(integer.cmp(&float_integer));
    }

    if float >= I128_MAX_EXCLUSIVE_AS_F64 {
        return Some(Ordering::Less);
    }
    if float < I128_MIN_AS_F64 {
        return Some(Ordering::Greater);
    }

    let float_floor = f64_to_i128_in_range(float.floor())?;
    if integer <= float_floor {
        Some(Ordering::Less)
    } else {
        Some(Ordering::Greater)
    }
}

fn whole_f64_to_i128(value: f64) -> Option<i128> {
    (value.fract() == 0.0)
        .then(|| f64_to_i128_in_range(value))
        .flatten()
}

pub(crate) fn json_number_from_i128(value: i128) -> Option<Number> {
    i64::try_from(value)
        .map(Number::from)
        .or_else(|_| u64::try_from(value).map(Number::from))
        .ok()
}

pub(crate) fn json_number_from_u128(value: u128) -> Option<Number> {
    u64::try_from(value).map(Number::from).ok()
}

#[cfg(feature = "schema")]
pub(crate) fn finite_f64_to_i128(value: f64) -> Option<i128> {
    f64_to_i128_in_range(value)
}

fn f64_to_i128_in_range(value: f64) -> Option<i128> {
    (value.is_finite() && (I128_MIN_AS_F64..I128_MAX_EXCLUSIVE_AS_F64).contains(&value))
        .then_some(value as i128)
}

pub(crate) fn json_number_is_multiple_of(value: &Number, factor: &Number) -> Option<bool> {
    if let (Some(value), Some(factor)) = (json_number_as_i128(value), json_number_as_i128(factor)) {
        return (factor > 0).then_some(value % factor == 0);
    }

    if let (Some(value), Some(factor)) = (
        decimal_rational_from_number(value),
        positive_decimal_rational_from_number(factor),
    ) && let Some(is_multiple) = rational_is_multiple_of(value, factor)
    {
        return Some(is_multiple);
    }

    let value = value.as_f64()?;
    let factor = factor.as_f64()?;
    f64_is_multiple_of(value, factor)
}

#[cfg(feature = "schema")]
pub(crate) fn integer_multiple_step_from_json_number(number: &Number) -> Option<i128> {
    if let Some(integer) = json_number_as_i128(number) {
        return (integer > 0).then_some(integer);
    }

    positive_decimal_rational_from_number(number).and_then(|rational| {
        let divisor = gcd_i128(rational.numerator, rational.denominator);
        rational
            .numerator
            .checked_div(divisor)
            .filter(|step| *step > 0)
    })
}

fn f64_is_multiple_of(value: f64, factor: f64) -> Option<bool> {
    if !value.is_finite() || !factor.is_finite() || !factor.is_normal() || factor <= 0.0 {
        return None;
    }

    let quotient = value / factor;
    let nearest = quotient.round();
    let tolerance = f64::EPSILON * 16.0 * quotient.abs().max(1.0);
    Some((quotient - nearest).abs() <= tolerance)
}

fn rational_is_multiple_of(value: DecimalRational, factor: DecimalRational) -> Option<bool> {
    let dividend = value.numerator.checked_mul(factor.denominator)?;
    let divisor = value.denominator.checked_mul(factor.numerator)?;
    (divisor > 0).then_some(dividend % divisor == 0)
}

fn positive_decimal_rational_from_number(number: &Number) -> Option<DecimalRational> {
    decimal_rational_from_number(number).filter(|rational| rational.numerator > 0)
}

fn decimal_rational_from_number(number: &Number) -> Option<DecimalRational> {
    let text = number.to_string();
    let (mantissa, exponent) = match text.split_once(['e', 'E']) {
        Some((mantissa, exponent)) => (mantissa, exponent.parse::<i32>().ok()?),
        None => (text.as_str(), 0),
    };
    let (negative, mantissa) = match mantissa.strip_prefix('-') {
        Some(mantissa) => (true, mantissa),
        None => (false, mantissa),
    };

    let (whole, fraction) = mantissa.split_once('.').unwrap_or((mantissa, ""));
    let digits = format!("{whole}{fraction}");
    let unsigned_numerator = digits.parse::<i128>().ok()?;
    let numerator = if negative {
        unsigned_numerator.checked_neg()?
    } else {
        unsigned_numerator
    };

    let fraction_digits = i32::try_from(fraction.len()).ok()?;
    let scale = fraction_digits.checked_sub(exponent)?;
    let rational = if scale <= 0 {
        let multiplier = pow10_i128(scale.unsigned_abs())?;
        DecimalRational {
            numerator: numerator.checked_mul(multiplier)?,
            denominator: 1,
        }
    } else {
        DecimalRational {
            numerator,
            denominator: pow10_i128(u32::try_from(scale).ok()?)?,
        }
    };

    Some(reduce_decimal_rational(rational))
}

fn reduce_decimal_rational(rational: DecimalRational) -> DecimalRational {
    let divisor = gcd_i128(rational.numerator, rational.denominator);
    DecimalRational {
        numerator: rational.numerator / divisor,
        denominator: rational.denominator / divisor,
    }
}

fn pow10_i128(exponent: u32) -> Option<i128> {
    let mut value = 1i128;
    for _ in 0..exponent {
        value = value.checked_mul(10)?;
    }
    Some(value)
}

fn gcd_i128(mut left: i128, mut right: i128) -> i128 {
    while right != 0 {
        let remainder = left % right;
        left = right;
        right = remainder;
    }
    left.abs().max(1)
}

#[cfg(all(test, feature = "schema"))]
mod tests {
    use std::cmp::Ordering;

    use super::{
        compare_json_numbers, finite_f64_to_i128, json_number_is_integer,
        json_number_is_multiple_of,
    };

    #[test]
    fn finite_f64_to_i128_rejects_non_finite_and_out_of_range_values() {
        assert_eq!(finite_f64_to_i128(42.0), Some(42));
        assert_eq!(finite_f64_to_i128(-42.0), Some(-42));
        assert_eq!(finite_f64_to_i128(f64::NAN), None);
        assert_eq!(finite_f64_to_i128(f64::INFINITY), None);
        assert_eq!(finite_f64_to_i128(2.0_f64.powi(127)), None);
        assert_eq!(finite_f64_to_i128(-2.0_f64.powi(127) * 2.0), None);
    }

    #[test]
    fn json_number_is_integer_accepts_float_encoded_whole_numbers() {
        let whole = serde_json::json!(1.0);
        let fractional = serde_json::json!(1.5);

        assert!(json_number_is_integer(
            whole.as_number().expect("whole number")
        ));
        assert!(!json_number_is_integer(
            fractional.as_number().expect("fractional number")
        ));
    }

    #[test]
    fn compare_json_numbers_keeps_large_integer_float_comparisons_exact() {
        let integer = serde_json::json!(9_007_199_254_740_993_u64);
        let float = serde_json::json!(9_007_199_254_740_992.0_f64);

        assert_eq!(
            compare_json_numbers(
                integer.as_number().expect("integer"),
                float.as_number().expect("float")
            ),
            Some(Ordering::Greater)
        );
    }

    #[test]
    fn json_number_is_multiple_of_preserves_large_integer_precision() {
        let odd = serde_json::Number::from(9_007_199_254_740_993_u64);
        let even = serde_json::Number::from(9_007_199_254_740_994_u64);
        let factor = serde_json::Number::from_f64(2.0).expect("finite factor");

        assert_eq!(json_number_is_multiple_of(&odd, &factor), Some(false));
        assert_eq!(json_number_is_multiple_of(&even, &factor), Some(true));
    }
}