use std::fmt::Debug;
mod sealed {
pub trait Sealed {}
impl Sealed for i32 {}
impl Sealed for i64 {}
impl Sealed for u8 {}
impl Sealed for u32 {}
impl Sealed for u64 {}
impl Sealed for usize {}
impl Sealed for f32 {}
impl Sealed for f64 {}
}
pub trait SpinValue: sealed::Sealed + Copy + PartialOrd + Debug + 'static {
fn to_f64(self) -> f64;
fn from_f64_saturating(v: f64) -> Self;
fn parse(s: &str) -> Option<Self>;
fn format(self, decimals: u8) -> String;
fn saturating_add(self, rhs: Self) -> Self;
fn saturating_sub(self, rhs: Self) -> Self;
fn saturating_mul_u32(self, rhs: u32) -> Self;
fn is_integer() -> bool;
fn is_valid_input_char(c: char) -> bool;
fn clamp_value(self, min: Self, max: Self) -> Self {
if self < min {
min
} else if self > max {
max
} else {
self
}
}
}
macro_rules! impl_spin_value_int {
($t:ty, signed = $signed:expr) => {
impl SpinValue for $t {
fn to_f64(self) -> f64 {
self as f64
}
fn from_f64_saturating(v: f64) -> Self {
if v.is_nan() {
return 0;
}
if v <= <$t>::MIN as f64 {
return <$t>::MIN;
}
if v >= <$t>::MAX as f64 {
return <$t>::MAX;
}
v as Self
}
fn parse(s: &str) -> Option<Self> {
s.trim().parse::<Self>().ok()
}
fn format(self, _decimals: u8) -> String {
self.to_string()
}
fn saturating_add(self, rhs: Self) -> Self {
<$t>::saturating_add(self, rhs)
}
fn saturating_sub(self, rhs: Self) -> Self {
<$t>::saturating_sub(self, rhs)
}
fn saturating_mul_u32(self, rhs: u32) -> Self {
let wide = (self as i128) * (rhs as i128);
if wide <= <$t>::MIN as i128 {
<$t>::MIN
} else if wide >= <$t>::MAX as i128 {
<$t>::MAX
} else {
wide as Self
}
}
fn is_integer() -> bool {
true
}
fn is_valid_input_char(c: char) -> bool {
if $signed {
c.is_ascii_digit() || c == '-'
} else {
c.is_ascii_digit()
}
}
}
};
}
impl_spin_value_int!(i32, signed = true);
impl_spin_value_int!(i64, signed = true);
impl_spin_value_int!(u8, signed = false);
impl_spin_value_int!(u32, signed = false);
impl_spin_value_int!(u64, signed = false);
impl_spin_value_int!(usize, signed = false);
macro_rules! impl_spin_value_float {
($t:ty) => {
impl SpinValue for $t {
fn to_f64(self) -> f64 {
self as f64
}
fn from_f64_saturating(v: f64) -> Self {
if v.is_nan() {
return 0.0;
}
v as Self
}
fn parse(s: &str) -> Option<Self> {
s.trim()
.parse::<Self>()
.ok()
.filter(|f: &Self| f.is_finite())
}
fn format(self, decimals: u8) -> String {
format!("{:.*}", decimals as usize, self)
}
fn saturating_add(self, rhs: Self) -> Self {
let r = self + rhs;
if r.is_finite() {
r
} else if r.is_sign_positive() {
<$t>::MAX
} else {
<$t>::MIN
}
}
fn saturating_sub(self, rhs: Self) -> Self {
let r = self - rhs;
if r.is_finite() {
r
} else if r.is_sign_positive() {
<$t>::MAX
} else {
<$t>::MIN
}
}
fn saturating_mul_u32(self, rhs: u32) -> Self {
let r = self * (rhs as Self);
if r.is_finite() {
r
} else if r.is_sign_positive() {
<$t>::MAX
} else {
<$t>::MIN
}
}
fn is_integer() -> bool {
false
}
fn is_valid_input_char(c: char) -> bool {
c.is_ascii_digit() || c == '-' || c == '+' || c == '.' || c == 'e' || c == 'E'
}
}
};
}
impl_spin_value_float!(f32);
impl_spin_value_float!(f64);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn i32_round_trip() {
assert_eq!(<i32 as SpinValue>::parse("42"), Some(42));
assert_eq!(<i32 as SpinValue>::parse("-1"), Some(-1));
assert_eq!(<i32 as SpinValue>::parse(" 7 "), Some(7));
assert_eq!(<i32 as SpinValue>::parse("abc"), None);
assert_eq!((42_i32).format(0), "42");
}
#[test]
fn u32_rejects_negative() {
assert_eq!(<u32 as SpinValue>::parse("-5"), None);
assert!(!<u32 as SpinValue>::is_valid_input_char('-'));
}
#[test]
fn f64_formats_with_decimals() {
assert_eq!((std::f64::consts::PI).format(2), "3.14");
assert_eq!((std::f64::consts::PI).format(4), "3.1416");
assert_eq!((0.1_f64 + 0.2).format(1), "0.3");
}
#[test]
fn f64_parse_rejects_nan_and_infinity() {
assert_eq!(<f64 as SpinValue>::parse("nan"), None);
assert_eq!(<f64 as SpinValue>::parse("inf"), None);
assert_eq!(<f64 as SpinValue>::parse("3.5"), Some(3.5));
}
#[test]
fn integer_saturating_add_clamps() {
let v = i32::MAX - 1;
assert_eq!(SpinValue::saturating_add(v, 10_i32), i32::MAX);
assert_eq!(SpinValue::saturating_sub(i32::MIN, 1_i32), i32::MIN);
}
#[test]
fn page_step_multiply_saturates() {
assert_eq!(
SpinValue::saturating_mul_u32(1_000_000_i32, 1_000_000),
i32::MAX
);
}
#[test]
fn is_integer_flag() {
assert!(<i32 as SpinValue>::is_integer());
assert!(<u64 as SpinValue>::is_integer());
assert!(!<f32 as SpinValue>::is_integer());
assert!(!<f64 as SpinValue>::is_integer());
}
#[test]
fn clamp_value() {
assert_eq!((42_i32).clamp_value(0, 100), 42);
assert_eq!((150_i32).clamp_value(0, 100), 100);
assert_eq!((-5_i32).clamp_value(0, 100), 0);
}
#[test]
fn u8_round_trip() {
assert_eq!(<u8 as SpinValue>::parse("0"), Some(0));
assert_eq!(<u8 as SpinValue>::parse("128"), Some(128));
assert_eq!(<u8 as SpinValue>::parse("255"), Some(255));
assert_eq!(<u8 as SpinValue>::parse("256"), None); assert_eq!(<u8 as SpinValue>::parse("-1"), None);
assert!(!<u8 as SpinValue>::is_valid_input_char('-'));
assert_eq!((255_u8).format(0), "255");
assert_eq!(SpinValue::saturating_add(250_u8, 10_u8), u8::MAX);
assert_eq!(SpinValue::saturating_sub(5_u8, 10_u8), u8::MIN);
assert!(<u8 as SpinValue>::is_integer());
}
}