use std::cmp::Ordering;
use fraction::{BigFraction, BigInt, Integer, Sign};
use jsonschema_value::numeric_check::{divisor_kind, satisfies_multiple_of, DivisorKind};
use num_traits::{One, Signed, Zero};
use serde_json::Number;
use super::{normalized_number, BoundInteger};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct BoundRational {
limit: Number,
value: Option<BigFraction>,
}
enum IntegerFold {
Vacuous,
Divisor(BoundInteger),
Unfaithful,
}
impl BoundRational {
pub(crate) fn new(limit: &Number) -> Option<Self> {
let limit = normalized_number(limit);
limit.as_f64()?;
let value = exact(&limit).filter(|value| decimal(value).as_ref() == Some(&limit));
Some(Self { limit, value })
}
fn exact_value(&self) -> Option<&BigFraction> {
self.value.as_ref()
}
pub(crate) fn to_number(&self) -> Number {
self.limit.clone()
}
pub(crate) fn admits_only_whole(&self) -> bool {
matches!(self.kind(), DivisorKind::Whole | DivisorKind::WholeLossy)
}
fn kind(&self) -> DivisorKind {
divisor_kind(&self.limit)
}
pub(crate) fn divides(&self, value: &Number) -> bool {
satisfies_multiple_of(&self.limit, value)
}
pub(crate) fn divides_divisor(&self, other: &Self) -> bool {
let (Some(mine), Some(theirs)) = (self.exact_value(), other.exact_value()) else {
return false;
};
(theirs / mine.clone()).denom().is_none_or(One::is_one)
}
#[cfg_attr(feature = "arbitrary-precision", allow(clippy::unused_self))]
pub(crate) fn shares_arithmetic(&self, other: &Self) -> bool {
#[cfg(feature = "arbitrary-precision")]
{
let _ = other;
true
}
#[cfg(not(feature = "arbitrary-precision"))]
{
self.kind() == other.kind()
}
}
pub(crate) fn checked_lcm(&self, other: &Self) -> Option<Self> {
let (mine, theirs) = (self.exact_value()?, other.exact_value()?);
let numerator = mine.numer()?.lcm(theirs.numer()?);
let denominator = mine.denom()?.gcd(theirs.denom()?);
let combined = Self::new(&decimal(&BigFraction::new(numerator, denominator))?)?;
(combined.exact_value().is_some()
&& combined.shares_arithmetic(self)
&& combined.shares_arithmetic(other))
.then_some(combined)
}
pub(crate) fn admits_between(
&self,
minimum: Option<&super::BoundNumber>,
maximum: Option<&super::BoundNumber>,
) -> bool {
let Some(step) = self.exact_value() else {
return true;
};
let (Some(low), Some((maximum, high))) = (
minimum.and_then(|bound| exact(&bound.to_number())),
maximum.and_then(|bound| Some((bound, exact(&bound.to_number())?))),
) else {
return true;
};
let Some(candidate) =
step_index(&low, step, super::Round::Up).and_then(|index| grid_point(step, &index))
else {
return true;
};
if candidate > high {
return false;
}
maximum.is_inclusive() || candidate < high
}
pub(crate) fn without_factors_of(&self, other: &Self) -> Option<Self> {
let (mine, theirs) = (self.whole()?, other.whole()?);
let (mut shared, mut rest) = (fraction::BigUint::one(), mine.clone());
loop {
let common = rest.gcd(theirs);
if common.is_one() {
break;
}
shared *= &common;
rest /= &common;
}
if shared.is_one() || !(theirs % &shared).is_zero() {
return None;
}
Self::new(&decimal(&BigFraction::new(rest, fraction::BigUint::one()))?)
.filter(|stripped| stripped.exact_value().is_some())
}
fn whole(&self) -> Option<&fraction::BigUint> {
let value = self.exact_value()?;
value.denom()?.is_one().then(|| value.numer()).flatten()
}
pub(crate) fn multiple_beyond(
&self,
bound: &super::BoundNumber,
direction: super::Round,
) -> Option<super::BoundNumber> {
let step = self.exact_value()?;
let spelling = bound.to_number();
let limit = exact(&spelling).filter(|limit| decimal(limit).as_ref() == Some(&spelling))?;
let mut index = step_index(&limit, step, direction)?;
let mut candidate = grid_point(step, &index)?;
if !bound.is_inclusive() && candidate == limit {
index = match direction {
super::Round::Up => index + 1_u8,
super::Round::Down => index - 1_u8,
};
candidate = grid_point(step, &index)?;
}
let snapped = decimal(&candidate)?;
(exact(&snapped).as_ref() == Some(&candidate))
.then(|| super::BoundNumber::new(&snapped, true))
}
pub(crate) fn is_identity(&self) -> bool {
self.exact_value().is_some_and(One::is_one)
}
pub(crate) fn is_vacuous_over_integers(&self) -> bool {
matches!(self.integer_fold(), IntegerFold::Vacuous)
}
pub(crate) fn exact_integer(&self) -> Option<BoundInteger> {
match self.integer_fold() {
IntegerFold::Divisor(divisor) => Some(divisor),
IntegerFold::Vacuous | IntegerFold::Unfaithful => None,
}
}
fn integer_fold(&self) -> IntegerFold {
let Some(numerator) = self
.exact_value()
.and_then(BigFraction::numer)
.and_then(|numerator| numerator.to_string().parse().ok())
.and_then(|numerator: Number| BoundInteger::from_number(&numerator))
else {
return IntegerFold::Unfaithful;
};
if numerator.is_one() {
return IntegerFold::Vacuous;
}
match self.kind() {
DivisorKind::Whole if numerator.is_exact_in_f64() => IntegerFold::Divisor(numerator),
DivisorKind::Whole | DivisorKind::WholeLossy | DivisorKind::Fractional => {
IntegerFold::Unfaithful
}
}
}
}
impl PartialOrd for BoundRational {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for BoundRational {
fn cmp(&self, other: &Self) -> Ordering {
self.value
.cmp(&other.value)
.then_with(|| self.limit.to_string().cmp(&other.limit.to_string()))
}
}
fn step_index(
bound: &BigFraction,
divisor: &BigFraction,
direction: super::Round,
) -> Option<BigInt> {
let (bound_numerator, bound_denominator) = (bound.numer()?, bound.denom()?);
let (divisor_numerator, divisor_denominator) = (divisor.numer()?, divisor.denom()?);
let magnitude = BigInt::from(bound_numerator * divisor_denominator);
let numerator = if bound.is_sign_negative() {
-magnitude
} else {
magnitude
};
let denominator = BigInt::from(bound_denominator * divisor_numerator);
Some(match direction {
super::Round::Up => numerator.div_ceil(&denominator),
super::Round::Down => numerator.div_floor(&denominator),
})
}
fn grid_point(divisor: &BigFraction, index: &BigInt) -> Option<BigFraction> {
let (divisor_numerator, divisor_denominator) = (divisor.numer()?, divisor.denom()?);
let magnitude = index.magnitude();
let common = (magnitude % divisor_denominator).gcd(divisor_denominator);
let numerator = divisor_numerator * (magnitude / &common);
let denominator = divisor_denominator / &common;
debug_assert!(
(&numerator % &denominator).gcd(&denominator).is_one(),
"a reduced divisor times a whole step is already in lowest terms"
);
let sign = if index.is_negative() {
Sign::Minus
} else {
Sign::Plus
};
Some(BigFraction::new_raw_signed(sign, numerator, denominator))
}
fn exact(number: &Number) -> Option<BigFraction> {
#[cfg(feature = "arbitrary-precision")]
{
if let Some(value) = jsonschema_value::numeric::bignum::try_parse_bigint(number) {
return Some(whole(value));
}
if let Some(value) = jsonschema_value::numeric::bignum::try_parse_bigfraction(number) {
return Some(value);
}
}
number.as_f64().map(BigFraction::from)
}
#[cfg(feature = "arbitrary-precision")]
fn whole(value: num_bigint::BigInt) -> BigFraction {
let (sign, magnitude) = value.into_parts();
let fraction = BigFraction::new_raw(magnitude, fraction::BigUint::one());
if sign == num_bigint::Sign::Minus {
-fraction
} else {
fraction
}
}
fn decimal(value: &BigFraction) -> Option<Number> {
let (numerator, denominator) = (value.numer()?, value.denom()?);
let (mut rest, mut twos, mut fives) = (denominator.clone(), 0_u32, 0_u32);
let (two, five) = (fraction::BigUint::from(2_u8), fraction::BigUint::from(5_u8));
while (&rest % &two).is_zero() {
rest /= &two;
twos += 1;
}
while (&rest % &five).is_zero() {
rest /= &five;
fives += 1;
}
debug_assert!(
rest.is_one(),
"a JSON number's denominator is a power of ten"
);
let places = twos.max(fives);
let text = spelled(numerator, denominator, places, value.is_sign_negative());
debug_assert_eq!(
text,
format!("{value:.width$}", width = places as usize),
"the scaled spelling reads as the formatted one"
);
text.parse().ok()
}
fn spelled(
numerator: &fraction::BigUint,
denominator: &fraction::BigUint,
places: u32,
negative: bool,
) -> String {
let scale = fraction::BigUint::from(10_u8).pow(places);
debug_assert!(
(&scale % denominator).is_zero(),
"ten to the places is a multiple of the denominator"
);
let digits = (numerator * (scale / denominator)).to_string();
let places = places as usize;
let mut text = String::with_capacity(digits.len() + places + 3);
if negative {
text.push('-');
}
if places == 0 {
text.push_str(&digits);
return text;
}
if let Some(width) = digits.len().checked_sub(places).filter(|whole| *whole > 0) {
let (whole, fraction) = digits.split_at(width);
text.push_str(whole);
text.push('.');
text.push_str(fraction);
} else {
text.push_str("0.");
for _ in 0..places - digits.len() {
text.push('0');
}
text.push_str(&digits);
}
text
}