Files
glances-rs/crates/glances-core/src/units.rs
T
glances-rs 975385da5d M1: core engine, config, plugin model and core plugins
- Cargo workspace (core, plugins, tui, cli) with drop-in compatible CLI
- Python-compatible config file handling, limits, views, rates, MMM, history
- process collection via sysinfo + procfs, filters, thresholds, events, actions
- 20 core plugins (cpu, percpu, mem, memswap, load, fs, network, diskio, ...)
- stdout, stdout-csv, stdout-json, --issue, --modules-list output modes
- differential test harness against the Python implementation
2026-10-09 23:25:35 +00:00

246 lines
7.9 KiB
Rust

//! Formatting helpers (units, Python-compatible `str()`/`repr()`).
use serde_json::Value;
/// Convert Celsius to Fahrenheit.
pub fn to_fahrenheit(celsius: f64) -> f64 {
celsius * 1.8 + 32.0
}
/// Make a nice human-readable string out of a number (port of `auto_unit`).
pub fn auto_unit(number: f64, low_precision: bool, min_symbol: &str, none_symbol: &str) -> String {
let symbols: &[char] = &['K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];
let symbols: Vec<char> = match symbols.iter().position(|&s| s.to_string() == min_symbol) {
Some(idx) => symbols[idx..].to_vec(),
None => symbols.to_vec(),
};
let prefix = |symbol: char| -> f64 {
match symbol {
'Y' => 1208925819614629174706176.0,
'Z' => 1180591620717411303424.0,
'E' => 1152921504606846976.0,
'P' => 1125899906842624.0,
'T' => 1099511627776.0,
'G' => 1073741824.0,
'M' => 1048576.0,
'K' => 1024.0,
_ => 1.0,
}
};
if number.is_nan() {
return none_symbol.to_string();
}
if number == 0.0 {
// Avoid 0.0
return "0".to_string();
}
if !number.is_finite() {
return format!("{number}");
}
// If the value was given as an integer, decimal_precision is 0.
let mut decimal_precision = if number.fract() != 0.0 { 2 } else { 0 };
for &symbol in symbols.iter().rev() {
let value = number / prefix(symbol);
if value > 1.0 {
decimal_precision = 0;
if value <= 9.995 {
decimal_precision = 2;
} else if value < 99.95 {
decimal_precision = 1;
}
if low_precision {
if symbol == 'M' || symbol == 'K' {
decimal_precision = 0;
} else {
decimal_precision = decimal_precision.min(1);
}
} else if symbol == 'K' {
decimal_precision = 0;
}
return format!("{value:.decimal_precision$}{symbol}");
}
}
format!("{number:.decimal_precision$}")
}
/// Parse a value like `'12.5 MB'` into a float (port of `string_value_to_float`).
pub fn string_value_to_float(s: &str) -> Option<f64> {
let cleaned: String = s.chars().filter(|c| *c != ' ').collect();
let mut value: Option<f64> = None;
let mut unit: Option<String> = None;
let mut digits = String::new();
let mut rest = String::new();
for c in cleaned.chars() {
if c.is_ascii_digit() || c == '.' {
if rest.is_empty() {
digits.push(c);
continue;
}
}
rest.push(c);
}
if !digits.is_empty() {
value = digits.parse::<f64>().ok();
}
if !rest.is_empty() {
unit = Some(rest.to_uppercase());
}
let value = value?;
let multiplier = match unit.as_deref() {
None => 1.0,
Some("B") => 1.0,
Some("KB") => 1000.0,
Some("MB") => 1_000_000.0,
Some("GB") => 1_000_000_000.0,
Some("TB") => 1_000_000_000_000.0,
Some("PB") => 1_000_000_000_000_000.0,
Some(_) => return None,
};
Some(value * multiplier)
}
/// Python `str()` for a JSON value (used by stdout/csv outputs).
pub fn py_str(value: &Value) -> String {
match value {
Value::Null => "None".to_string(),
Value::Bool(true) => "True".to_string(),
Value::Bool(false) => "False".to_string(),
Value::Number(n) => py_number(n),
Value::String(s) => s.clone(),
other => py_repr(other),
}
}
fn py_number(n: &serde_json::Number) -> String {
if let Some(i) = n.as_i64() {
return i.to_string();
}
if let Some(u) = n.as_u64() {
return u.to_string();
}
let f = n.as_f64().unwrap_or(0.0);
py_float(f)
}
/// Python `repr()` for a float.
pub fn py_float(f: f64) -> String {
if f.is_nan() {
return "nan".to_string();
}
if f.is_infinite() {
return if f > 0.0 { "inf" } else { "-inf" }.to_string();
}
if f.fract() == 0.0 && f.abs() < 1e16 {
format!("{f:.1}")
} else {
format!("{f}")
}
}
/// Python `repr()` for a JSON value (single quotes, True/False/None).
pub fn py_repr(value: &Value) -> String {
match value {
Value::Null => "None".to_string(),
Value::Bool(b) => if *b { "True" } else { "False" }.to_string(),
Value::Number(n) => py_number(n),
Value::String(s) => format!("'{}'", s.replace('\\', "\\\\").replace('\'', "\\'")),
Value::Array(items) => {
let inner: Vec<String> = items.iter().map(py_repr).collect();
format!("[{}]", inner.join(", "))
}
Value::Object(map) => {
let inner: Vec<String> = map
.iter()
.map(|(k, v)| format!("'{}': {}", k.replace('\'', "\\'"), py_repr(v)))
.collect();
format!("{{{}}}", inner.join(", "))
}
}
}
/// Extract a f64 from a JSON value the way Python would accept a number.
pub fn as_f64(value: &Value) -> Option<f64> {
match value {
Value::Number(n) => n.as_f64(),
Value::Bool(_) | Value::Null | Value::String(_) | Value::Array(_) | Value::Object(_) => None,
}
}
/// Simple mean subsampling (port of `subsample`).
pub fn subsample(data: &[f64], sampling: usize) -> Vec<f64> {
if data.len() <= sampling || sampling == 0 {
return data.to_vec();
}
let sampling_length = (data.len() as f64 / sampling as f64).round() as usize;
(0..sampling)
.map(|s| {
let start = s * sampling_length;
let end = ((s + 1) * sampling_length).min(data.len());
let slice = &data[start..end];
slice.iter().sum::<f64>() / slice.len() as f64
})
.collect()
}
/// Return a list of the substrings in the input string, using `sep` as the
/// separator char and `esc` as the escape character (port of `split_esc`,
/// only the explicit separator flavour is required).
pub fn split_esc(input: &str, sep: char, esc: char) -> Vec<String> {
let mut ret: Vec<String> = vec![String::new()];
let mut esc_seen = false;
for ch in input.chars() {
if !esc_seen {
if ch == esc {
esc_seen = true;
} else if ch == sep {
ret.push(String::new());
} else {
ret.last_mut().unwrap().push(ch);
}
} else {
ret.last_mut().unwrap().push(ch);
esc_seen = false;
}
}
ret
}
/// Replace special chars by another (new lines, tabs...) or escape for a
/// shell by adding a `$` prefix in front of them. (Only the replacement
/// flavour is needed by the plugins.)
pub fn replace_special_chars(input: &str, by: char) -> String {
input.replace("\r\n", &by.to_string()).replace('\n', &by.to_string()).replace('\t', &by.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_auto_unit() {
assert_eq!(auto_unit(613421788.0, false, "K", "-"), "585M");
assert_eq!(auto_unit(5307033647.0, false, "K", "-"), "4.94G");
assert_eq!(auto_unit(5307033647.0, true, "K", "-"), "4.9G");
assert_eq!(auto_unit(44968414685.0, false, "K", "-"), "41.9G");
assert_eq!(auto_unit(838471403472.0, false, "K", "-"), "781G");
assert_eq!(auto_unit(9683209690677.0, false, "K", "-"), "8.81T");
assert_eq!(auto_unit(1073741824.0, false, "K", "-"), "1024M");
assert_eq!(auto_unit(1181116006.0, false, "K", "-"), "1.10G");
assert_eq!(auto_unit(0.0, false, "K", "-"), "0");
}
#[test]
fn test_py_repr() {
assert_eq!(py_repr(&json!({"a": 1, "b": "x"})), "{'a': 1, 'b': 'x'}");
assert_eq!(py_str(&json!(1234.0)), "1234.0");
assert_eq!(py_str(&json!(5)), "5");
assert_eq!(py_str(&json!(true)), "True");
assert_eq!(py_str(&json!(null)), "None");
}
}