- 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
246 lines
7.9 KiB
Rust
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");
|
|
}
|
|
}
|