Files
glances-rs/crates/glances-core/src/units.rs
T
glances-rs ea9debf71b containers: Docker (and best-effort Podman) plugin
- minimal Docker Engine API client over Unix/TCP sockets with version
  negotiation and chunked response decoding
- drop-in stats schema: active/stopped containers, CPU/memory/IO/network
  rates, ports, uptime (pretty_date), image, engine field
- Docker + Podman engines, config (all, podman_sock, max_name_size,
  disable_stats, show/hide, per-container thresholds)
- export strips the computation sub-dicts (--stdout/--stdout-csv)
- TUI block above the process count
- CI-safe fake Docker socket test + live differential parity
2026-10-10 00:24:39 +00:00

355 lines
11 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$}")
}
/// Render a progress bar the way the Python `Bar` class does.
pub fn bar_percent(percent: f64, size: usize, bar_char: &str, display_value: bool) -> String {
bar_custom(percent, size, bar_char, " ", display_value)
}
/// Render a progress bar with custom characters.
pub fn bar_custom(
percent: f64,
size: usize,
bar_char: &str,
empty_char: &str,
display_value: bool,
) -> String {
let size = if display_value {
size.saturating_sub(6)
} else {
size
};
let value = percent.clamp(0.0, 100.0);
let scaled = size as f64 * value / 100.0;
let whole = scaled.floor() as usize;
let frac = scaled - whole as f64;
let mut bar = bar_char.repeat(whole);
let mut whole = whole;
if frac > 0.0 {
// curses_bars = [empty; 5] + [bar; 5]: index 5..7 draws a partial block.
if (frac * 8.0) as usize >= 5 {
bar.push_str(bar_char);
} else {
bar.push_str(empty_char);
}
whole += 1;
}
bar.push_str(&empty_char.repeat(size.saturating_sub(whole)));
if display_value {
if percent >= 100.0 {
bar = format!(
"{bar} {}{:3.0}%",
if percent > 100.0 { ">" } else { " " },
100.0
);
} else {
bar = format!("{bar}{percent:5.1}%");
}
}
bar
}
/// Get a datetime or a timestamp and return a pretty string like
/// 'an hour ago', 'yesterday', '3 months ago', 'just now'...
/// (port of `glances.globals.pretty_date`).
pub fn pretty_date(
started: chrono::DateTime<chrono::Local>,
now: chrono::DateTime<chrono::Local>,
) -> String {
let diff = now.signed_duration_since(started);
let day_diff = diff.num_days();
if day_diff < 0 {
return String::new();
}
// Python's timedelta.seconds excludes the days.
let second_diff = diff.num_seconds() - day_diff * 86_400;
if day_diff == 0 {
let thresholds: [(i64, i64, Option<&str>, Option<&str>); 6] = [
(10, 1, Some("just now"), None),
(60, 1, None, Some(" secs")),
(120, 1, Some("a min"), None),
(3600, 60, None, Some(" mins")),
(7200, 1, Some("an hour"), None),
(86_400, 3600, None, Some(" hours")),
];
for (max_value, divisor, singular, plural) in thresholds {
if second_diff < max_value {
if let Some(singular) = singular
&& plural.is_none()
{
return singular.to_string();
}
let value = second_diff / divisor;
return format!("{value}{}", plural.unwrap_or(""));
}
}
}
let day_thresholds: [(i64, i64, Option<&str>, Option<&str>); 5] = [
(2, 1, Some("yesterday"), None),
(7, 1, Some("a day"), Some(" days")),
(31, 7, Some("a week"), Some(" weeks")),
(365, 30, Some("a month"), Some(" months")),
(i64::MAX, 365, Some("an year"), Some(" years")),
];
for (max_days, divisor, singular, plural) in day_thresholds {
if day_diff < max_days {
let value = day_diff / divisor;
if let Some(singular) = singular
&& (value <= 1 || plural.is_none())
{
return singular.to_string();
}
return format!("{value}{}", plural.unwrap_or(""));
}
}
String::new()
}
/// 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 == '.') && 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");
}
}