si_scale/lib.rs
1#![warn(missing_docs)]
2#![allow(clippy::needless_doctest_main)]
3
4//! Format value with units according to SI ([système international d'unités](https://en.wikipedia.org/wiki/International_System_of_Units)).
5//!
6//! Version requirement: _rustc 1.78+_
7//!
8//! ```toml
9//! [dependencies]
10//! si-scale = "0.2"
11//! ```
12//!
13//! ## Overview
14//!
15//! This crate formats numbers using the
16//! [SI Scales](https://en.wikipedia.org/wiki/International_System_of_Units):
17//! from 1 y (yocto, i.e. 1e-24) to 1 Y (Yotta, i.e. 1e24).
18//!
19//! It has the same purpose as the great
20//! [human-repr](https://docs.rs/human-repr), but strikes a different balance:
21//!
22//! - this crate yields more terse code at the call sites
23//! - it gives you more control over the output. As shown later in this page,
24//! you can extend it pretty easily to handle throughput, etc. (seriously, see
25//! below)
26//! - but it only operates on numbers, so it does not prevent you from using a
27//! function to print meters on a duration value (which human-repr does
28//! brilliantly).
29//!
30//! ## Getting started
31//!
32//! To use this crate, either use one of the few pre-defined helper functions,
33//! or build your own.
34//!
35//! Basic example:
36//!
37//! ```rust
38//! use si_scale::helpers::{seconds, seconds3};
39//!
40//! let actual = format!("{}", seconds(1.3e-5));
41//! let expected = "13 µs";
42//! assert_eq!(actual, expected);
43//!
44//! let actual = format!("{}", seconds3(1.3e-5));
45//! let expected = "13.000 µs";
46//! assert_eq!(actual, expected);
47//! ```
48//!
49//! ## Features
50//!
51//! - **`lossy-conversions`**: enables support for `u64`, `i64`, `usize`, and
52//! `isize`. These conversions may lose precision for values > 2^53.
53//!
54//! ```toml
55//! [dependencies]
56//! si-scale = { version = "0.2", features = ["lossy-conversions"] }
57//! ```
58//!
59//! ## Pre-defined helper functions
60//!
61//! The helper functions use the following naming convention:
62//!
63//! - the name indicates the units to use
64//! - a number suffix indicates the decimal digits for floating points
65//! - a `_` suffix indicates the digits use "thousands grouping"
66//!
67//! But that's up to you to depart from that when writing your own functions.
68//!
69//! Currently the helper functions are:
70//!
71//! | helper fn | input | output |
72//! | --- | --- | --- |
73//! | `number_()` | `1.234567`, `1515` | `1.234_567`, `1_515` |
74//! | --- | --- | --- |
75//! | `seconds()` | `1.234567e-6`, `16e-3` | `1.234567 µs`, `16 ms` |
76//! | `seconds3()` | `1.234567e-6`, `16e-3` | `1.235 µs`, `16.000 ms`|
77//! | --- | --- | --- |
78//! | `bytes()` | `1234567` | `1.234567 MB` |
79//! | `bytes_()` | `1234567` | `1_234_567 B` |
80//! | `bytes1()` | `2.3 * 1e12` | `2.3 TB` |
81//! | `bytes2()` | `2.3 * 1e12` | `2.30 TB` |
82//! | --- | --- | --- |
83//! | `bibytes()` | `1024 * 1024 * 1.25` | `1.25 MiB` |
84//! | `bibytes1()` | `1024 * 1024 * 1.25` | `1.3 MiB` |
85//! | `bibytes2()` | `1024 * 1024 * 1.25` | `1.25 MiB` |
86//!
87//! ## Custom helper functions - BYOU (bring your own unit)
88//!
89//! To define your own format function, use the
90//! [`scale_fn!()`](`crate::scale_fn!()`) macro. All pre-defined helper
91//! functions from this crate are defined using this macro.
92//!
93//! | helper fn | mantissa | prefix constraint | base | groupings | input | output |
94//! | --- | -- | --- | --- | --- | --- | --- |
95//! | `number_()` | `"{}"` | `UnitOnly` | B1000 | `_` | `1.234567`, `1515` | `1.234_567`, `1_515` |
96//! | --- | -- | --- | --- | --- | --- | --- |
97//! | `seconds()` | `"{}"` | `UnitAndBelow` | B1000 | none | `1.234567e-6`, `16e-3` | `1.234567 µs`, `16 ms` |
98//! | `seconds3()` | `"{:.3}"` | `UnitAndBelow` | B1000 | none | `1.234567e-6`, `16e-3` | `1.235 µs`, `16.000 ms`|
99//! | --- | -- | --- | --- | --- | --- | --- |
100//! | `bytes()` | `"{}"` | `UnitAndAbove` | B1000 | none | `1234567` | `1.234567 MB` |
101//! | `bytes_()` | `"{}"` | `UnitOnly` | B1000 | `_` | `1234567` | `1_234_567 B` |
102//! | `bytes1()` | `"{:.1}"` | `UnitAndAbove` | B1000 | none | `2.3 * 1e12` | `2.3 TB` |
103//! | `bytes2()` | `"{:.2}"` | `UnitAndAbove` | B1000 | none | `2.3 * 1e12` | `2.30 TB` |
104//! | --- | -- | --- | --- | --- | --- | --- |
105//! | `bibytes()` | `"{}"` | `UnitAndAbove` | B1024 | none | `1024 * 1024 * 1.25` | `1.25 MiB` |
106//! | `bibytes1()` | `"{:.1}"` | `UnitAndAbove` | B1024 | none | `1024 * 1024 * 1.25` | `1.3 MiB` |
107//! | `bibytes2()` | `"{:.2}"` | `UnitAndAbove` | B1024 | none | `1024 * 1024 * 1.25` | `1.25 MiB` |
108//!
109//! The additional table columns show the underlying controls.
110//!
111//! ### The "mantissa" column
112//!
113//! It is a format string which only acts on the mantissa after scaling. For
114//! instance, `"{}"` will display the value with all its digits or no digits if
115//! it is round, and `"{:.1}"` for instance will always display one decimal.
116//!
117//! ### The "prefix constraint" column
118//!
119//! In a nutshell, this allows values to be represented in unsurprising scales:
120//! for instance, you would never write `1.2 ksec`, but always `1200 sec` or
121//! `1.2e3 sec`. In the same vein, you would never write `2 mB`, but always
122//! `0.002 B` or `2e-3 B`.
123//!
124//! So, here the term "unit" refers to the unit scale (`1`), and has nothing to
125//! do with units of measurements. It constrains the possible scales for a
126//! value:
127//!
128//! - `UnitOnly` means the provided value won't be scaled: if you provide a
129//! value larger than 1000, say 1234, it will be printed as 1234.
130//! - `UnitAndAbove` means the provided value can only use higher scales, for
131//! instance `16 GB` but never `4.3 µB`.
132//! - `UnitAndBelow` means the provided value can only use lower scales, for
133//! instance `1.3 µsec` but not `16 Gsec`.
134//!
135//! ### The "base" column
136//!
137//! Base B1000 means 1k = 1000, the base B1024 means 1k = 1024. This is defined
138//! in an [IEC document](https://www.iec.ch/prefixes-binary-multiples). If you
139//! set the base to `B1024`, the mantissa will be scaled appropriately, but in
140//! most cases, you will be using `B1000`.
141//!
142//! ### The "groupings" column
143//!
144//! Groupings refer to "thousands groupings"; the provided char will be
145//! used (for instance 1234 is displayed as 1\_234), if none, the value is
146//! displayed 1234.
147//!
148//! ### Example - how to define a helper for kibits/s
149//!
150//! For instance, let's define a formatting function for bits per sec which
151//! prints the mantissa with 2 decimals, and also uses base 1024 (where 1 ki =
152//! 1024). Note that although we define the function in a separate module,
153//! this is not a requirement.
154//!
155//! ```rust
156//! mod unit_fmt {
157//! use si_scale::scale_fn;
158//! use si_scale::prelude::Value;
159//!
160//! // defines the `bits_per_sec()` function
161//! scale_fn!(bits_per_sec,
162//! base: B1024,
163//! constraint: UnitAndAbove,
164//! mantissa_fmt: "{:.2}",
165//! groupings: '_',
166//! unit: "bit/s",
167//! doc: "Return a string with the value and its si-scaled unit of bit/s.");
168//! }
169//!
170//! use unit_fmt::bits_per_sec;
171//!
172//! fn main() {
173//! let x = 2.1 * 1024 as f32;
174//! let actual = format!("throughput: {:>15}", bits_per_sec(x));
175//! let expected = "throughput: 2.10 kibit/s";
176//! assert_eq!(actual, expected);
177//!
178//! let x = 2;
179//! let actual = format!("throughput: {}", bits_per_sec(x));
180//! let expected = "throughput: 2.00 bit/s";
181//! assert_eq!(actual, expected);
182//! }
183//!
184//! ```
185//!
186//! You can omit the `groupings` argument of the macro to not separate
187//! thousands.
188//!
189//! ## SI Scales - Developer doc
190//!
191//! With base = 1000, 1k = 1000, 1M = 1\_000\_000, 1m = 0.001, 1µ = 0.000\_001,
192//! etc.
193//!
194//! | min (incl.) | max (excl.) | magnitude | prefix |
195//! | --- | --- | --- | ---- |
196//! | .. | .. | -24 | `Prefix::Yocto` |
197//! | .. | .. | -21 | `Prefix::Zepto` |
198//! | .. | .. | -18 | `Prefix::Atto` |
199//! | .. | .. | -15 | `Prefix::Femto` |
200//! | .. | .. | -12 | `Prefix::Pico` |
201//! | .. | .. | -9 | `Prefix::Nano` |
202//! | 0.000\_001 | 0.001 | -6 | `Prefix::Micro` |
203//! | 0.001 | 1 | -3 | `Prefix::Milli` |
204//! | 1 | 1_000 | 0 | `Prefix::Unit` |
205//! | 1000 | 1\_000\_000 | 3 | `Prefix::Kilo` |
206//! | 1\_000\_000 | 1\_000\_000\_000 | 6 | `Prefix::Mega` |
207//! | .. | .. | 9 | `Prefix::Giga` |
208//! | .. | .. | 12 | `Prefix::Tera` |
209//! | .. | .. | 15 | `Prefix::Peta` |
210//! | .. | .. | 18 | `Prefix::Exa` |
211//! | .. | .. | 21 | `Prefix::Zetta` |
212//! | .. | .. | 24 | `Prefix::Yotta` |
213//!
214//! The base is usually 1000, but can also be 1024 (bibytes).
215//!
216//! With base = 1024, 1ki = 1024, 1Mi = 1024 * 1024, etc.
217//!
218//! ### API overview
219//!
220//! The central representation is the [`Value`](`crate::value::Value`) type,
221//! which holds
222//!
223//! - the mantissa,
224//! - the SI unit prefix (such as "kilo", "Mega", etc),
225//! - and the base which represents the cases where "1 k" means 1000 (most
226//! common) and the cases where "1 k" means 1024 (for kiB, MiB, etc).
227//!
228//! This crate provides 2 APIs: a low-level API, and a high-level API for
229//! convenience.
230//!
231//! For the low-level API, the typical use case is
232//!
233//! - first parse a number into a [`Value`](`crate::value::Value`). For doing
234//! this, you have to specify the base, and maybe some constraint on the SI
235//! scales. See [`Value::new()`](`crate::value::Value::new()`) and
236//! [`Value::new_with()`](`crate::value::Value::new_with()`)
237//!
238//! - then display the `Value` either by yourself formatting the mantissa
239//! and prefix (implements the `fmt::Display` trait), or using the provided
240//! Formatter.
241//!
242//! For the high-level API, the typical use cases are
243//!
244//! 1. parse and display a number using the provided functions such as
245//! `bibytes()`, `bytes()` or `seconds()`, they will choose for each number
246//! the most appropriate SI scale.
247//!
248//! 2. In case you want the same control granularity as the low-level API
249//! (e.g. constraining the scale in some way, using some base, specific
250//! mantissa formatting), then you can build a custom function using the
251//! provided macro `scale_fn!()`. The existing functions such as
252//! `bibytes()`, `bytes()`, `seconds()` are all built using this same
253//! macro.
254//!
255//! ### The high-level API
256//!
257//! The `seconds3()` function parses a number into a `Value` and displays it
258//! using 3 decimals and the appropriate scale for seconds (`UnitAndBelow`),
259//! so that non-sensical scales such as kilo-seconds can't be output. The
260//! `seconds()` function does the same but formats the mantissa with the
261//! default `"{}"`, so no decimals are printed for integer mantissa.
262//!
263//! ```
264//! use si_scale::helpers::{seconds, seconds3};
265//!
266//! let actual = format!("result is {:>15}", seconds(1234.5678));
267//! let expected = "result is 1234.5678 s";
268//! assert_eq!(actual, expected);
269//!
270//! let actual = format!("result is {:>10}", seconds3(12.3e-7));
271//! let expected = "result is 1.230 µs";
272//! assert_eq!(actual, expected);
273//! ```
274//!
275//! The `bytes()` function parses a number into a `Value` _using base 1000_
276//! and displays it using 1 decimal and the appropriate scale for bytes
277//! (`UnitAndAbove`), so that non-sensical scales such as milli-bytes may not
278//! appear.
279//!
280//! ```
281//! use si_scale::helpers::{bytes, bytes1};
282//!
283//! let actual = format!("result is {}", bytes1(12_345_678));
284//! let expected = "result is 12.3 MB";
285//! assert_eq!(actual, expected);
286//!
287//! let actual = format!("result is {:>10}", bytes(16));
288//! let expected = "result is 16 B";
289//! assert_eq!(actual, expected);
290//!
291//! let actual = format!("result is {}", bytes(0.12));
292//! let expected = "result is 0.12 B";
293//! assert_eq!(actual, expected);
294//! ```
295//!
296//! The `bibytes1()` function parses a number into a `Value` _using base 1024_
297//! and displays it using 1 decimal and the appropriate scale for bytes
298//! (`UnitAndAbove`), so that non-sensical scales such as milli-bytes may not
299//! appear.
300//!
301//! ```
302//! use si_scale::helpers::{bibytes, bibytes1};
303//!
304//! let actual = format!("result is {}", bibytes1(12_345_678));
305//! let expected = "result is 11.8 MiB";
306//! assert_eq!(actual, expected);
307//!
308//! let actual = format!("result is {}", bibytes(16 * 1024));
309//! let expected = "result is 16 kiB";
310//! assert_eq!(actual, expected);
311//!
312//! let actual = format!("result is {:>10}", bibytes1(16));
313//! let expected = "result is 16.0 B";
314//! assert_eq!(actual, expected);
315//!
316//! let actual = format!("result is {}", bibytes(0.12));
317//! let expected = "result is 0.12 B";
318//! assert_eq!(actual, expected);
319//! ```
320//!
321//! ### The low-level API
322//!
323//! #### Creating a `Value` with `Value::new()`
324//!
325//! The low-level function [`Value::new()`](`crate::value::Value::new()`)
326//! converts any number convertible to f64 into a `Value` using base 1000. The
327//! `Value` struct implements `From` for common numbers and delegates to
328//! `Value::new()`, so they are equivalent in practice. Here are a few
329//! examples.
330//!
331//! ```rust
332//! use std::convert::From;
333//! use si_scale::prelude::*;
334//!
335//! let actual = Value::from(0.123);
336//! let expected = Value {
337//! mantissa: 123f64,
338//! prefix: Prefix::Milli,
339//! base: Base::B1000,
340//! };
341//! assert_eq!(actual, expected);
342//! assert_eq!(Value::new(0.123), expected);
343//!
344//! let actual: Value = 0.123.into();
345//! assert_eq!(actual, expected);
346//!
347//! let actual: Value = 1300i32.into();
348//! let expected = Value {
349//! mantissa: 1.3f64,
350//! prefix: Prefix::Kilo,
351//! base: Base::B1000,
352//! };
353//! assert_eq!(actual, expected);
354//!
355//! let actual: Vec<Value> = vec![0.123f64, -1.5e28]
356//! .iter().map(|n| n.into()).collect();
357//! let expected = vec![
358//! Value {
359//! mantissa: 123f64,
360//! prefix: Prefix::Milli,
361//! base: Base::B1000,
362//! },
363//! Value {
364//! mantissa: -1.5e4f64,
365//! prefix: Prefix::Yotta,
366//! base: Base::B1000,
367//! },
368//! ];
369//! assert_eq!(actual, expected);
370//! ```
371//!
372//! As you can see in the last example, values which scale are outside of the
373//! SI prefixes are represented using the closest SI prefix.
374//!
375//! #### Creating a `Value` with `Value::new_with()`
376//!
377//! The low-level [`Value::new_with()`](`crate::value::Value::new_with()`)
378//! operates similarly to [`Value::new()`](`crate::value::Value::new()`) but
379//! also expects a base and a constraint on the scales you want to use. In
380//! comparison with the simple `Value::new()`, this allows base 1024 scaling
381//! (for kiB, MiB, etc) and preventing upper scales for seconds or lower
382//! scales for integral units such as bytes (e.g. avoid writing 1300 sec as
383//! 1.3 ks or 0.415 B as 415 mB).
384//!
385//! ```rust
386//! use si_scale::prelude::*;
387//!
388//! // Assume this is seconds, no kilo-seconds make sense.
389//! let actual = Value::new_with(1234, Base::B1000, Constraint::UnitAndBelow);
390//! let expected = Value {
391//! mantissa: 1234f64,
392//! prefix: Prefix::Unit,
393//! base: Base::B1000,
394//! };
395//! assert_eq!(actual, expected);
396//! ```
397//!
398//! Don't worry yet about the verbosity, the following parser helps with this.
399//!
400//! #### Formatting values
401//!
402//! In this example, the number `x` is converted into a value and displayed
403//! using the most appropriate SI prefix. The user chose to constrain the
404//! prefix to be anything lower than `Unit` (1) because kilo-seconds make
405//! no sense.
406//!
407//! ```
408//! use si_scale::format_value;
409//! # fn main() {
410//! use si_scale::{value::Value, base::Base, prefix::Constraint};
411//!
412//! let x = 1234.5678;
413//! let v = Value::new_with(x, Base::B1000, Constraint::UnitAndBelow);
414//! let unit = "s";
415//!
416//! let actual = format!(
417//! "result is {}{u}",
418//! format_value!(v, "{:.5}", groupings: '_'),
419//! u = unit
420//! );
421//! let expected = "result is 1_234.567_80 s";
422//! assert_eq!(actual, expected);
423//! # }
424//! ```
425//!
426//! ## Run code-coverage
427//!
428//! Install the llvm-tools-preview component and grcov
429//!
430//! ```sh
431//! rustup component add llvm-tools-preview
432//! cargo install grcov
433//! ```
434//!
435//! Install nightly
436//!
437//! ```sh
438//! rustup toolchain install nightly
439//! ```
440//!
441//! The following make invocation will switch to nigthly run the tests using
442//! Cargo, and output coverage HTML report in `./coverage/`
443//!
444//! ```sh
445//! make coverage
446//! ```
447//!
448//! The coverage report is located in `./coverage/index.html`
449//!
450//! ## License
451//!
452//! Licensed under either of
453//!
454//! - [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0)
455//! - [MIT license](http://opensource.org/licenses/MIT)
456//!
457//! at your option.
458//!
459//! ### Contribution
460//!
461//! Unless you explicitly state otherwise, any contribution intentionally submitted
462//! for inclusion in the work by you, as defined in the Apache-2.0 license, shall
463//! be dual licensed as above, without any additional terms or conditions.
464
465/// Error type used by this crate.
466#[derive(Debug, PartialEq, Eq)]
467pub enum SIUnitsError {
468 /// Indicates an error occurred when parsing the exponent.
469 ExponentParsing(String),
470}
471
472/// Result type used by this crate.
473pub type Result<T> = std::result::Result<T, SIUnitsError>;
474
475pub mod base;
476pub mod format;
477pub mod helpers;
478pub mod prefix;
479pub mod value;
480
481/// Holds first-class citizens of this crate, for convenience.
482pub mod prelude {
483 pub use crate::base::Base;
484 pub use crate::prefix::{Constraint, Prefix};
485 pub use crate::value::{IntoF64, Value};
486}