human_format_next/
lib.rs

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