Skip to main content

pfx_core/
numbers.rs

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;