use crate::v2;
pub fn range_holds_zero(min: Option<&str>, max: Option<&str>, step: Option<&str>) -> bool {
let read = |text: Option<&str>| -> Result<Option<Decimal>, ()> {
match text {
None => Ok(None),
Some(text) => Decimal::parse(text).map(Some).ok_or(()),
}
};
let (Ok(min), Ok(max), Ok(step)) = (read(min), read(max), read(step)) else {
return false;
};
if min.is_some_and(|min| min.units > 0) || max.is_some_and(|max| max.units < 0) {
return false;
}
match (step, min) {
(None, _) => true,
(Some(_), None) => false,
(Some(step), Some(min)) => {
let scale = min.scale.max(step.scale);
match (min.rescaled(scale), step.rescaled(scale)) {
(Some(min), Some(step)) if step > 0 => min % step == 0,
_ => false,
}
}
}
}
pub fn enum_zero_member(values: &[v2::EnumValue]) -> Option<usize> {
values.iter().position(|value| value.value == 0)
}
#[derive(Debug, Clone, Copy)]
struct Decimal {
units: i128,
scale: u32,
}
impl Decimal {
fn parse(text: &str) -> Option<Self> {
let (negative, unsigned) = match text.strip_prefix('-') {
Some(rest) => (true, rest),
None => (false, text),
};
let (whole, fraction) = match unsigned.split_once('.') {
Some((whole, fraction)) => (whole, fraction),
None => (unsigned, ""),
};
let digits_only = |part: &str| part.bytes().all(|byte| byte.is_ascii_digit());
if whole.is_empty()
|| !digits_only(whole)
|| !digits_only(fraction)
|| (unsigned.contains('.') && fraction.is_empty())
|| whole.len() + fraction.len() > 30
{
return None;
}
let units: i128 = format!("{whole}{fraction}").parse().ok()?;
Some(Decimal {
units: if negative { -units } else { units },
scale: u32::try_from(fraction.len()).ok()?,
})
}
fn rescaled(self, scale: u32) -> Option<i128> {
self.units
.checked_mul(10i128.checked_pow(scale.checked_sub(self.scale)?)?)
}
}
#[cfg(test)]
mod tests {
use super::{enum_zero_member, range_holds_zero, v2};
#[test]
fn range_holds_zero_follows_the_range_and_the_step() {
let cases = [
(None, None, None, true),
(Some("0"), Some("10"), None, true),
(Some("1"), Some("10"), None, false),
(Some("-10"), Some("-1"), None, false),
(Some("-10"), Some("0"), None, true),
(Some("0"), Some("0"), None, true),
(Some("-1.5"), Some("1.5"), Some("1.0"), false),
(Some("-1.0"), Some("1.0"), Some("1.0"), true),
(Some("0.0"), Some("1.0"), Some("0.01"), true),
(Some("-0.25"), Some("1"), Some("0.125"), true),
(Some("-0.3"), Some("1"), Some("0.2"), false),
(None, Some("10"), Some("1"), false),
(Some("-1e3"), Some("10"), None, false),
(Some("-1."), Some("10"), None, false),
];
for (min, max, step, legal) in cases {
assert_eq!(
range_holds_zero(min, max, step),
legal,
"min {min:?}, max {max:?}, step {step:?}"
);
}
}
#[test]
fn the_zero_member_is_the_value_numbered_zero_in_any_position() {
let value = |name: &str, value: i64| v2::EnumValue {
name: name.to_string(),
value,
doc: String::new(),
};
assert_eq!(
enum_zero_member(&[value("ON", 1), value("OFF", 0)]),
Some(1)
);
assert_eq!(
enum_zero_member(&[value("PARK", 1), value("DRIVE", 2)]),
None
);
}
}