human_format_next/
lib.rs

1//! Human-format-next
2
3#![cfg_attr(not(feature = "std"), no_std)]
4
5use core::fmt;
6
7#[derive(Debug, Clone, Copy)]
8/// Entry point to the lib. Use this to handle your formatting needs.
9///
10/// - `BASE`: the base
11/// - `DECIMALS`: target decimal places (if not keeping the original number)
12pub struct Formatter<const BASE: usize = 0, const DECIMALS: usize = 2> {
13    /// Separator between numbers and units.
14    ///
15    /// Defaults to be " " (space)
16    separator: &'static str,
17
18    /// The abbreviated number's units.
19    ///
20    /// If the number is too large and no corresponding unit is found, the
21    /// scientific notation like `3.0e99` will be used.
22    units: &'static [&'static str],
23
24    /// The custom unit attached after the abbreviated number's unit.
25    custom_unit: Option<&'static str>,
26}
27
28impl Formatter {
29    /// SI units (western format).
30    pub const SI: Formatter<1000, 2> = Formatter::new(&["K", "M", "G", "T", "P", "E", "Z", "Y"]);
31
32    /// Binary units (western format).
33    pub const BINARY: Formatter<1024, 2> =
34        Formatter::new(&["Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi", "Yi"]);
35
36    /// Chinese units.
37    pub const CHINESE: Formatter<10000, 2> =
38        Formatter::new(&["万", "亿", "兆", "京", "垓", "秭", "穰", "沟"]);
39}
40
41impl<const BASE: usize, const DECIMALS: usize> Formatter<BASE, DECIMALS> {
42    #[inline]
43    /// Create a new [`Formatter`] with given `BASE`, `DECIMALS` and units.
44    pub const fn new(units: &'static [&'static str]) -> Self {
45        Self {
46            separator: " ",
47            units,
48            custom_unit: None,
49        }
50    }
51
52    #[inline]
53    /// Set the separator between numbers and units.
54    pub const fn with_separator(self, separator: &'static str) -> Self {
55        Self { separator, ..self }
56    }
57
58    #[inline]
59    /// Set scales, including base and the abbreviated number's units.
60    pub const fn with_scales<const N_BASE: usize>(
61        self,
62        units: &'static [&'static str],
63    ) -> Formatter<N_BASE, DECIMALS> {
64        // wait for feature `generic_const_exprs`
65        debug_assert!(BASE > 0, "BASE CANNOT BE 0");
66
67        Formatter {
68            separator: self.separator,
69            units,
70            custom_unit: self.custom_unit,
71        }
72    }
73
74    #[inline]
75    /// Set custom unit attached after the abbreviated number's unit.
76    pub const fn with_custom_unit(self, custom_unit: &'static str) -> Self {
77        Self {
78            custom_unit: Some(custom_unit),
79            ..self
80        }
81    }
82
83    #[inline]
84    /// Set the decimal places to keep.
85    pub const fn with_decimals<const N_DECIMALS: usize>(self) -> Formatter<BASE, N_DECIMALS> {
86        // wait for feature `generic_const_exprs`
87        debug_assert!(
88            N_DECIMALS <= f64::DIGITS as usize,
89            "DECIMALS too large, for RELEASE profile will make use of f64::DIGITS",
90        );
91
92        Formatter {
93            separator: self.separator,
94            units: self.units,
95            custom_unit: self.custom_unit,
96        }
97    }
98
99    #[inline]
100    /// Formats the given `number` into a human-readable string using the
101    /// specified units and separator.
102    ///
103    /// See [`NumberT`] for all types we accept as param.
104    ///
105    /// # Notice
106    ///
107    /// For better performance (may be so), you may need
108    /// [`format_int`](Self::format_int) or [`format_uint`](Self::format_uint).
109    ///
110    /// # Limitation
111    ///
112    /// `f64` can only handle 15 decimal places at most. We may introduce
113    /// `macro_toolset` for large number formatting.
114    pub fn format(&self, number: impl NumberT) -> FormatResult<DECIMALS> {
115        if let Some(integer) = number.integer() {
116            self.format_general(integer, number.fraction())
117                .set_result_is_negative(number.is_negative())
118        } else {
119            #[cfg(feature = "std")]
120            {
121                self.format_float(
122                    number
123                        .fraction()
124                        .expect("must be floating number which is too large"),
125                )
126            }
127
128            #[cfg(not(feature = "std"))]
129            #[allow(unsafe_code)]
130            unsafe {
131                core::hint::unreachable_unchecked()
132            }
133        }
134    }
135
136    #[inline]
137    /// Formats the given `number` into a human-readable string using the
138    /// specified units and separator.
139    ///
140    /// We accept any number that fits into `isize`. For `i128`, see
141    /// [`format_large_int`](Self::format_large_int).
142    pub fn format_int(&self, number: impl Into<i128>) -> FormatResult<DECIMALS> {
143        let number: i128 = number.into();
144
145        self.format_general(number.unsigned_abs(), None)
146            .set_result_is_negative(number.is_negative())
147    }
148
149    #[inline]
150    /// Formats the given `number` into a human-readable string using the
151    /// specified units and separator.
152    pub fn format_uint(&self, number: impl Into<u128>) -> FormatResult<DECIMALS> {
153        self.format_general(number.into(), None)
154    }
155
156    /// Formats the given `number` into a human-readable string using the
157    /// specified units and separator.
158    ///
159    /// # Params
160    ///
161    /// - `integer`: the integer part of the number.
162    ///   - For float, [`f32::trunc`] or [`f64::trunc`] may helps you.
163    /// - `fraction`: the fractional part of the number.
164    ///   - For float, [`f32::fract`] or [`f64::fract`] may helps you.
165    ///   - For integer, leave it `None`.
166    ///
167    /// # Notice
168    ///
169    /// It's NOT recommended that you use this directly, use
170    /// [`format`](Self::format) instead unless you know exactly what you do.
171    pub fn format_general(&self, integer: u128, fraction: Option<f64>) -> FormatResult<DECIMALS> {
172        let base = BASE as u128;
173
174        if integer < base {
175            return FormatType::General {
176                integer,
177                fraction,
178                unit: None,
179            }
180            .formatter_result(self);
181        }
182
183        let mut index: usize = 0;
184        let mut value = integer;
185
186        while value >= base {
187            value /= base;
188            index += 1;
189        }
190
191        match self.units.get(index - 1) {
192            Some(&unit) => {
193                let leftover = {
194                    let leftover_exp = (base).pow(index as u32);
195                    (integer - value * leftover_exp) as f64 / leftover_exp as f64
196                };
197
198                #[cfg(feature = "std")]
199                {
200                    // fraction may be larger than 1.
201                    let leftover_fraction = fraction.unwrap_or(0.0) + leftover.fract();
202
203                    FormatType::General {
204                        integer: value
205                            + leftover.trunc() as u128
206                            + leftover_fraction.trunc() as u128,
207                        fraction: Some(leftover_fraction.fract()),
208                        unit: Some(unit),
209                    }
210                }
211
212                #[cfg(not(feature = "std"))]
213                {
214                    let mut leftover = leftover;
215
216                    // fraction may be larger than 1.
217                    let mut integer = value;
218                    while leftover >= 1.0 {
219                        leftover -= 1.0;
220                        integer += 1;
221                    }
222
223                    let mut fraction = leftover + fraction.unwrap_or(0.0);
224                    while fraction >= 1.0 {
225                        fraction -= 1.0;
226                        integer += 1;
227                    }
228
229                    FormatType::General {
230                        integer,
231                        fraction: Some(fraction),
232                        unit: Some(unit),
233                    }
234                }
235            }
236            None => {
237                let mut exponent: usize = 0;
238                let mut value = integer;
239                // Safe: have checked DECIMALS <= u32::MAX
240                let target_len = 10usize.pow((DECIMALS as u32).min(f64::DIGITS) + 1);
241
242                loop {
243                    value /= 10;
244                    exponent += 1;
245
246                    if value < target_len as _ {
247                        break;
248                    }
249                }
250
251                // calc the leftover
252                {
253                    let mut value = value;
254                    loop {
255                        value /= 10;
256                        exponent += 1;
257
258                        if value < 10 {
259                            break;
260                        }
261                    }
262                }
263
264                FormatType::Scientific {
265                    coefficient: value as f64 / (target_len / 10) as f64,
266                    exponent,
267                }
268            }
269        }
270        .formatter_result(self)
271    }
272
273    #[cfg(feature = "std")]
274    /// Formats the given `number` into a human-readable string using the
275    /// specified units and separator.
276    ///
277    /// # Notice
278    ///
279    /// It's NOT recommended that you use this directly, floating point
280    /// calculation often slower than integer arithmetic. Use
281    /// [`format`](Self::format) instead unless you know exactly what you do.
282    pub fn format_float(&self, number: f64) -> FormatResult<DECIMALS> {
283        let base = BASE as f64;
284        if number < base {
285            return FormatType::Float { number, unit: None }.formatter_result(self);
286        }
287
288        let mut index: usize = 0;
289        let mut value = number;
290
291        while value >= base {
292            value /= base;
293            index += 1;
294        }
295
296        match self.units.get(index - 1) {
297            Some(&unit) => {
298                let leftover = {
299                    let leftover_exp = base.powi(index as i32);
300                    (number - value * leftover_exp) / leftover_exp
301                };
302
303                FormatType::Float {
304                    number: value + leftover,
305                    unit: Some(unit),
306                }
307            }
308            None => {
309                let value = number.log10();
310
311                FormatType::Scientific {
312                    coefficient: 10.0f64.powf(value.fract()),
313                    exponent: value.trunc() as _,
314                }
315            }
316        }
317        .formatter_result(self)
318    }
319}
320
321#[allow(private_bounds)]
322/// Sealed trait for number that can be formatted, including:
323///
324/// - `u8`
325/// - `u16`
326/// - `u32`
327/// - `u64`
328/// - `u128`
329/// - `i8`
330/// - `i16`
331/// - `i32`
332/// - `i64`
333/// - `i128`
334/// - `f32`
335/// - `f64`
336pub trait NumberT: number_sealed::NumberT {}
337
338impl<T: number_sealed::NumberT> NumberT for T {}
339
340mod number_sealed {
341    pub(super) trait NumberT: Copy {
342        fn is_negative(self) -> bool;
343
344        fn integer(self) -> Option<u128>;
345
346        #[inline]
347        fn fraction(self) -> Option<f64> {
348            None
349        }
350    }
351
352    macro_rules! impl_number_trait {
353        (UINT: $($ty:ident),+) => {
354            $(
355                impl NumberT for $ty {
356                    #[inline]
357                    fn is_negative(self) -> bool {
358                        false
359                    }
360
361                    #[inline]
362                    fn integer(self) -> Option<u128> {
363                        Some(self as _)
364                    }
365                }
366            )+
367        };
368        (INT: $($ty:ident),+) => {
369            $(
370                impl NumberT for $ty {
371                    #[inline]
372                    fn is_negative(self) -> bool {
373                        self < 0
374                    }
375
376                    #[inline]
377                    fn integer(self) -> Option<u128> {
378                        Some(self.unsigned_abs() as _)
379                    }
380                }
381            )+
382        };
383    }
384
385    impl_number_trait!(UINT: u8, u16, u32, u64, usize, u128);
386    impl_number_trait!(INT: i8, i16, i32, i64, isize, i128);
387
388    #[cfg(feature = "std")]
389    impl NumberT for f32 {
390        #[inline]
391        fn is_negative(self) -> bool {
392            self < 0.0
393        }
394
395        #[inline]
396        fn integer(self) -> Option<u128> {
397            Some(self.trunc() as _)
398        }
399
400        #[inline]
401        fn fraction(self) -> Option<f64> {
402            Some(self.fract() as _)
403        }
404    }
405
406    #[cfg(feature = "std")]
407    impl NumberT for f64 {
408        #[inline]
409        fn is_negative(self) -> bool {
410            self < 0.0
411        }
412
413        #[inline]
414        fn integer(self) -> Option<u128> {
415            if self < 3.40282366920938e+38 {
416                Some(self.trunc() as _)
417            } else {
418                None
419            }
420        }
421
422        #[inline]
423        fn fraction(self) -> Option<f64> {
424            if self < 3.40282366920938e+38 {
425                Some(self.fract() as _)
426            } else {
427                Some(self as _)
428            }
429        }
430    }
431}
432
433#[derive(Debug, Clone, Copy)]
434/// Format result
435///
436/// This implements [`Display`](fmt::Display) and `to_string` is supported.
437pub struct FormatResult<const DECIMALS: usize> {
438    result: FormatType<DECIMALS>,
439    result_is_negative: bool,
440    separator: &'static str,
441    custom_unit: Option<&'static str>,
442}
443
444impl<const DECIMALS: usize> fmt::Display for FormatResult<DECIMALS> {
445    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
446        if self.result_is_negative {
447            write!(f, "-")?;
448        }
449
450        match self.result {
451            FormatType::General {
452                integer,
453                fraction: _fraction,
454                unit,
455            } => {
456                write!(f, "{integer}")?;
457
458                // Keep 15, f64::DIGITS
459                #[cfg(feature = "std")]
460                {
461                    let full_fraction = _fraction.map(|fraction| format!("{fraction:.15}"));
462                    let fraction = full_fraction
463                        .as_ref()
464                        .map(|full_fraction| {
465                            let digits = (f64::DIGITS as usize).min(DECIMALS);
466                            &full_fraction[1..digits + 2]
467                        })
468                        .unwrap_or_default();
469                    write!(f, "{fraction}")?;
470                };
471
472                if unit.is_some() {
473                    write!(f, "{}{}", self.separator, unit.unwrap())?;
474                }
475
476                if self.custom_unit.is_some() {
477                    if unit.is_none() {
478                        write!(f, "{}", self.separator)?;
479                    }
480
481                    write!(f, "{}", self.custom_unit.unwrap())?;
482                };
483            }
484            #[cfg(feature = "std")]
485            FormatType::Float { number, unit } => {
486                // Keep 15, f64::DIGITS
487                let number = format!("{number:.15}");
488                let digits = (f64::DIGITS as usize).min(DECIMALS);
489                let number = &number[1..digits + 2];
490                write!(f, "{number}")?;
491
492                if unit.is_some() {
493                    write!(f, "{}{}", self.separator, unit.unwrap())?;
494                }
495
496                if self.custom_unit.is_some() {
497                    if unit.is_none() {
498                        write!(f, "{}", self.separator)?;
499                    }
500
501                    write!(f, "{}", self.custom_unit.unwrap())?;
502                };
503            }
504            FormatType::Scientific {
505                coefficient,
506                exponent,
507            } => {
508                #[cfg(not(feature = "std"))]
509                write!(f, "{coefficient}")?;
510
511                #[cfg(feature = "std")]
512                {
513                    // Keep 15, f64::DIGITS
514                    let coefficient = format!("{coefficient:.15}");
515                    let digits = (f64::DIGITS as usize).min(DECIMALS);
516                    let coefficient = &coefficient[..digits + 2];
517                    write!(f, "{coefficient}")?;
518                }
519
520                write!(f, "e{exponent}")?;
521
522                if self.custom_unit.is_some() {
523                    write!(f, "{}{}", self.separator, self.custom_unit.unwrap())?;
524                };
525            }
526        };
527
528        Ok(())
529    }
530}
531
532impl<const DECIMALS: usize> FormatResult<DECIMALS> {
533    #[inline]
534    const fn set_result_is_negative(self, result_is_negative: bool) -> Self {
535        Self {
536            result_is_negative,
537            ..self
538        }
539    }
540}
541
542#[derive(Debug, Clone, Copy)]
543enum FormatType<const DECIMALS: usize> {
544    /// General
545    General {
546        /// The integer part.
547        integer: u128,
548
549        /// The fractional part.
550        fraction: Option<f64>,
551
552        /// The abbreviated number's unit.
553        unit: Option<&'static str>,
554    },
555
556    #[cfg(feature = "std")]
557    /// General
558    Float {
559        /// The integer part.
560        number: f64,
561
562        /// The abbreviated number's unit.
563        unit: Option<&'static str>,
564    },
565
566    /// Scientific notation
567    Scientific {
568        /// The coefficient part, must be within `1.0` ~ `9.99...`
569        coefficient: f64,
570        /// The exponent part, must be a positive integer
571        exponent: usize,
572    },
573}
574
575impl<const DECIMALS: usize> FormatType<DECIMALS> {
576    #[inline]
577    const fn formatter_result<const BASE: usize>(
578        self,
579        formatter: &Formatter<BASE, DECIMALS>,
580    ) -> FormatResult<DECIMALS> {
581        FormatResult {
582            result: self,
583            result_is_negative: false,
584            separator: formatter.separator,
585            custom_unit: formatter.custom_unit,
586        }
587    }
588}