use crate::NiceFloat;
use super::{
Digiter,
nice_uint,
NiceChar,
};
#[expect(dead_code, reason = "For readability.")]
#[repr(u8)]
#[derive(Debug, Clone, Copy, Eq, Ord, PartialEq, PartialOrd)]
enum NicePercentIdx {
From00 = 0_u8, From01 = 1_u8, From02 = 2_u8, From03 = 3_u8, From04 = 4_u8, From05 = 5_u8, From06 = 6_u8, }
impl NicePercentIdx {
const DIGITS: [Self; 4] = [
Self::From05, Self::From04, Self::From02, Self::From01,
];
const LEN: usize = 7;
}
#[derive(Clone, Copy)]
pub struct NicePercent {
data: [NiceChar; NicePercentIdx::LEN],
from: NicePercentIdx,
}
impl NicePercent {
pub const MIN: Self = Self {
data: [
NiceChar::Digit0, NiceChar::Digit0, NiceChar::Digit0,
NiceChar::Period,
NiceChar::Digit0, NiceChar::Digit0,
NiceChar::Percent,
],
from: NicePercentIdx::From02,
};
pub const MAX: Self = Self {
data: [
NiceChar::Digit1, NiceChar::Digit0, NiceChar::Digit0,
NiceChar::Period,
NiceChar::Digit0, NiceChar::Digit0,
NiceChar::Percent,
],
from: NicePercentIdx::From00,
};
}
macro_rules! from {
($($ty:ty)+) => ($(
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "It is what it is.",
)]
impl From<$ty> for NicePercent {
#[doc = concat!("# Percent From `", stringify!($ty), "`.")]
fn from(num: $ty) -> Self {
if num.is_nan() { return Self::MIN; }
let whole = (num.clamp(0.0, 1.0) * 10_000.0).round() as u16;
if 9999 < whole { return Self::MAX; }
let mut out = Self::MIN;
if let Some(digits) = Digiter::<u16>::new(whole) {
for (k, v) in NicePercentIdx::DIGITS.into_iter().zip(digits) {
out.data[k as usize] = v;
}
if ! matches!(out.data[1], NiceChar::Digit0) {
out.from = NicePercentIdx::From01;
}
}
out
}
}
)+);
}
from!(f32 f64);
macro_rules! div_int {
($($ty:ident $fn:ident),+ $(,)?) => ($(
impl From<($ty, $ty)> for NicePercent {
#[inline]
#[doc = concat!("# Percent From `", stringify!($ty), "`/`", stringify!($ty), "`.")]
#[doc = concat!("[`NiceFloat::", stringify!($fn), "`]")]
fn from((e, d): ($ty, $ty)) -> Self {
match NiceFloat::$fn(e, d) {
Ok(f) | Err(f) => Self::from(f)
}
}
}
)+);
}
div_int! {
u8 div_u8,
u16 div_u16,
u32 div_u32,
u64 div_u64,
u128 div_u128,
usize div_usize,
i8 div_i8,
i16 div_i16,
i32 div_i32,
i64 div_i64,
i128 div_i128,
isize div_isize,
}
nice_uint!(@traits NicePercent);
nice_uint!(@bytes NicePercent, "1.0_f32", "100.00%");
impl NicePercent {
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "False positive.",
)]
pub fn replace(&mut self, num: f32) {
if num.is_nan() { return self.reset_min(); }
let whole = (num.clamp(0.0, 1.0) * 10_000.0).round() as u16;
if 9999 < whole { return self.reset_max(); }
self.reset_min();
if let Some(digits) = Digiter::<u16>::new(whole) {
for (k, v) in NicePercentIdx::DIGITS.into_iter().zip(digits) {
self.data[k as usize] = v;
}
if ! matches!(self.data[1], NiceChar::Digit0) {
self.from = NicePercentIdx::From01;
}
}
}
const fn reset_min(&mut self) {
self.data[2] = NiceChar::Digit0;
self.data[4] = NiceChar::Digit0;
self.data[5] = NiceChar::Digit0;
self.from = NicePercentIdx::From02;
}
const fn reset_max(&mut self) {
self.data[0] = NiceChar::Digit1;
self.data[1] = NiceChar::Digit0;
self.data[2] = NiceChar::Digit0;
self.data[4] = NiceChar::Digit0;
self.data[5] = NiceChar::Digit0;
self.from = NicePercentIdx::From00;
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeSet;
#[test]
fn t_nice_idx() {
let mut digits = NicePercentIdx::DIGITS.into_iter().map(|d| d as u8);
let mut last = digits.next().unwrap();
for next in digits {
assert!(
next < last,
concat!("BUG: NicePercentIdx::DIGITS are not descending!"),
);
last = next;
}
}
#[cfg(miri)]
const SAMPLE_SIZE: usize = 500;
#[test]
fn t_nice() {
const TOTAL: u32 = 10_000;
assert_eq!(NicePercent::MIN, NicePercent::from(f32::NAN));
assert_eq!(NicePercent::default(), NicePercent::from(f32::MIN));
assert_eq!(NicePercent::MIN, NicePercent::from(f32::MIN));
assert_eq!(NicePercent::MAX, NicePercent::from(f32::MAX));
let set: BTreeSet<u32>;
#[cfg(not(miri))]
{
set = (0..TOTAL).collect();
}
#[cfg(miri)]
{
let mut rng = fastrand::Rng::new();
set = std::iter::repeat_with(|| rng.u32(0..TOTAL))
.take(SAMPLE_SIZE)
.collect();
}
let mut last = NicePercent::MAX;
for i in set {
let fraction = i as f32 / TOTAL as f32;
let nice = NicePercent::from(fraction);
let istr = format!("{:0.02}%", fraction * 100.0);
assert_eq!(istr, nice.as_str());
assert_eq!(istr.as_bytes(), nice.as_bytes());
assert_eq!(istr.len(), nice.len());
assert_ne!(nice, last);
last.replace(fraction);
assert_eq!(nice, last);
assert_eq!(
NicePercent::from(i as f64 / TOTAL as f64),
nice,
);
}
last.replace(0.0);
assert_eq!(last.as_str(), "0.00%");
last.replace(1.0);
assert_eq!(last.as_str(), "100.00%");
}
}