use std::fmt;
use fixed_decimal::Decimal;
use icu_decimal::DecimalFormatter;
#[derive(Clone, Copy, Debug)]
pub enum Number {
Int(i64),
UInt(u64),
Float(f64),
}
impl Number {
pub fn as_f64(&self) -> f64 {
match self {
Number::Int(v) => *v as f64,
Number::UInt(v) => *v as f64,
Number::Float(v) => *v,
}
}
}
impl fmt::Display for Number {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Number::Int(v) => write!(f, "{v}"),
Number::UInt(v) => write!(f, "{v}"),
Number::Float(v) => write!(f, "{v}"),
}
}
}
macro_rules! impl_from_int { ($($t:ty),*) => { $(impl From<$t> for Number { fn from(v: $t) -> Self { Number::Int(v as i64) } })* }; }
macro_rules! impl_from_uint { ($($t:ty),*) => { $(impl From<$t> for Number { fn from(v: $t) -> Self { Number::UInt(v as u64) } })* }; }
impl_from_int!(i8, i16, i32, i64, isize);
impl_from_uint!(u8, u16, u32, u64, usize);
impl From<f32> for Number {
fn from(v: f32) -> Self {
Number::Float(v as f64)
}
}
impl From<f64> for Number {
fn from(v: f64) -> Self {
Number::Float(v)
}
}
pub struct NumberArg {
pub value: Number,
display: Option<String>,
}
impl NumberArg {
pub fn new(n: impl Into<Number>) -> Self {
Self {
value: n.into(),
display: None,
}
}
pub fn with_display(n: impl Into<Number>, display: impl Into<String>) -> Self {
Self {
value: n.into(),
display: Some(display.into()),
}
}
pub fn format_display(&self, locale_code: &str) -> String {
match &self.display {
Some(s) => s.clone(),
None => format_number_icu(locale_code, &self.value),
}
}
}
impl<T: Into<Number>> From<T> for NumberArg {
fn from(v: T) -> Self {
Self::new(v)
}
}
fn format_number_icu(locale_code: &str, n: &Number) -> String {
let locale = super::parse_icu_locale(locale_code);
let Ok(fmt) = DecimalFormatter::try_new(locale.into(), Default::default()) else {
return format!("{n}");
};
match n {
Number::Int(v) => fmt.format(&Decimal::from(*v)).to_string(),
Number::UInt(v) => fmt.format(&Decimal::from(*v)).to_string(),
Number::Float(v) => {
let s = format!("{v}");
match s.parse::<Decimal>() {
Ok(d) => fmt.format(&d).to_string(),
Err(_) => s,
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn number_auto_format_en() {
assert_eq!(NumberArg::new(1000i64).format_display("en"), "1,000");
}
#[test]
fn number_auto_format_fr() {
let s = NumberArg::new(1000i64).format_display("fr");
assert!(s.contains('1') && s.contains("000"), "got: {s:?}");
assert_ne!(s, "1000", "should be locale-formatted, not plain");
}
#[test]
fn number_auto_format_de() {
let s = NumberArg::new(1000i64).format_display("de");
assert!(s.contains('1') && s.contains("000"), "got: {s:?}");
}
#[test]
fn number_custom_display() {
let n = NumberArg::with_display(1_000_000i64, "1M");
assert_eq!(n.format_display("en"), "1M");
assert_eq!(n.format_display("fr"), "1M");
}
#[test]
fn number_arg_from_primitives() {
let _: NumberArg = 5i32.into();
let _: NumberArg = 5u64.into();
let _: NumberArg = core::f64::consts::PI.into();
}
}