use crate::error::{CaError, CaResult};
use crate::types::{DbFieldType, EpicsValue};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NumericField {
Char,
UChar,
Short,
UShort,
Long,
ULong,
Int64,
UInt64,
Float,
Double,
}
impl NumericField {
pub fn of(t: DbFieldType) -> Option<Self> {
Some(match t {
DbFieldType::Char => Self::Char,
DbFieldType::UChar => Self::UChar,
DbFieldType::Short => Self::Short,
DbFieldType::UShort => Self::UShort,
DbFieldType::Long => Self::Long,
DbFieldType::ULong => Self::ULong,
DbFieldType::Int64 => Self::Int64,
DbFieldType::UInt64 => Self::UInt64,
DbFieldType::Float => Self::Float,
DbFieldType::Double => Self::Double,
DbFieldType::String | DbFieldType::Enum => return None,
})
}
}
fn refuse(field: &str, s: &str, target: NumericField) -> CaError {
CaError::InvalidValue(format!(
"{field}: cannot convert \"{s}\" to {target:?} (C epicsParse* refuses this put)"
))
}
pub fn put_string(field: &str, target: NumericField, s: &str) -> CaResult<EpicsValue> {
parse(target, s).ok_or_else(|| refuse(field, s, target))
}
fn parse(target: NumericField, s: &str) -> Option<EpicsValue> {
Some(match target {
NumericField::Char => EpicsValue::Char(in_range(parse_long(s)?, -0x80, 0x7f)? as i8 as u8),
NumericField::Short => EpicsValue::Short(in_range(parse_long(s)?, -0x8000, 0x7fff)? as i16),
NumericField::Long => {
EpicsValue::Long(in_range(parse_long(s)?, -0x8000_0000, 0x7fff_ffff)? as i32)
}
NumericField::Int64 => EpicsValue::Int64(parse_long(s)?),
NumericField::UChar => EpicsValue::UChar(outside_band(parse_ulong(s)?, 0xff)? as u8),
NumericField::UShort => EpicsValue::UShort(outside_band(parse_ulong(s)?, 0xffff)? as u16),
NumericField::ULong => {
EpicsValue::ULong(outside_band(parse_ulong(s)?, 0xffff_ffff)? as u32)
}
NumericField::UInt64 => EpicsValue::UInt64(parse_ulong(s)?),
NumericField::Float => {
let v = parse_double(s)?;
let abs = v.abs();
if v > 0.0 && abs <= f32::MIN_POSITIVE as f64 {
return None; }
if v.is_finite() && abs >= f32::MAX as f64 {
return None; }
EpicsValue::Float(v as f32)
}
NumericField::Double => EpicsValue::Double(parse_double(s)?),
})
}
fn in_range(v: i64, lo: i64, hi: i64) -> Option<i64> {
(lo..=hi).contains(&v).then_some(v)
}
fn outside_band(v: u64, max: u64) -> Option<u64> {
(!(v > max && v <= !max)).then_some(v)
}
struct Digits {
negative: bool,
magnitude: Option<u128>,
any: bool,
}
fn scan_int(s: &str) -> Digits {
let b = s.as_bytes();
let mut i = 0;
while i < b.len() && b[i].is_ascii_whitespace() {
i += 1;
}
let negative = i < b.len() && b[i] == b'-';
if i < b.len() && (b[i] == b'-' || b[i] == b'+') {
i += 1;
}
let mut base = 10u32;
if i < b.len() && b[i] == b'0' {
let next = b.get(i + 1).map(|c| c.to_ascii_lowercase());
let after = b.get(i + 2).copied();
if next == Some(b'x') && after.is_some_and(|c| c.is_ascii_hexdigit()) {
base = 16;
i += 2;
} else if next == Some(b'b') && after.is_some_and(|c| c == b'0' || c == b'1') {
base = 2;
i += 2;
} else {
base = 8;
}
}
let mut magnitude = Some(0u128);
let mut any = false;
while i < b.len() {
let Some(d) = (b[i] as char).to_digit(base) else {
break;
};
any = true;
magnitude = magnitude
.and_then(|m| m.checked_mul(u128::from(base)))
.and_then(|m| m.checked_add(u128::from(d)));
i += 1;
}
Digits {
negative,
magnitude,
any,
}
}
fn parse_long(s: &str) -> Option<i64> {
let d = scan_int(s);
if !d.any {
return None;
}
let m = d.magnitude?;
if d.negative {
(m <= i64::MAX as u128 + 1).then(|| (m as i128).wrapping_neg() as i64)
} else {
(m <= i64::MAX as u128).then_some(m as i64)
}
}
fn parse_ulong(s: &str) -> Option<u64> {
let d = scan_int(s);
if !d.any {
return None;
}
let m = d.magnitude?;
let v = u64::try_from(m).ok()?;
Some(if d.negative { v.wrapping_neg() } else { v })
}
fn parse_double(s: &str) -> Option<f64> {
let (v, kind) = strtod(s)?;
match kind {
Literal::NonFinite => Some(v),
Literal::Finite { significant } => {
if v.is_infinite() {
return None; }
if v == 0.0 && significant {
return None; }
if v != 0.0 && v.abs() < f64::MIN_POSITIVE {
return None; }
Some(v)
}
}
}
enum Literal {
NonFinite,
Finite { significant: bool },
}
fn strtod(s: &str) -> Option<(f64, Literal)> {
let t = s.trim_start_matches(|c: char| c.is_ascii_whitespace());
let (sign, body) = match t.as_bytes().first() {
Some(b'-') => (-1.0, &t[1..]),
Some(b'+') => (1.0, &t[1..]),
_ => (1.0, t),
};
let lower = body.to_ascii_lowercase();
if lower.starts_with("infinity") || lower.starts_with("inf") {
return Some((sign * f64::INFINITY, Literal::NonFinite));
}
if lower.starts_with("nan") {
return Some((f64::NAN, Literal::NonFinite));
}
if lower.starts_with("0x") {
let (v, significant) = hex_float(&lower[2..])?;
return Some((sign * v, Literal::Finite { significant }));
}
let b = body.as_bytes();
let mut i = 0;
let mut significant = false;
let mut mantissa_digits = 0;
while i < b.len() && (b[i].is_ascii_digit() || b[i] == b'.') {
if b[i].is_ascii_digit() {
mantissa_digits += 1;
significant |= b[i] != b'0';
}
i += 1;
}
if mantissa_digits == 0 {
return None;
}
let mantissa_end = i;
if i < b.len() && (b[i] | 0x20) == b'e' {
let mut j = i + 1;
if j < b.len() && (b[j] == b'+' || b[j] == b'-') {
j += 1;
}
if j < b.len() && b[j].is_ascii_digit() {
while j < b.len() && b[j].is_ascii_digit() {
j += 1;
}
i = j;
}
}
let v: f64 = body[..i]
.parse()
.or_else(|_| body[..mantissa_end].parse())
.ok()?;
Some((sign * v, Literal::Finite { significant }))
}
fn hex_float(s: &str) -> Option<(f64, bool)> {
let b = s.as_bytes();
let mut i = 0;
let mut mantissa = 0.0f64;
let mut significant = false;
let mut digits = 0;
while i < b.len() && b[i].is_ascii_hexdigit() {
let d = (b[i] as char).to_digit(16)?;
mantissa = mantissa * 16.0 + f64::from(d);
significant |= d != 0;
digits += 1;
i += 1;
}
let mut scale = 0i32;
if i < b.len() && b[i] == b'.' {
i += 1;
while i < b.len() && b[i].is_ascii_hexdigit() {
let d = (b[i] as char).to_digit(16)?;
mantissa = mantissa * 16.0 + f64::from(d);
significant |= d != 0;
digits += 1;
scale -= 4;
i += 1;
}
}
if digits == 0 {
return None;
}
if i < b.len() && b[i] == b'p' {
let mut j = i + 1;
let neg = b.get(j) == Some(&b'-');
if b.get(j) == Some(&b'+') || neg {
j += 1;
}
let start = j;
let mut e = 0i32;
while j < b.len() && b[j].is_ascii_digit() {
e = e.saturating_mul(10).saturating_add((b[j] - b'0') as i32);
j += 1;
}
if j > start {
scale += if neg { -e } else { e };
}
}
Some((mantissa * (scale as f64).exp2(), significant))
}
#[cfg(test)]
mod tests {
use super::*;
fn put(t: NumericField, s: &str) -> Option<EpicsValue> {
put_string("F", t, s).ok()
}
#[test]
fn integer_past_the_destination_is_refused_not_saturated() {
assert_eq!(put(NumericField::Short, "32768"), None);
assert_eq!(put(NumericField::Short, "-32769"), None);
assert_eq!(put(NumericField::Long, "2147483648"), None);
assert_eq!(put(NumericField::Long, "-2147483649"), None);
assert_eq!(put(NumericField::Char, "128"), None);
assert_eq!(put(NumericField::Char, "-129"), None);
}
#[test]
fn at_the_limit_is_accepted() {
assert_eq!(
put(NumericField::Short, "32767"),
Some(EpicsValue::Short(32767))
);
assert_eq!(
put(NumericField::Short, "-32768"),
Some(EpicsValue::Short(-32768))
);
assert_eq!(
put(NumericField::Long, "2147483647"),
Some(EpicsValue::Long(2147483647))
);
assert_eq!(put(NumericField::Char, "127"), Some(EpicsValue::Char(127)));
}
#[test]
fn text_that_is_not_a_number_is_refused_not_stored_as_zero() {
assert_eq!(put(NumericField::Short, "notanumber"), None);
assert_eq!(put(NumericField::Double, "notanumber"), None);
assert_eq!(put(NumericField::Short, ""), None);
assert_eq!(put(NumericField::Double, ""), None);
}
#[test]
fn negative_into_unsigned_wraps_and_is_accepted() {
assert_eq!(put(NumericField::UChar, "-1"), Some(EpicsValue::UChar(255)));
assert_eq!(
put(NumericField::UShort, "-1"),
Some(EpicsValue::UShort(65535))
);
assert_eq!(
put(NumericField::ULong, "-1"),
Some(EpicsValue::ULong(4294967295))
);
assert_eq!(
put(NumericField::UInt64, "-1"),
Some(EpicsValue::UInt64(u64::MAX))
);
}
#[test]
fn unsigned_past_the_destination_is_refused() {
assert_eq!(put(NumericField::UChar, "256"), None);
assert_eq!(put(NumericField::UShort, "65536"), None);
assert_eq!(put(NumericField::ULong, "4294967296"), None);
assert_eq!(put(NumericField::UInt64, "18446744073709551616"), None);
assert_eq!(
put(NumericField::UChar, "255"),
Some(EpicsValue::UChar(255))
);
assert_eq!(
put(NumericField::ULong, "4294967295"),
Some(EpicsValue::ULong(4294967295))
);
}
#[test]
fn nan_and_infinity_are_stored_but_overflow_is_refused() {
assert!(
matches!(put(NumericField::Double, "NaN"), Some(EpicsValue::Double(v)) if v.is_nan())
);
assert_eq!(
put(NumericField::Double, "Inf"),
Some(EpicsValue::Double(f64::INFINITY))
);
assert_eq!(
put(NumericField::Double, "-Inf"),
Some(EpicsValue::Double(f64::NEG_INFINITY))
);
assert_eq!(
put(NumericField::Double, "infinity"),
Some(EpicsValue::Double(f64::INFINITY))
);
assert_eq!(put(NumericField::Double, "1e400"), None);
assert_eq!(put(NumericField::Double, "-1e400"), None);
assert_eq!(
put(NumericField::Double, "1e308"),
Some(EpicsValue::Double(1e308))
);
}
#[test]
fn double_underflow_to_zero_or_subnormal_is_refused_but_a_real_zero_is_not() {
assert_eq!(put(NumericField::Double, "1e-400"), None);
assert_eq!(put(NumericField::Double, "1e-320"), None);
assert_eq!(put(NumericField::Double, "4.9e-324"), None);
assert_eq!(
put(NumericField::Double, "0"),
Some(EpicsValue::Double(0.0))
);
assert_eq!(
put(NumericField::Double, "0.0"),
Some(EpicsValue::Double(0.0))
);
}
#[test]
fn float_refuses_what_a_double_accepts() {
assert_eq!(put(NumericField::Float, "1e39"), None);
assert_eq!(put(NumericField::Float, "1e308"), None);
assert_eq!(put(NumericField::Float, "1e-40"), None); assert_eq!(put(NumericField::Float, "1"), Some(EpicsValue::Float(1.0)));
assert_eq!(
put(NumericField::Float, "Inf"),
Some(EpicsValue::Float(f32::INFINITY))
);
assert!(
matches!(put(NumericField::Float, "NaN"), Some(EpicsValue::Float(v)) if v.is_nan())
);
}
#[test]
fn integers_are_parsed_base_zero() {
assert_eq!(
put(NumericField::Short, "0x10"),
Some(EpicsValue::Short(16))
);
assert_eq!(put(NumericField::Short, "010"), Some(EpicsValue::Short(8)));
assert_eq!(put(NumericField::Short, "0b11"), Some(EpicsValue::Short(3)));
assert_eq!(put(NumericField::Short, "+7"), Some(EpicsValue::Short(7)));
assert_eq!(put(NumericField::Short, "0x8000"), None);
assert_eq!(
put(NumericField::Double, "0x10"),
Some(EpicsValue::Double(16.0))
);
}
#[test]
fn trailing_text_is_units_and_is_ignored() {
assert_eq!(
put(NumericField::Short, "5volts"),
Some(EpicsValue::Short(5))
);
assert_eq!(put(NumericField::Short, "1.7"), Some(EpicsValue::Short(1)));
assert_eq!(put(NumericField::Short, "1e2"), Some(EpicsValue::Short(1)));
assert_eq!(
put(NumericField::Double, "1."),
Some(EpicsValue::Double(1.0))
);
assert_eq!(
put(NumericField::Double, ".5"),
Some(EpicsValue::Double(0.5))
);
assert_eq!(
put(NumericField::Short, " 42"),
Some(EpicsValue::Short(42))
);
}
#[test]
fn string_and_enum_have_no_numeric_row() {
assert_eq!(NumericField::of(DbFieldType::String), None);
assert_eq!(NumericField::of(DbFieldType::Enum), None);
assert_eq!(
NumericField::of(DbFieldType::Double),
Some(NumericField::Double)
);
}
}