//! 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$}") } /// 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 == '.' { if 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"); } }