//! 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 = 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, now: chrono::DateTime, ) -> 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 { let cleaned: String = s.chars().filter(|c| *c != ' ').collect(); let mut value: Option = None; let mut unit: Option = 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::().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 = items.iter().map(py_repr).collect(); format!("[{}]", inner.join(", ")) } Value::Object(map) => { let inner: Vec = 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 { 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 { 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::() / 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 { let mut ret: Vec = 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"); } }