use crate::error::{Error, Result};
use num_bigint::BigUint;
use num_traits::{One, Pow, Zero};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Max {
Pow10(u32),
Unbounded,
}
impl Max {
#[must_use]
pub const fn western(scale_count: u32) -> Max {
Max::Pow10((scale_count + 1) * 3)
}
#[must_use]
pub const fn myriad(scale_count: u32) -> Max {
Max::Pow10((scale_count + 1) * 4)
}
#[must_use]
pub const fn indian(word_count: u32) -> Max {
Max::Pow10(3 + 2 * (word_count - 1))
}
#[must_use]
pub const fn long_scale(unit_count: u32) -> Max {
Max::Pow10(6 * (unit_count + 1))
}
#[must_use]
pub const fn bounded(exponent: u32) -> Max {
Max::Pow10(exponent)
}
#[must_use]
pub const fn exponent(self) -> Option<u32> {
match self {
Max::Pow10(e) => Some(e),
Max::Unbounded => None,
}
}
#[must_use]
pub const fn is_bounded(self) -> bool {
matches!(self, Max::Pow10(_))
}
#[must_use]
pub fn value(self) -> Option<BigUint> {
self.exponent().map(pow10)
}
#[must_use]
pub fn largest(self) -> Option<BigUint> {
self.value().map(|v| v - BigUint::one())
}
#[must_use]
pub fn largest_display(self) -> String {
match self {
Max::Pow10(e) => format!("10^{e} - 1"),
Max::Unbounded => "∞".to_string(),
}
}
pub fn check(self, value: &BigUint, fraction: Option<&str>) -> Result<()> {
let Max::Pow10(exponent) = self else {
return Ok(());
};
if reaches(value, exponent) {
return Err(Error::OutOfRange { max: self });
}
if let Some(digits) = fraction {
let significant = digits.trim_start_matches('0').len();
if significant > exponent as usize {
return Err(Error::OutOfRange { max: self });
}
}
Ok(())
}
}
impl fmt::Display for Max {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Max::Pow10(e) => write!(f, "10^{e}"),
Max::Unbounded => f.write_str("∞"),
}
}
}
#[must_use]
pub fn pow10(exponent: u32) -> BigUint {
BigUint::from(10u32).pow(exponent)
}
fn reaches(value: &BigUint, exponent: u32) -> bool {
if value.is_zero() {
return false;
}
const LOG2_10_LOW: u64 = 3321; const LOG2_10_HIGH: u64 = 3322; let bits = value.bits();
let exponent = u64::from(exponent);
if bits * 1000 <= exponent * LOG2_10_LOW {
return false;
}
if (bits - 1) * 1000 >= exponent * LOG2_10_HIGH {
return true;
}
*value >= pow10(exponent as u32)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn constructors_match_the_javascript_helpers() {
assert_eq!(Max::western(21), Max::Pow10(66));
assert_eq!(Max::myriad(17), Max::Pow10(72));
assert_eq!(Max::indian(9), Max::Pow10(19));
assert_eq!(Max::long_scale(4), Max::Pow10(30));
assert_eq!(Max::bounded(16), Max::Pow10(16));
}
#[test]
fn check_pins_the_exact_boundary() {
for e in 1..80u32 {
let max = Max::Pow10(e);
let ceiling = pow10(e);
assert!(
max.check(&(&ceiling - BigUint::one()), None).is_ok(),
"10^{e} - 1 must pass"
);
assert_eq!(
max.check(&ceiling, None),
Err(Error::OutOfRange { max }),
"10^{e} must fail"
);
assert!(max.check(&(&ceiling + BigUint::one()), None).is_err());
}
assert!(Max::Pow10(3).check(&BigUint::zero(), None).is_ok());
assert!(Max::Unbounded.check(&pow10(500), Some("1234567")).is_ok());
}
#[test]
fn check_applies_to_integer_spelled_fractions() {
let max = Max::Pow10(3);
assert!(max.check(&BigUint::from(5u32), Some("999")).is_ok());
assert!(max.check(&BigUint::from(5u32), Some("000999")).is_ok());
assert!(max.check(&BigUint::from(5u32), Some("1000")).is_err());
}
#[test]
fn display() {
assert_eq!(Max::Pow10(66).largest_display(), "10^66 - 1");
assert_eq!(Max::Unbounded.largest_display(), "∞");
assert_eq!(Max::Pow10(9).largest(), Some(BigUint::from(999_999_999u32)));
assert_eq!(Max::Unbounded.value(), None);
}
}