1#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
22pub enum Color {
23 #[default]
25 Gray,
26 Primary,
28 Success,
30 Warning,
32 Danger,
34 Info,
36}
37
38impl Color {
39 #[doc(hidden)]
41 pub const fn from_name(name: &str) -> Option<Self> {
42 Some(match name.as_bytes() {
43 b"gray" => Self::Gray,
44 b"primary" => Self::Primary,
45 b"success" => Self::Success,
46 b"warning" => Self::Warning,
47 b"danger" => Self::Danger,
48 b"info" => Self::Info,
49 _ => return None,
50 })
51 }
52
53 pub(crate) const fn dot_class(self) -> &'static str {
55 match self {
56 Self::Gray => "size-1.5 shrink-0 rounded-full bg-muted-foreground/60",
57 Self::Primary => "size-1.5 shrink-0 rounded-full bg-primary",
58 Self::Success => "size-1.5 shrink-0 rounded-full bg-success",
59 Self::Warning => "size-1.5 shrink-0 rounded-full bg-warning",
60 Self::Danger => "size-1.5 shrink-0 rounded-full bg-destructive",
61 Self::Info => "size-1.5 shrink-0 rounded-full bg-info",
62 }
63 }
64
65 pub(crate) const fn text_class(self) -> &'static str {
67 match self {
68 Self::Gray => "text-muted-foreground",
69 Self::Primary => "text-primary",
70 Self::Success => "text-success",
71 Self::Warning => "text-warning",
72 Self::Danger => "text-destructive",
73 Self::Info => "text-info",
74 }
75 }
76}
77
78#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
80pub enum Alignment {
81 #[default]
83 Start,
84 Center,
86 End,
88}
89
90impl Alignment {
91 pub(crate) const fn class(self) -> &'static str {
93 match self {
94 Self::Start => "",
95 Self::Center => "text-center",
96 Self::End => "text-end",
97 }
98 }
99}
100
101#[derive(Clone, Copy, Debug, PartialEq)]
103pub enum Number {
104 Int(i128),
106 Float(f64),
108}
109
110pub trait Numeric {
114 fn number(&self) -> Option<Number>;
116}
117
118impl<T: Numeric + ?Sized> Numeric for &T {
119 fn number(&self) -> Option<Number> {
120 (**self).number()
121 }
122}
123
124macro_rules! numeric_ints {
125 ($($ty:ty),+) => {
126 $(
127 impl Numeric for $ty {
128 fn number(&self) -> Option<Number> {
129 Some(Number::Int(i128::from(*self)))
130 }
131 }
132 )+
133 };
134}
135
136numeric_ints!(i8, i16, i32, i64, u8, u16, u32, u64);
137
138impl Numeric for f32 {
139 fn number(&self) -> Option<Number> {
140 Some(Number::Float(f64::from(*self)))
141 }
142}
143
144impl Numeric for f64 {
145 fn number(&self) -> Option<Number> {
146 Some(Number::Float(*self))
147 }
148}
149
150impl<T: Numeric> Numeric for Option<T> {
151 fn number(&self) -> Option<Number> {
152 self.as_ref().and_then(Numeric::number)
153 }
154}
155
156#[derive(Clone, Copy, Debug, PartialEq, Eq)]
174pub struct Currency {
175 symbol: &'static str,
176 decimals: u8,
177 symbol_after: bool,
178 minor_units: bool,
179}
180
181impl Currency {
182 pub const USD: Self = Self::new("$");
184 pub const EUR: Self = Self::new("€");
186 pub const GBP: Self = Self::new("£");
188 pub const JPY: Self = Self::new("¥").decimals(0);
190
191 pub const fn new(symbol: &'static str) -> Self {
193 Self {
194 symbol,
195 decimals: 2,
196 symbol_after: false,
197 minor_units: false,
198 }
199 }
200
201 #[must_use]
203 pub const fn decimals(mut self, decimals: u8) -> Self {
204 self.decimals = decimals;
205 self
206 }
207
208 #[must_use]
210 pub const fn symbol_after(mut self) -> Self {
211 self.symbol_after = true;
212 self
213 }
214
215 #[must_use]
217 pub const fn minor_units(mut self) -> Self {
218 self.minor_units = true;
219 self
220 }
221
222 pub fn format(&self, value: impl Numeric) -> String {
224 let Some(number) = value.number() else {
225 return String::new();
226 };
227 let spelled = match (self.minor_units, number) {
228 (true, Number::Int(minor)) => format_minor_units(minor, self.decimals),
229 (true, Number::Float(minor)) => format_number(
230 Number::Float(minor / 10f64.powi(self.decimals.into())),
231 self.decimals,
232 ),
233 (false, number) => format_number(number, self.decimals),
234 };
235 let (sign, digits) = match spelled.strip_prefix('-') {
236 Some(digits) => ("-", digits),
237 None => ("", spelled.as_str()),
238 };
239 if self.symbol_after {
240 format!("{sign}{digits} {}", self.symbol)
241 } else {
242 format!("{sign}{}{digits}", self.symbol)
243 }
244 }
245}
246
247fn format_minor_units(minor: i128, decimals: u8) -> String {
249 let (whole, fraction) = match 10i128.checked_pow(decimals.into()) {
251 Some(scale) => (minor.abs() / scale, minor.abs() % scale),
252 None => (0, minor.abs()),
253 };
254 let whole = format_number(Number::Int(whole), 0);
255 let sign = if minor < 0 { "-" } else { "" };
256 if decimals == 0 {
257 return format!("{sign}{whole}");
258 }
259 format!(
260 "{sign}{whole}.{fraction:0width$}",
261 width = usize::from(decimals)
262 )
263}
264
265pub(crate) fn format_number(number: Number, decimals: u8) -> String {
267 let fixed = match number {
268 Number::Int(int) if decimals == 0 => int.to_string(),
269 Number::Int(int) => format!("{int}.{}", "0".repeat(decimals.into())),
270 Number::Float(float) => {
271 let fixed = format!("{float:.*}", usize::from(decimals));
272 if fixed
274 .trim_start_matches('-')
275 .bytes()
276 .all(|b| b == b'0' || b == b'.')
277 {
278 fixed.trim_start_matches('-').to_string()
279 } else {
280 fixed
281 }
282 }
283 };
284 let (sign, unsigned) = match fixed.strip_prefix('-') {
285 Some(rest) => ("-", rest),
286 None => ("", fixed.as_str()),
287 };
288 let (whole, fraction) = match unsigned.split_once('.') {
289 Some((whole, fraction)) => (whole, Some(fraction)),
290 None => (unsigned, None),
291 };
292 let mut grouped = String::with_capacity(whole.len() + whole.len() / 3);
293 for (index, digit) in whole.chars().enumerate() {
294 if index > 0 && (whole.len() - index) % 3 == 0 {
295 grouped.push(',');
296 }
297 grouped.push(digit);
298 }
299 match fraction {
300 Some(fraction) => format!("{sign}{grouped}.{fraction}"),
301 None => format!("{sign}{grouped}"),
302 }
303}
304
305#[cfg(test)]
306mod tests;