use crate::error::{CaError, CaResult};
use crate::server::record::{FieldDesc, ProcessOutcome, Record};
use crate::types::{DbFieldType, EpicsValue};
pub struct PrintfRecord {
pub val: String,
pub sizv: u16,
pub fmt: String,
pub inp_links: [String; 10],
pub vals: [EpicsValue; 10],
}
impl Default for PrintfRecord {
fn default() -> Self {
Self {
val: String::new(),
sizv: 256,
fmt: String::new(),
inp_links: Default::default(),
vals: std::array::from_fn(|_| EpicsValue::Double(0.0)),
}
}
}
struct Directive {
width: Option<usize>,
star_width: bool,
precision: Option<usize>,
star_prec: bool,
left_align: bool,
zero_pad: bool,
alt_form: bool, conv: u8,
long: bool,
bad: bool,
}
impl PrintfRecord {
fn val_as_string(v: &EpicsValue) -> String {
match v {
EpicsValue::String(s) => s.clone(),
EpicsValue::CharArray(bytes) => {
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
String::from_utf8_lossy(&bytes[..end]).into_owned()
}
EpicsValue::Double(d) => format!("{d}"),
EpicsValue::Float(f) => format!("{f}"),
EpicsValue::Long(n) => format!("{n}"),
EpicsValue::Short(n) => format!("{n}"),
EpicsValue::Int64(n) => format!("{n}"),
EpicsValue::Char(c) => format!("{c}"),
EpicsValue::Enum(e) => format!("{e}"),
other => format!("{other:?}"),
}
}
fn val_as_f64(v: &EpicsValue) -> f64 {
v.to_f64().unwrap_or(0.0)
}
fn parse_directive(bytes: &[u8], mut i: usize) -> (Directive, usize) {
let mut d = Directive {
width: None,
star_width: false,
precision: None,
star_prec: false,
left_align: false,
zero_pad: false,
alt_form: false,
conv: b's',
long: false,
bad: false,
};
i += 1; loop {
match bytes.get(i) {
Some(b'-') => d.left_align = true,
Some(b'+') | Some(b' ') => {}
Some(b'#') => d.alt_form = true,
Some(b'0') => d.zero_pad = true,
_ => break,
}
i += 1;
}
if bytes.get(i) == Some(&b'*') {
d.star_width = true;
i += 1;
} else {
let mut w = 0usize;
let mut any = false;
while let Some(c) = bytes.get(i) {
if c.is_ascii_digit() {
w = w * 10 + (c - b'0') as usize;
any = true;
i += 1;
} else {
break;
}
}
if any {
d.width = Some(w);
}
}
if bytes.get(i) == Some(&b'.') {
i += 1;
if bytes.get(i) == Some(&b'*') {
d.star_prec = true;
i += 1;
} else {
let mut p = 0usize;
while let Some(c) = bytes.get(i) {
if c.is_ascii_digit() {
p = p * 10 + (c - b'0') as usize;
i += 1;
} else {
break;
}
}
d.precision = Some(p);
}
}
loop {
match bytes.get(i) {
Some(b'h') => {
i += 1;
}
Some(b'l') => {
d.long = true;
i += 1;
}
_ => break,
}
}
match bytes.get(i) {
Some(&c) if b"diouxXeEfFgGcs".contains(&c) => {
d.conv = c;
i += 1;
}
Some(_) => {
d.bad = true;
i += 1;
}
None => {
d.bad = true;
}
}
(d, i)
}
fn apply_fmt(&self) -> String {
let mut result = String::new();
let bytes = self.fmt.as_bytes();
let mut i = 0;
let mut inp_idx = 0usize;
let take = |idx: &mut usize| -> Option<&EpicsValue> {
if *idx < 10 {
let v = &self.vals[*idx];
*idx += 1;
Some(v)
} else {
*idx += 1;
None
}
};
while i < bytes.len() {
if bytes[i] != b'%' {
result.push(bytes[i] as char);
i += 1;
continue;
}
if bytes.get(i + 1) == Some(&b'%') {
result.push('%');
i += 2;
continue;
}
let (d, next) = Self::parse_directive(bytes, i);
i = next;
if d.bad {
continue;
}
let width = if d.star_width {
take(&mut inp_idx)
.map(|v| Self::val_as_f64(v) as i64)
.unwrap_or(0)
} else {
d.width.unwrap_or(0) as i64
};
let precision = if d.star_prec {
take(&mut inp_idx)
.map(|v| Self::val_as_f64(v) as i64)
.unwrap_or(0)
.max(0) as usize
} else {
d.precision.unwrap_or(usize::MAX)
};
let (width, left_align) = if width < 0 {
((-width) as usize, true)
} else {
(width as usize, d.left_align)
};
let arg = take(&mut inp_idx);
let conv_prec = if precision == usize::MAX {
6
} else {
precision
};
let substituted = match d.conv {
b'd' | b'i' => {
let v = arg.map(Self::val_as_f64).unwrap_or(0.0);
pad_string(format!("{}", v as i64), width, left_align, d.zero_pad)
}
b'u' => {
let v = arg.map(Self::val_as_f64).unwrap_or(0.0);
pad_string(
format!("{}", v as i64 as u64),
width,
left_align,
d.zero_pad,
)
}
b'o' => {
let v = arg.map(Self::val_as_f64).unwrap_or(0.0) as i64 as u64;
let s = if d.alt_form && v != 0 {
format!("0{v:o}")
} else {
format!("{v:o}")
};
pad_string(s, width, left_align, d.zero_pad)
}
b'x' => {
let v = arg.map(Self::val_as_f64).unwrap_or(0.0) as i64 as u64;
let s = if d.alt_form && v != 0 {
format!("0x{v:x}")
} else {
format!("{v:x}")
};
pad_string(s, width, left_align, d.zero_pad)
}
b'X' => {
let v = arg.map(Self::val_as_f64).unwrap_or(0.0) as i64 as u64;
let s = if d.alt_form && v != 0 {
format!("0X{v:X}")
} else {
format!("{v:X}")
};
pad_string(s, width, left_align, d.zero_pad)
}
b'e' | b'E' | b'f' | b'F' | b'g' | b'G' => {
let v = arg.map(Self::val_as_f64).unwrap_or(0.0);
let s = format_float_conv(d.conv, v, conv_prec, d.alt_form);
pad_string(s, width, left_align, d.zero_pad)
}
b'c' => {
let ch = match arg {
Some(EpicsValue::String(s)) => s.chars().next().unwrap_or(' '),
Some(v) => {
let code = Self::val_as_f64(v) as u32;
char::from_u32(code).unwrap_or('\u{0}')
}
None => '\u{0}',
};
pad_string(ch.to_string(), width, left_align, false)
}
b's' => {
let mut s = arg.map(Self::val_as_string).unwrap_or_default();
if precision != usize::MAX && s.chars().count() > precision {
s = s.chars().take(precision).collect();
}
pad_string(s, width, left_align, false)
}
_ => String::new(),
};
result.push_str(&substituted);
}
let max = (self.sizv as usize).saturating_sub(1);
if result.len() > max {
let trunc = (0..=max)
.rev()
.find(|&n| result.is_char_boundary(n))
.unwrap_or(0);
result.truncate(trunc);
}
result
}
fn inp_index(name: &str) -> Option<usize> {
let bytes = name.as_bytes();
if bytes.len() == 4 && &bytes[0..3] == b"INP" {
let digit = bytes[3];
if digit.is_ascii_digit() {
return Some((digit - b'0') as usize);
}
}
None
}
fn val_index(name: &str) -> Option<usize> {
if name.len() == 1 {
let c = name.as_bytes()[0];
if (b'A'..=b'J').contains(&c) {
return Some((c - b'A') as usize);
}
}
None
}
}
fn pad_string(s: String, width: usize, left_align: bool, zero_pad: bool) -> String {
if width <= s.chars().count() {
return s;
}
if left_align {
format!("{s:<width$}")
} else if zero_pad {
if s.starts_with('-') || s.starts_with('+') {
format!("{}{:0>width$}", &s[..1], &s[1..], width = width - 1)
} else {
format!("{s:0>width$}")
}
} else {
format!("{s:>width$}")
}
}
fn format_float_conv(conv: u8, val: f64, prec: usize, alt_form: bool) -> String {
match conv {
b'e' => format!("{val:.prec$e}"),
b'E' => format!("{val:.prec$E}"),
b'f' | b'F' => format!("{val:.prec$}"),
b'g' => format_g_val(val, prec, false, alt_form),
b'G' => format_g_val(val, prec, true, alt_form),
_ => format!("{val:.prec$}"),
}
}
fn format_g_val(val: f64, prec: usize, upper: bool, alt_form: bool) -> String {
if val == 0.0 {
if alt_form {
let p = if prec == 0 { 1 } else { prec };
let decimals = p.saturating_sub(1);
return format!("{:.*}", decimals, 0.0);
}
return "0".to_string();
}
let p = if prec == 0 { 1 } else { prec };
let exp = val.abs().log10().floor() as i32;
if exp < -4 || exp >= p as i32 {
let sig_prec = p.saturating_sub(1);
let raw = if upper {
format!("{val:.sig_prec$E}")
} else {
format!("{val:.sig_prec$e}")
};
if alt_form {
raw
} else {
strip_trailing_zeros_sci(&raw, upper)
}
} else {
let decimal_places = (p as i32 - 1 - exp).max(0) as usize;
let raw = format!("{val:.decimal_places$}");
if alt_form {
raw
} else if raw.contains('.') {
raw.trim_end_matches('0').trim_end_matches('.').to_string()
} else {
raw
}
}
}
fn strip_trailing_zeros_sci(s: &str, upper: bool) -> String {
let sep = if upper { 'E' } else { 'e' };
if let Some(pos) = s.find(sep) {
let mantissa = &s[..pos];
let exp_part = &s[pos..];
let trimmed = if mantissa.contains('.') {
mantissa.trim_end_matches('0').trim_end_matches('.')
} else {
mantissa
};
format!("{trimmed}{exp_part}")
} else {
s.to_string()
}
}
static PRINTF_FIELDS: &[FieldDesc] = &[
FieldDesc {
name: "VAL",
dbf_type: DbFieldType::Char,
read_only: true,
},
FieldDesc {
name: "SIZV",
dbf_type: DbFieldType::Short,
read_only: false,
},
FieldDesc {
name: "FMT",
dbf_type: DbFieldType::String,
read_only: false,
},
FieldDesc {
name: "INP0",
dbf_type: DbFieldType::String,
read_only: false,
},
FieldDesc {
name: "INP1",
dbf_type: DbFieldType::String,
read_only: false,
},
FieldDesc {
name: "INP2",
dbf_type: DbFieldType::String,
read_only: false,
},
FieldDesc {
name: "INP3",
dbf_type: DbFieldType::String,
read_only: false,
},
FieldDesc {
name: "INP4",
dbf_type: DbFieldType::String,
read_only: false,
},
FieldDesc {
name: "INP5",
dbf_type: DbFieldType::String,
read_only: false,
},
FieldDesc {
name: "INP6",
dbf_type: DbFieldType::String,
read_only: false,
},
FieldDesc {
name: "INP7",
dbf_type: DbFieldType::String,
read_only: false,
},
FieldDesc {
name: "INP8",
dbf_type: DbFieldType::String,
read_only: false,
},
FieldDesc {
name: "INP9",
dbf_type: DbFieldType::String,
read_only: false,
},
FieldDesc {
name: "A",
dbf_type: DbFieldType::Double,
read_only: false,
},
FieldDesc {
name: "B",
dbf_type: DbFieldType::Double,
read_only: false,
},
FieldDesc {
name: "C",
dbf_type: DbFieldType::Double,
read_only: false,
},
FieldDesc {
name: "D",
dbf_type: DbFieldType::Double,
read_only: false,
},
FieldDesc {
name: "E",
dbf_type: DbFieldType::Double,
read_only: false,
},
FieldDesc {
name: "F",
dbf_type: DbFieldType::Double,
read_only: false,
},
FieldDesc {
name: "G",
dbf_type: DbFieldType::Double,
read_only: false,
},
FieldDesc {
name: "H",
dbf_type: DbFieldType::Double,
read_only: false,
},
FieldDesc {
name: "I",
dbf_type: DbFieldType::Double,
read_only: false,
},
FieldDesc {
name: "J",
dbf_type: DbFieldType::Double,
read_only: false,
},
];
impl Record for PrintfRecord {
fn record_type(&self) -> &'static str {
"printf"
}
fn field_list(&self) -> &'static [FieldDesc] {
PRINTF_FIELDS
}
fn uses_monitor_deadband(&self) -> bool {
false
}
fn process(&mut self) -> CaResult<ProcessOutcome> {
self.val = self.apply_fmt();
Ok(ProcessOutcome::complete())
}
fn val(&self) -> Option<EpicsValue> {
Some(EpicsValue::CharArray(self.val.as_bytes().to_vec()))
}
fn get_field(&self, name: &str) -> Option<EpicsValue> {
match name {
"VAL" => Some(EpicsValue::CharArray(self.val.as_bytes().to_vec())),
"SIZV" => Some(EpicsValue::Short(self.sizv as i16)),
"FMT" => Some(EpicsValue::String(self.fmt.clone())),
_ => {
if let Some(idx) = Self::inp_index(name) {
return Some(EpicsValue::String(self.inp_links[idx].clone()));
}
if let Some(idx) = Self::val_index(name) {
return Some(self.vals[idx].clone());
}
None
}
}
}
fn put_field(&mut self, name: &str, value: EpicsValue) -> CaResult<()> {
match name {
"SIZV" => {
if let EpicsValue::Short(v) = value {
let v = (v as i32).clamp(16, 0x7fff);
self.sizv = v as u16;
}
}
"FMT" => {
if let EpicsValue::String(s) = value {
self.fmt = s;
} else {
return Err(CaError::TypeMismatch("FMT".into()));
}
}
_ => {
if let Some(idx) = Self::inp_index(name) {
if let EpicsValue::String(s) = value {
self.inp_links[idx] = s;
} else {
return Err(CaError::TypeMismatch(name.into()));
}
} else if let Some(idx) = Self::val_index(name) {
self.vals[idx] = value;
} else {
return Err(CaError::FieldNotFound(name.to_string()));
}
}
}
Ok(())
}
fn multi_input_links(&self) -> &[(&'static str, &'static str)] {
&[
("INP0", "A"),
("INP1", "B"),
("INP2", "C"),
("INP3", "D"),
("INP4", "E"),
("INP5", "F"),
("INP6", "G"),
("INP7", "H"),
("INP8", "I"),
("INP9", "J"),
]
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rec_with(fmt: &str) -> PrintfRecord {
PrintfRecord {
fmt: fmt.to_string(),
..Default::default()
}
}
#[test]
fn percent_s_formats_string_input() {
let mut rec = rec_with("name=%s");
rec.vals[0] = EpicsValue::String("motor1".into());
rec.process().unwrap();
assert_eq!(rec.val, "name=motor1");
}
#[test]
fn percent_s_with_width_padding() {
let mut rec = rec_with("[%8s]");
rec.vals[0] = EpicsValue::String("ab".into());
rec.process().unwrap();
assert_eq!(rec.val, "[ ab]");
}
#[test]
fn star_width_consumes_an_input() {
let mut rec = rec_with("%*d");
rec.vals[0] = EpicsValue::Long(6); rec.vals[1] = EpicsValue::Long(42); rec.process().unwrap();
assert_eq!(rec.val, " 42");
}
#[test]
fn long_modifier_consumed() {
let mut rec = rec_with("%ld");
rec.vals[0] = EpicsValue::Long(99);
rec.process().unwrap();
assert_eq!(rec.val, "99");
}
#[test]
fn long_string_conversion() {
let mut rec = rec_with("%ls");
rec.vals[0] = EpicsValue::String("hello".into());
rec.process().unwrap();
assert_eq!(rec.val, "hello");
}
#[test]
fn percent_c_formats_char() {
let mut rec = rec_with("%c");
rec.vals[0] = EpicsValue::Long(65); rec.process().unwrap();
assert_eq!(rec.val, "A");
}
#[test]
fn g_zero_alt_form() {
let mut rec = rec_with("%g");
rec.vals[0] = EpicsValue::Double(0.0);
rec.process().unwrap();
assert_eq!(rec.val, "0");
let mut rec = rec_with("%#.3g");
rec.vals[0] = EpicsValue::Double(0.0);
rec.process().unwrap();
assert_eq!(rec.val, "0.00");
}
#[test]
fn percent_escape() {
let mut rec = rec_with("100%%");
rec.process().unwrap();
assert_eq!(rec.val, "100%");
}
}