1use std::fmt::{self, Write as _};
2
3const MAX_DIGITS: usize = 40;
4
5pub const SHORT_NAMES: &[&str] = &["K", "M", "B", "T"];
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum Style {
9 Plain,
10 Scientific,
11 Engineering,
12 Short,
13}
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Precision {
17 Decimals(u8),
18 Significant(u8),
19 AtLeast(u8),
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum Rounding {
24 HalfAway,
25 HalfEven,
26 Ceil,
27 Truncate,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum NonFinite {
32 Text,
33 OneText,
34 Clamp,
35 Error,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum Suffixes<'a> {
40 Named(&'a [&'a str]),
41 Alpha,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum NumberError {
46 NotANumber,
47 Infinite,
48 Write,
49}
50
51impl fmt::Display for NumberError {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 f.write_str(match self {
54 Self::NotANumber => "not a number",
55 Self::Infinite => "infinite",
56 Self::Write => "the output refused the text",
57 })
58 }
59}
60
61impl std::error::Error for NumberError {}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub struct Format<'a> {
65 pub style: Style,
66 pub from_exp: i32,
67 pub sci_from: Option<i32>,
68 pub precision: Precision,
69 pub big_precision: Option<Precision>,
70 pub rounding: Rounding,
71 pub sci_rounding: Option<Rounding>,
72 pub trim_zeros: bool,
73 pub group_sep: Option<char>,
74 pub group_size: u8,
75 pub decimal_mark: char,
76 pub suffixes: Suffixes<'a>,
77 pub non_finite: NonFinite,
78 pub nan_text: &'a str,
79 pub inf_text: &'a str,
80}
81
82impl Default for Format<'_> {
83 fn default() -> Self {
84 Self {
85 style: Style::Plain,
86 from_exp: 0,
87 sci_from: None,
88 precision: Precision::Decimals(2),
89 big_precision: None,
90 rounding: Rounding::HalfAway,
91 sci_rounding: None,
92 trim_zeros: false,
93 group_sep: Some(','),
94 group_size: 3,
95 decimal_mark: '.',
96 suffixes: Suffixes::Named(SHORT_NAMES),
97 non_finite: NonFinite::Text,
98 nan_text: "NaN",
99 inf_text: "inf",
100 }
101 }
102}
103
104mod sealed {
105 pub enum Parts {
106 Int { negative: bool, magnitude: u128 },
107 F64(f64),
108 F32(f32),
109 }
110
111 pub trait Sealed {
112 fn parts(self) -> Parts;
113 }
114}
115
116use sealed::Parts;
117
118pub trait Number: sealed::Sealed + Copy {}
119
120macro_rules! unsigned {
121 ($($t:ty),*) => {$(
122 impl sealed::Sealed for $t {
123 fn parts(self) -> Parts {
124 Parts::Int { negative: false, magnitude: self as u128 }
125 }
126 }
127 impl Number for $t {}
128 )*};
129}
130
131macro_rules! signed {
132 ($($t:ty),*) => {$(
133 impl sealed::Sealed for $t {
134 fn parts(self) -> Parts {
135 Parts::Int { negative: self < 0, magnitude: (self as i128).unsigned_abs() }
136 }
137 }
138 impl Number for $t {}
139 )*};
140}
141
142unsigned!(u8, u16, u32, u64, u128, usize);
143signed!(i8, i16, i32, i64, i128, isize);
144
145impl sealed::Sealed for f64 {
146 fn parts(self) -> Parts {
147 Parts::F64(self)
148 }
149}
150impl Number for f64 {}
151
152impl sealed::Sealed for f32 {
153 fn parts(self) -> Parts {
154 Parts::F32(self)
155 }
156}
157impl Number for f32 {}
158
159struct Stack<const N: usize> {
160 bytes: [u8; N],
161 len: usize,
162}
163
164impl<const N: usize> Stack<N> {
165 fn new() -> Self {
166 Self {
167 bytes: [0; N],
168 len: 0,
169 }
170 }
171}
172
173impl<const N: usize> fmt::Write for Stack<N> {
174 fn write_str(&mut self, s: &str) -> fmt::Result {
175 let end = self.len + s.len();
176 if end > self.bytes.len() {
177 return Err(fmt::Error);
178 }
179 self.bytes[self.len..end].copy_from_slice(s.as_bytes());
180 self.len = end;
181 Ok(())
182 }
183}
184
185#[derive(Clone, Copy)]
186struct Decimal {
187 negative: bool,
188 digits: [u8; MAX_DIGITS],
189 len: usize,
190 exp: i32,
191}
192
193impl Decimal {
194 fn zero(negative: bool) -> Self {
195 Self {
196 negative,
197 digits: [0; MAX_DIGITS],
198 len: 0,
199 exp: 0,
200 }
201 }
202
203 fn from_int(negative: bool, mut magnitude: u128) -> Self {
204 if magnitude == 0 {
205 return Self::zero(false);
206 }
207 let mut reversed = [0u8; MAX_DIGITS];
208 let mut count = 0;
209 while magnitude > 0 {
210 reversed[count] = (magnitude % 10) as u8;
211 magnitude /= 10;
212 count += 1;
213 }
214 let mut out = Self::zero(negative);
215 for i in 0..count {
216 out.digits[i] = reversed[count - 1 - i];
217 }
218 out.len = count;
219 out.exp = count as i32 - 1;
220 out.strip();
221 out
222 }
223
224 fn from_scientific(negative: bool, text: &[u8]) -> Option<Self> {
225 let mut out = Self::zero(negative);
226 let mut at = 0;
227 while at < text.len() && text[at] != b'e' {
228 if text[at].is_ascii_digit() {
229 if out.len == MAX_DIGITS {
230 return None;
231 }
232 out.digits[out.len] = text[at] - b'0';
233 out.len += 1;
234 }
235 at += 1;
236 }
237 let mut exp: i32 = 0;
238 let mut minus = false;
239 for &b in text.get(at + 1..)? {
240 if b == b'-' {
241 minus = true;
242 } else {
243 exp = exp * 10 + i32::from(b - b'0');
244 }
245 }
246 out.exp = if minus { -exp } else { exp };
247 out.strip();
248 if out.len == 0 {
249 return Some(Self::zero(false));
250 }
251 Some(out)
252 }
253
254 fn strip(&mut self) {
255 while self.len > 0 && self.digits[self.len - 1] == 0 {
256 self.len -= 1;
257 }
258 }
259
260 fn is_zero(&self) -> bool {
261 self.len == 0
262 }
263
264 fn digit_at(&self, index: i32) -> u8 {
265 if index < 0 || index as usize >= self.len {
266 0
267 } else {
268 self.digits[index as usize]
269 }
270 }
271
272 fn round_to(&mut self, keep: i32, rounding: Rounding) {
273 if self.len == 0 || keep >= self.len as i32 {
274 return;
275 }
276 let up = if keep < 0 {
277 rounding == Rounding::Ceil && !self.negative
278 } else {
279 let keep = keep as usize;
280 let next = self.digits[keep];
281 match rounding {
282 Rounding::Truncate => false,
283 Rounding::Ceil => !self.negative,
284 _ if next != 5 => next > 5,
285 _ if self.len > keep + 1 => true,
286 Rounding::HalfEven => keep > 0 && self.digits[keep - 1] % 2 == 1,
287 _ => true,
288 }
289 };
290 if keep < 0 {
291 if up {
292 self.digits[0] = 1;
293 self.len = 1;
294 self.exp = self.exp - keep + 1;
295 } else {
296 *self = Self::zero(false);
297 }
298 return;
299 }
300 let keep = keep as usize;
301 self.len = keep;
302 if up {
303 let mut i = keep;
304 loop {
305 if i == 0 {
306 self.digits[0] = 1;
307 self.len = 1;
308 self.exp += 1;
309 return;
310 }
311 i -= 1;
312 if self.digits[i] == 9 {
313 self.digits[i] = 0;
314 } else {
315 self.digits[i] += 1;
316 break;
317 }
318 }
319 }
320 self.strip();
321 if self.len == 0 {
322 *self = Self::zero(false);
323 }
324 }
325}
326
327fn keep_for(precision: Precision, int_len: i32) -> i32 {
328 match precision {
329 Precision::Decimals(d) => int_len + i32::from(d),
330 Precision::Significant(s) => i32::from(s.max(1)),
331 Precision::AtLeast(s) => i32::from(s.max(1)).max(int_len),
332 }
333}
334
335fn frac_for(precision: Precision, int_digits: i32) -> usize {
336 match precision {
337 Precision::Decimals(d) => usize::from(d),
338 Precision::Significant(s) | Precision::AtLeast(s) => {
339 (i32::from(s.max(1)) - int_digits).max(0) as usize
340 }
341 }
342}
343
344fn suffix_for<'a>(suffixes: Suffixes<'a>, group: i32, buf: &'a mut [u8; 4]) -> Option<&'a str> {
345 if group < 1 {
346 return None;
347 }
348 let index = (group - 1) as usize;
349 match suffixes {
350 Suffixes::Named(list) => list.get(index).copied(),
351 Suffixes::Alpha => {
352 if index < SHORT_NAMES.len() {
353 return Some(SHORT_NAMES[index]);
354 }
355 let mut rest = index - SHORT_NAMES.len();
356 let mut width = 2;
357 let mut block = 26 * 26;
358 while rest >= block {
359 rest -= block;
360 width += 1;
361 block *= 26;
362 if width > buf.len() {
363 return None;
364 }
365 }
366 for slot in (0..width).rev() {
367 buf[slot] = b'a' + (rest % 26) as u8;
368 rest /= 26;
369 }
370 std::str::from_utf8(&buf[..width]).ok()
371 }
372 }
373}
374
375struct Layout {
376 int_digits: i32,
377 frac_len: usize,
378 grouped: bool,
379}
380
381enum Special {
382 Done,
383 Zero,
384 Largest,
385}
386
387impl Format<'_> {
388 pub fn write<N: Number, W: fmt::Write>(
389 &self,
390 out: &mut W,
391 number: N,
392 ) -> Result<(), NumberError> {
393 let decimal = match number.parts() {
394 Parts::Int {
395 negative,
396 magnitude,
397 } => Decimal::from_int(negative, magnitude),
398 Parts::F64(v) => {
399 if v.is_finite() {
400 float_digits(v.is_sign_negative(), &v.abs())
401 } else {
402 match self.special(v.is_nan(), v.is_sign_negative(), out)? {
403 Special::Done => return Ok(()),
404 Special::Zero => Decimal::zero(false),
405 Special::Largest => float_digits(v.is_sign_negative(), &f64::MAX),
406 }
407 }
408 }
409 Parts::F32(v) => {
410 if v.is_finite() {
411 float_digits(v.is_sign_negative(), &v.abs())
412 } else {
413 match self.special(v.is_nan(), v.is_sign_negative(), out)? {
414 Special::Done => return Ok(()),
415 Special::Zero => Decimal::zero(false),
416 Special::Largest => float_digits(v.is_sign_negative(), &f32::MAX),
417 }
418 }
419 }
420 };
421 self.render(out, decimal)
422 }
423
424 pub fn format<N: Number>(&self, number: N) -> Result<String, NumberError> {
425 let mut text = String::new();
426 self.write(&mut text, number)?;
427 Ok(text)
428 }
429
430 fn special<W: fmt::Write>(
431 &self,
432 nan: bool,
433 negative: bool,
434 out: &mut W,
435 ) -> Result<Special, NumberError> {
436 match self.non_finite {
437 NonFinite::Error => Err(if nan {
438 NumberError::NotANumber
439 } else {
440 NumberError::Infinite
441 }),
442 NonFinite::Clamp => Ok(if nan { Special::Zero } else { Special::Largest }),
443 NonFinite::Text | NonFinite::OneText => {
444 let written = if nan || self.non_finite == NonFinite::OneText {
445 out.write_str(self.nan_text)
446 } else {
447 if negative {
448 out.write_char('-').map_err(|_| NumberError::Write)?;
449 }
450 out.write_str(self.inf_text)
451 };
452 written.map_err(|_| NumberError::Write)?;
453 Ok(Special::Done)
454 }
455 }
456 }
457
458 fn render<W: fmt::Write>(&self, out: &mut W, original: Decimal) -> Result<(), NumberError> {
459 self.render_inner(out, original)
460 .map_err(|_| NumberError::Write)
461 }
462
463 fn big_from(&self) -> i32 {
464 match self.style {
465 Style::Short => self.from_exp.max(3),
466 _ => self.from_exp,
467 }
468 }
469
470 fn render_inner<W: fmt::Write>(&self, out: &mut W, original: Decimal) -> fmt::Result {
471 let big = self.big_precision.unwrap_or(self.precision);
472 let mut style = self.style;
473 if original.is_zero() || original.exp < self.big_from() {
474 style = Style::Plain;
475 }
476 loop {
477 if style == Style::Short && self.sci_from.is_some_and(|e| original.exp >= e) {
478 style = Style::Scientific;
479 }
480 let mut dec = original;
481 let layout = match style {
482 Style::Plain => {
483 dec.round_to(keep_for(self.precision, dec.exp + 1), self.rounding);
484 if self.style != Style::Plain && !dec.is_zero() && dec.exp >= self.big_from() {
485 style = self.style;
486 continue;
487 }
488 let int_digits = if dec.is_zero() { 1 } else { dec.exp + 1 };
489 Layout {
490 int_digits,
491 frac_len: frac_for(self.precision, int_digits),
492 grouped: true,
493 }
494 }
495 Style::Scientific => {
496 dec.round_to(keep_for(big, 1), self.sci_rounding.unwrap_or(self.rounding));
497 self.shifted_layout(big, &dec, 1)
498 }
499 Style::Engineering | Style::Short => {
500 let int_len = original.exp - 3 * original.exp.div_euclid(3) + 1;
501 dec.round_to(keep_for(big, int_len), self.rounding);
502 if style == Style::Short && self.sci_from.is_some_and(|e| dec.exp >= e) {
503 style = Style::Scientific;
504 continue;
505 }
506 let int_len = dec.exp - 3 * dec.exp.div_euclid(3) + 1;
507 self.shifted_layout(big, &dec, int_len)
508 }
509 };
510 let mut letters = [0u8; 4];
511 let mut suffix = None;
512 if style == Style::Short {
513 suffix = suffix_for(self.suffixes, dec.exp.div_euclid(3), &mut letters);
514 if suffix.is_none() {
515 style = Style::Scientific;
516 continue;
517 }
518 }
519 if dec.negative {
520 out.write_char('-')?;
521 }
522 self.emit(out, &dec, &layout)?;
523 match style {
524 Style::Scientific => write!(out, "e{}", dec.exp)?,
525 Style::Engineering => write!(out, "e{}", 3 * dec.exp.div_euclid(3))?,
526 Style::Short => {
527 if let Some(s) = suffix {
528 out.write_str(s)?;
529 }
530 }
531 Style::Plain => {}
532 }
533 return Ok(());
534 }
535 }
536
537 fn shifted_layout(&self, precision: Precision, dec: &Decimal, int_digits: i32) -> Layout {
538 Layout {
539 int_digits: if dec.is_zero() { 1 } else { int_digits },
540 frac_len: frac_for(precision, int_digits),
541 grouped: false,
542 }
543 }
544
545 fn emit<W: fmt::Write>(&self, out: &mut W, dec: &Decimal, layout: &Layout) -> fmt::Result {
546 let mut frac_len = layout.frac_len;
547 if self.trim_zeros {
548 let needed = (dec.len as i32 - layout.int_digits).max(0) as usize;
549 frac_len = frac_len.min(needed);
550 }
551 let size = i32::from(self.group_size);
552 if layout.int_digits <= 0 {
553 out.write_char('0')?;
554 } else {
555 for i in 0..layout.int_digits {
556 if layout.grouped
557 && i > 0
558 && size > 0
559 && (layout.int_digits - i) % size == 0
560 && let Some(sep) = self.group_sep
561 {
562 out.write_char(sep)?;
563 }
564 out.write_char((b'0' + dec.digit_at(i)) as char)?;
565 }
566 }
567 if frac_len > 0 {
568 out.write_char(self.decimal_mark)?;
569 for p in 0..frac_len {
570 out.write_char((b'0' + dec.digit_at(layout.int_digits + p as i32)) as char)?;
571 }
572 }
573 Ok(())
574 }
575}
576
577fn float_digits<F: fmt::LowerExp>(negative: bool, magnitude: &F) -> Decimal {
578 let mut stack = Stack::<48>::new();
579 let _ = write!(stack, "{magnitude:e}");
580 Decimal::from_scientific(negative, &stack.bytes[..stack.len]).unwrap_or(Decimal::zero(false))
581}
582
583#[cfg(test)]
584mod tests;