Skip to main content

krabka_units/
fmt.rs

1//! Rendering a quantity in the form an operator wrote it.
2//!
3//! `uom`'s own formatting needs the unit named at the call site
4//! (`size.into_format_args(mebibyte, Abbreviation)`). That is the wrong shape for
5//! a log line or a config dump, where the magnitude decides the unit. [`Human`]
6//! picks the unit for you: `536870912` bytes prints as `512MiB`, `604800` seconds
7//! as `7d`, `1536` bytes as `1.5KiB`.
8//!
9//! # Exactness
10//!
11//! Sizes, time extents, and byte rates round-trip. [`crate::parse`] reads back
12//! whatever these types print as the same value. That requirement picks the unit.
13//! [`Human`] uses the largest unit that leaves the magnitude at or above one
14//! *and* representable in at most three decimal places, so `1TiB + 1B` prints as
15//! `1099511627777B` and does not round to `1TiB`. A quantity that is not a whole
16//! number of base units, or is too large for that arithmetic, prints its
17//! base-unit value, which also reads back exactly.
18//!
19//! Fractions and event rates render to nine decimal places and are for display
20//! only. A non-finite quantity prints its raw value and base unit, for diagnostics.
21
22use core::fmt::{self, Display, Formatter};
23
24use num_traits::cast::ToPrimitive;
25
26use crate::{ByteRate, ByteSize, Frequency, Ratio, Time};
27
28/// Descending size units, in bytes.
29const SIZES: &[(i128, &str)] = &[
30    (1_099_511_627_776, "TiB"),
31    (1_073_741_824, "GiB"),
32    (1_048_576, "MiB"),
33    (1024, "KiB"),
34    (1, "B"),
35];
36
37/// Descending time units, in nanoseconds.
38const TIMES: &[(i128, &str)] = &[
39    (86_400_000_000_000, "d"),
40    (3_600_000_000_000, "h"),
41    (60_000_000_000, "m"),
42    (1_000_000_000, "s"),
43    (1_000_000, "ms"),
44    (1_000, "µs"),
45    (1, "ns"),
46];
47
48/// Nanoseconds per second, which is the sub-unit [`TIMES`] counts in.
49const NANOS_PER_SECOND: f64 = 1e9;
50
51/// One thousandth, the resolution a rendered magnitude may use.
52const MILLI: i128 = 1_000;
53
54/// How far a scaled magnitude may sit from a whole sub-unit and still be treated
55/// as that sub-unit count.
56///
57/// The base unit is `f64`, and 250 nanoseconds has no exact representation in
58/// seconds. Scaling it back up lands a few parts in 10^16 away from `250`. A
59/// tolerance a few orders of magnitude above that accepts every quantity built
60/// from whole sub-units and still rejects a truly fractional one. Half a byte is
61/// not this close to zero or one.
62const INTEGRAL_TOLERANCE: f64 = 1e-9;
63
64/// Writes `value`, a magnitude in `base` units, in the largest unit of `units`
65/// that it fits exactly.
66///
67/// `subunits_per_base` scales `value` into the integer sub-unit that the `units`
68/// table counts in: bytes for sizes, and nanoseconds for extents.
69fn write_scaled(
70    f: &mut Formatter<'_>,
71    value: f64,
72    subunits_per_base: f64,
73    units: &[(i128, &str)],
74    base: &str,
75    suffix: &str,
76) -> fmt::Result {
77    if value.is_finite() {
78        let subunits = value * subunits_per_base;
79        let rounded = subunits.round();
80        let drift = (subunits - rounded).abs();
81        // The thousandths multiply inside `write_units` has to fit: `parse` will
82        // accept a magnitude near `i128::MAX / 1000` (`10^36B`), which has no
83        // thousandths representation. Such a value falls through to the raw
84        // base-unit rendering below rather than overflowing.
85        if drift <= subunits.abs().max(1.0) * INTEGRAL_TOLERANCE
86            && let Some(count) = rounded.to_i128()
87            && count.checked_mul(MILLI).is_some()
88        {
89            return write_units(f, count, units, base, suffix);
90        }
91    }
92    write!(f, "{value}{base}{suffix}")
93}
94
95/// Writes an exact sub-unit `count` in the largest unit that divides it to at
96/// most three decimal places.
97fn write_units(
98    f: &mut Formatter<'_>,
99    count: i128,
100    units: &[(i128, &str)],
101    base: &str,
102    suffix: &str,
103) -> fmt::Result {
104    if count == 0 {
105        return write!(f, "0{base}{suffix}");
106    }
107    let scaled = count * MILLI;
108    let picked = units
109        .iter()
110        .copied()
111        .find(|&(scale, _)| count.abs() >= scale && scaled % scale == 0);
112    let (thousandths, unit) = picked.map_or((scaled, base), |(scale, unit)| (scaled / scale, unit));
113    write_thousandths(f, thousandths)?;
114    write!(f, "{unit}{suffix}")
115}
116
117/// Writes `thousandths` / 1000. This drops a zero fraction and trailing zeros.
118fn write_thousandths(f: &mut Formatter<'_>, thousandths: i128) -> fmt::Result {
119    if thousandths < 0 {
120        f.write_str("-")?;
121    }
122    let magnitude = thousandths.unsigned_abs();
123    write!(f, "{}", magnitude / 1_000)?;
124    let fraction = magnitude % 1_000;
125    if fraction != 0 {
126        let rendered = format!("{fraction:03}");
127        write!(f, ".{}", rendered.trim_end_matches('0'))?;
128    }
129    Ok(())
130}
131
132/// Writes `value` with at most nine decimal places and no trailing zeros.
133fn write_trimmed(f: &mut Formatter<'_>, value: f64) -> fmt::Result {
134    let rendered = format!("{value:.9}");
135    let trimmed = rendered.trim_end_matches('0').trim_end_matches('.');
136    f.write_str(trimmed)
137}
138
139/// A quantity that renders itself the way an operator would write it.
140///
141/// ```
142/// use krabka_units::{fmt::Human, prelude::*};
143///
144/// assert_eq!(mebibytes(512).human().to_string(), "512MiB");
145/// assert_eq!(days(7).human().to_string(), "7d");
146/// assert_eq!(mebibytes_per_sec(10).human().to_string(), "10MiB/s");
147/// ```
148pub trait Human: Sized {
149    /// The [`Display`] adapter this quantity renders through.
150    type Rendered: Display;
151
152    /// This quantity, ready to print.
153    fn human(self) -> Self::Rendered;
154}
155
156/// [`ByteSize`] rendered in binary size units.
157#[derive(Debug, Clone, Copy)]
158pub struct HumanByteSize(ByteSize);
159
160impl Display for HumanByteSize {
161    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
162        write_scaled(f, self.0.value, 1.0, SIZES, "B", "")
163    }
164}
165
166impl Human for ByteSize {
167    type Rendered = HumanByteSize;
168
169    fn human(self) -> Self::Rendered {
170        HumanByteSize(self)
171    }
172}
173
174/// [`Time`] rendered in time units, from days down to nanoseconds.
175#[derive(Debug, Clone, Copy)]
176pub struct HumanTime(Time);
177
178impl Display for HumanTime {
179    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
180        write_scaled(f, self.0.value, NANOS_PER_SECOND, TIMES, "s", "")
181    }
182}
183
184impl Human for Time {
185    type Rendered = HumanTime;
186
187    fn human(self) -> Self::Rendered {
188        HumanTime(self)
189    }
190}
191
192/// [`ByteRate`] rendered as a binary size over a second.
193#[derive(Debug, Clone, Copy)]
194pub struct HumanByteRate(ByteRate);
195
196impl Display for HumanByteRate {
197    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
198        write_scaled(f, self.0.value, 1.0, SIZES, "B", "/s")
199    }
200}
201
202impl Human for ByteRate {
203    type Rendered = HumanByteRate;
204
205    fn human(self) -> Self::Rendered {
206        HumanByteRate(self)
207    }
208}
209
210/// [`Frequency`] rendered as events per second.
211#[derive(Debug, Clone, Copy)]
212pub struct HumanFrequency(Frequency);
213
214impl Display for HumanFrequency {
215    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
216        if !self.0.value.is_finite() {
217            return write!(f, "{}/s", self.0.value);
218        }
219        write_trimmed(f, self.0.value)?;
220        f.write_str("/s")
221    }
222}
223
224impl Human for Frequency {
225    type Rendered = HumanFrequency;
226
227    fn human(self) -> Self::Rendered {
228        HumanFrequency(self)
229    }
230}
231
232/// [`Ratio`] rendered as a percentage.
233#[derive(Debug, Clone, Copy)]
234pub struct HumanRatio(Ratio);
235
236impl Display for HumanRatio {
237    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
238        if !self.0.value.is_finite() {
239            return write!(f, "{}%", self.0.value);
240        }
241        write_trimmed(f, self.0.value * 100.0)?;
242        f.write_str("%")
243    }
244}
245
246impl Human for Ratio {
247    type Rendered = HumanRatio;
248
249    fn human(self) -> Self::Rendered {
250        HumanRatio(self)
251    }
252}