use epics_base_rs::types::EpicsValue;
pub fn env_default_timeout() -> f64 {
std::env::var("EPICS_CLI_TIMEOUT")
.ok()
.and_then(|s| s.parse::<f64>().ok())
.unwrap_or(1.0)
}
pub const PV_NAME_WIDTH: usize = 30;
#[derive(Debug, Clone, Copy)]
pub enum FloatStyle {
G,
E,
F,
}
#[derive(Debug, Clone, Copy)]
pub struct FloatFormat {
pub style: FloatStyle,
pub precision: u32,
}
impl Default for FloatFormat {
fn default() -> Self {
Self {
style: FloatStyle::G,
precision: 6,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IntStyle {
Dec,
Hex,
Oct,
Bin,
}
#[derive(Debug, Clone)]
pub struct ValueFormat {
pub float: FloatFormat,
pub int_style: IntStyle,
pub float_as_int: bool,
pub enum_as_number: bool,
pub char_array_as_string: bool,
pub max_elements: Option<usize>,
pub field_separator: char,
}
impl Default for ValueFormat {
fn default() -> Self {
Self {
float: FloatFormat::default(),
int_style: IntStyle::Dec,
float_as_int: false,
enum_as_number: false,
char_array_as_string: false,
max_elements: None,
field_separator: ' ',
}
}
}
pub fn format_value(v: &EpicsValue, fmt: &ValueFormat, enum_strings: Option<&[String]>) -> String {
let sep = fmt.field_separator;
match v {
EpicsValue::String(s) => s.clone(),
EpicsValue::Short(n) => format_int_i64(*n as i64, fmt.int_style),
EpicsValue::Long(n) => format_int_i64(*n as i64, fmt.int_style),
EpicsValue::Int64(n) => format_int_i64(*n, fmt.int_style),
EpicsValue::Char(n) => format_int_i64((*n as i8) as i64, fmt.int_style),
EpicsValue::Enum(idx) => format_enum(*idx as i64, fmt, enum_strings),
EpicsValue::Float(x) => format_float(*x as f64, fmt),
EpicsValue::Double(x) => format_float(*x, fmt),
EpicsValue::ShortArray(arr) => {
render_array_int(arr.iter().map(|&n| n as i64), arr.len(), fmt, sep)
}
EpicsValue::LongArray(arr) => {
render_array_int(arr.iter().map(|&n| n as i64), arr.len(), fmt, sep)
}
EpicsValue::Int64Array(arr) => render_array_int(arr.iter().copied(), arr.len(), fmt, sep),
EpicsValue::EnumArray(arr) => {
let mut parts = Vec::with_capacity(arr.len() + 1);
parts.push(arr.len().to_string());
let take = fmt.max_elements.unwrap_or(arr.len()).min(arr.len());
for &idx in &arr[..take] {
parts.push(format_enum(idx as i64, fmt, enum_strings));
}
parts.join(&sep.to_string())
}
EpicsValue::FloatArray(arr) => render_array_iter(
arr.iter().map(|&x| format_float(x as f64, fmt)),
arr.len(),
fmt,
sep,
),
EpicsValue::DoubleArray(arr) => render_array_iter(
arr.iter().map(|&x| format_float(x, fmt)),
arr.len(),
fmt,
sep,
),
EpicsValue::CharArray(arr) => {
if fmt.char_array_as_string {
let end = arr.iter().position(|&b| b == 0).unwrap_or(arr.len());
String::from_utf8_lossy(&arr[..end]).into_owned()
} else {
render_array_int(arr.iter().map(|&b| (b as i8) as i64), arr.len(), fmt, sep)
}
}
EpicsValue::StringArray(arr) => {
let mut parts = Vec::with_capacity(arr.len() + 1);
parts.push(arr.len().to_string());
let take = fmt.max_elements.unwrap_or(arr.len()).min(arr.len());
parts.extend(arr[..take].iter().cloned());
parts.join(&sep.to_string())
}
}
}
fn render_array_int<I: Iterator<Item = i64>>(
iter: I,
total: usize,
fmt: &ValueFormat,
sep: char,
) -> String {
let take = fmt.max_elements.unwrap_or(total).min(total);
let mut parts = Vec::with_capacity(take + 1);
parts.push(total.to_string());
for n in iter.take(take) {
parts.push(format_int_i64(n, fmt.int_style));
}
parts.join(&sep.to_string())
}
fn render_array_iter<I: Iterator<Item = String>>(
iter: I,
total: usize,
fmt: &ValueFormat,
sep: char,
) -> String {
let take = fmt.max_elements.unwrap_or(total).min(total);
let mut parts = Vec::with_capacity(take + 1);
parts.push(total.to_string());
parts.extend(iter.take(take));
parts.join(&sep.to_string())
}
fn format_enum(idx: i64, fmt: &ValueFormat, enum_strings: Option<&[String]>) -> String {
if !fmt.enum_as_number
&& let Some(strs) = enum_strings
&& idx >= 0
&& (idx as usize) < strs.len()
{
return strs[idx as usize].clone();
}
format_int_i64(idx, fmt.int_style)
}
fn format_int_i64(n: i64, style: IntStyle) -> String {
match style {
IntStyle::Dec => n.to_string(),
IntStyle::Hex => format!("0x{:x}", n as u64),
IntStyle::Oct => format!("0o{:o}", n as u64),
IntStyle::Bin => format!("0b{:b}", n as u64),
}
}
fn format_float(x: f64, fmt: &ValueFormat) -> String {
if fmt.float_as_int {
let rounded = if x.is_nan() { 0i64 } else { x.round() as i64 };
return format_int_i64(rounded, fmt.int_style);
}
if !x.is_finite() {
return format!("{x}");
}
let p = fmt.float.precision as usize;
match fmt.float.style {
FloatStyle::F => format!("{x:.p$}"),
FloatStyle::E => format_e(x, p),
FloatStyle::G => format_g(x, p.max(1)),
}
}
fn format_g(x: f64, precision: usize) -> String {
if x == 0.0 {
return "0".to_string();
}
let abs = x.abs();
let exp = abs.log10().floor() as i32;
if exp >= -4 && exp < precision as i32 {
let digits = (precision as i32 - 1 - exp).max(0) as usize;
let s = format!("{x:.digits$}");
trim_g_fixed(&s)
} else {
format_g_scientific(x, precision)
}
}
fn trim_g_fixed(s: &str) -> String {
if !s.contains('.') {
return s.to_string();
}
s.trim_end_matches('0').trim_end_matches('.').to_string()
}
fn format_g_scientific(x: f64, precision: usize) -> String {
let s = format!("{:.*e}", precision - 1, x);
rewrite_rust_e_as_c(&s, true)
}
fn format_e(x: f64, precision: usize) -> String {
let s = format!("{x:.precision$e}");
rewrite_rust_e_as_c(&s, false)
}
fn rewrite_rust_e_as_c(s: &str, trim_mantissa: bool) -> String {
let Some(e_pos) = s.find('e') else {
return s.to_string();
};
let mantissa = &s[..e_pos];
let exp_part = &s[e_pos + 1..];
let mantissa_out = if trim_mantissa && mantissa.contains('.') {
let t = mantissa.trim_end_matches('0').trim_end_matches('.');
t.to_string()
} else {
mantissa.to_string()
};
let (sign, digits) = if let Some(d) = exp_part.strip_prefix('-') {
('-', d)
} else if let Some(d) = exp_part.strip_prefix('+') {
('+', d)
} else {
('+', exp_part)
};
let exp_padded = if digits.len() < 2 {
format!("{sign}0{digits}")
} else {
format!("{sign}{digits}")
};
format!("{mantissa_out}e{exp_padded}")
}
#[cfg(test)]
mod tests {
use super::*;
fn fmt_default() -> ValueFormat {
ValueFormat::default()
}
#[test]
fn g_default_precision_matches_c() {
assert_eq!(format_g(475.123, 6), "475.123");
assert_eq!(format_g(1.0, 6), "1");
assert_eq!(format_g(0.0, 6), "0");
assert_eq!(format_g(1e-5, 6), "1e-05");
assert_eq!(format_g(1e10, 6), "1e+10");
assert_eq!(format_g(0.0001, 6), "0.0001");
assert_eq!(format_g(1234567.0, 6), "1.23457e+06");
}
#[test]
fn e_format_matches_c() {
assert_eq!(format_e(1.5, 6), "1.500000e+00");
assert_eq!(format_e(1234.5, 2), "1.23e+03");
}
#[test]
fn negative_int_hex_matches_c() {
assert_eq!(format_int_i64(-1, IntStyle::Hex), "0xffffffffffffffff");
}
#[test]
fn array_renders_count_then_values() {
let v = EpicsValue::DoubleArray(vec![1.0, 2.5, 3.0]);
let s = format_value(&v, &fmt_default(), None);
assert_eq!(s, "3 1 2.5 3");
}
#[test]
fn enum_with_strings_renders_string() {
let strs = vec!["off".to_string(), "on".to_string()];
let v = EpicsValue::Enum(1);
let s = format_value(&v, &fmt_default(), Some(&strs));
assert_eq!(s, "on");
}
#[test]
fn enum_n_flag_renders_index() {
let strs = vec!["off".to_string(), "on".to_string()];
let v = EpicsValue::Enum(1);
let mut fmt = fmt_default();
fmt.enum_as_number = true;
let s = format_value(&v, &fmt, Some(&strs));
assert_eq!(s, "1");
}
#[test]
fn char_array_long_string_strips_at_nul() {
let v = EpicsValue::CharArray(b"hello\0xxxx".to_vec());
let mut fmt = fmt_default();
fmt.char_array_as_string = true;
assert_eq!(format_value(&v, &fmt, None), "hello");
}
#[test]
fn float_as_int_rounds_then_renders() {
let v = EpicsValue::Double(1234.6);
let mut fmt = fmt_default();
fmt.float_as_int = true;
fmt.int_style = IntStyle::Hex;
assert_eq!(format_value(&v, &fmt, None), "0x4d3");
}
#[test]
fn pv_name_width_constant_is_30() {
assert_eq!(PV_NAME_WIDTH, 30);
}
#[test]
fn max_elements_caps_array() {
let v = EpicsValue::LongArray((0..10).collect());
let mut fmt = fmt_default();
fmt.max_elements = Some(3);
let s = format_value(&v, &fmt, None);
assert_eq!(s, "10 0 1 2");
}
}