use fixed_decimal::{CompactDecimal, Decimal, UnsignedDecimal};
const LIMIT_18_DIGITS: u64 = 1_000_000_000_000_000_000;
#[derive(Debug, Clone, Copy, PartialEq, Default)]
#[allow(clippy::exhaustive_structs)] pub struct PluralOperands {
pub(crate) i: u64,
pub(crate) v: usize,
pub(crate) w: usize,
pub(crate) f: u64,
pub(crate) t: u64,
pub(crate) c: usize,
}
#[derive(displaydoc::Display, Debug, PartialEq, Eq)]
#[non_exhaustive]
#[cfg(feature = "datagen")]
pub enum OperandsError {
#[displaydoc("Input to the Operands parsing was empty")]
Empty,
#[displaydoc("Input to the Operands parsing was invalid")]
Invalid,
}
#[cfg(feature = "datagen")]
impl core::error::Error for OperandsError {}
#[cfg(feature = "datagen")]
impl From<core::num::ParseIntError> for OperandsError {
fn from(_: core::num::ParseIntError) -> Self {
Self::Invalid
}
}
#[cfg(feature = "datagen")]
impl core::str::FromStr for PluralOperands {
type Err = OperandsError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
fn get_exponent(input: &str) -> Result<(&str, usize), OperandsError> {
if let Some((base, exponent)) = input.split_once('e') {
Ok((base, exponent.parse()?))
} else {
Ok((input, 0))
}
}
if input.is_empty() {
return Err(OperandsError::Empty);
}
let abs_str = input.strip_prefix('-').unwrap_or(input);
let (
integer_digits,
num_fraction_digits0,
num_fraction_digits,
fraction_digits0,
fraction_digits,
exponent,
) = if let Some((int_str, rest)) = abs_str.split_once('.') {
let (dec_str, exponent) = get_exponent(rest)?;
let integer_digits = u64::from_str(int_str)?;
let dec_str_no_zeros = dec_str.trim_end_matches('0');
let num_fraction_digits0 = dec_str.len();
let num_fraction_digits = dec_str_no_zeros.len();
let fraction_digits0 = u64::from_str(dec_str)?;
let fraction_digits =
if num_fraction_digits == 0 || num_fraction_digits == num_fraction_digits0 {
fraction_digits0
} else {
u64::from_str(dec_str_no_zeros)?
};
(
integer_digits,
num_fraction_digits0,
num_fraction_digits,
fraction_digits0,
fraction_digits,
exponent,
)
} else {
let (abs_str, exponent) = get_exponent(abs_str)?;
let integer_digits = u64::from_str(abs_str)?;
(integer_digits, 0, 0, 0, 0, exponent)
};
Ok(Self {
i: integer_digits,
v: num_fraction_digits0,
w: num_fraction_digits,
f: fraction_digits0,
t: fraction_digits,
c: exponent,
})
}
}
macro_rules! impl_integer_type {
($ty:ident) => {
impl From<$ty> for PluralOperands {
#[inline]
#[allow(trivial_numeric_casts)]
fn from(input: $ty) -> Self {
Self {
i: input as u64,
v: 0,
w: 0,
f: 0,
t: 0,
c: 0,
}
}
}
};
($($ty:ident)+) => {
$(impl_integer_type!($ty);)+
};
}
macro_rules! impl_signed_integer_type {
($ty:ident) => {
impl From<$ty> for PluralOperands {
#[inline]
fn from(input: $ty) -> Self {
input.unsigned_abs().into()
}
}
};
($($ty:ident)+) => {
$(impl_signed_integer_type!($ty);)+
};
}
impl_integer_type!(u8 u16 u32 u64 usize);
impl_signed_integer_type!(i8 i16 i32 i64 i128 isize);
impl From<u128> for PluralOperands {
#[inline]
fn from(input: u128) -> Self {
let i = if input < LIMIT_18_DIGITS as u128 {
input as u64
} else {
(input % LIMIT_18_DIGITS as u128) as u64 + LIMIT_18_DIGITS
};
Self {
i,
v: 0,
w: 0,
f: 0,
t: 0,
c: 0,
}
}
}
impl PluralOperands {
#[doc(hidden)] pub fn from_significand_and_exponent(dec: &UnsignedDecimal, exp: u8) -> PluralOperands {
let exp_i16 = i16::from(exp);
let mag_range = dec.magnitude_range();
let upper_mag = (*mag_range.end()).saturating_add(exp_i16);
let lower_mag = (*mag_range.start()).saturating_add(exp_i16);
let mag_high = upper_mag.clamp(0, 17);
let mag_low = lower_mag.clamp(-18, 0);
let mut i: u64 = 0;
for magnitude in (0..=mag_high).rev() {
i *= 10;
i += dec.digit_at(magnitude - exp_i16) as u64;
}
if upper_mag > 17 {
i += LIMIT_18_DIGITS;
}
let mut f: u64 = 0;
let mut t: u64 = 0;
let mut w: usize = 0;
for magnitude in (mag_low..=-1).rev() {
let digit = dec.digit_at(magnitude - exp_i16) as u64;
f *= 10;
f += digit;
if digit != 0 {
t = f;
w = (-magnitude) as usize;
}
}
Self {
i,
v: (-mag_low) as usize,
w,
f,
t,
c: usize::from(exp),
}
}
pub(crate) fn is_exactly_zero(self) -> bool {
self == Self {
i: 0,
v: 0,
w: 0,
f: 0,
t: 0,
c: 0,
}
}
pub(crate) fn is_exactly_one(self) -> bool {
self == Self {
i: 1,
v: 0,
w: 0,
f: 0,
t: 0,
c: 0,
}
}
}
impl From<&Decimal> for PluralOperands {
fn from(dec: &Decimal) -> Self {
(&dec.absolute).into()
}
}
impl From<&UnsignedDecimal> for PluralOperands {
fn from(value: &UnsignedDecimal) -> Self {
Self::from_significand_and_exponent(value, 0)
}
}
impl From<&CompactDecimal> for PluralOperands {
fn from(compact: &CompactDecimal) -> Self {
Self::from_significand_and_exponent(&compact.significand().absolute, compact.exponent())
}
}
#[cfg(test)]
mod tests {
use super::*;
use core::str::FromStr;
use fixed_decimal::Decimal;
#[test]
fn test_u128_overflow() {
let limit_18 = LIMIT_18_DIGITS;
let val_small: u128 = 123;
let ops = PluralOperands::from(val_small);
assert_eq!(ops.i, 123);
assert_eq!(ops.v, 0);
let val_max = u128::MAX;
let ops = PluralOperands::from(val_max);
assert!(ops.i >= limit_18, "i should be offset by LIMIT_18_DIGITS");
assert_eq!(ops.i % 10, 5, "Mod 10 Check");
assert_eq!(ops.i % 100, 55, "Mod 100 Check");
let val_limit = limit_18 as u128;
let ops = PluralOperands::from(val_limit);
assert_eq!(ops.i, limit_18);
let val_limit_plus_1 = val_limit + 1;
let ops = PluralOperands::from(val_limit_plus_1);
assert_eq!(ops.i, limit_18 + 1);
}
#[test]
fn test_fixed_decimal_overflow() {
let limit_18 = LIMIT_18_DIGITS;
let mut s = "1".to_string();
for _ in 0..29 {
s.push('0');
}
s.push('7');
let dec = Decimal::from_str(&s).unwrap();
let ops = PluralOperands::from(&dec);
assert!(ops.i >= limit_18, "i should be offset");
assert_eq!(ops.i % 10, 7, "Mod 10 check");
assert_eq!(ops.i, limit_18 + 7);
}
#[test]
fn test_i128_overflow() {
let limit_18 = LIMIT_18_DIGITS;
let val = i128::MIN;
let ops = PluralOperands::from(val);
assert!(ops.i >= limit_18);
assert_eq!(ops.i % 10, 8);
}
#[test]
fn test_compact_decimal_overflow() {
use fixed_decimal::CompactDecimal;
let limit_18 = LIMIT_18_DIGITS;
let mut s = "9".repeat(i16::MAX as usize);
s.push_str("c2");
let cd = CompactDecimal::from_str(&s).unwrap();
let ops = PluralOperands::from(&cd);
assert!(ops.i >= limit_18, "i should be offset");
}
}