use crate::v2;
pub fn range_holds_zero(min: Option<&str>, max: Option<&str>, step: Option<&str>) -> bool {
if min.is_none_or(|min| decimal_sign(min) == Some(0))
&& max.is_none_or(|max| decimal_sign(max).is_some_and(|sign| sign >= 0))
{
return step.is_none_or(|step| decimal_sign(step) == Some(1));
}
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(step), None) => step.units > 0,
(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,
}
}
}
}
fn decimal_sign(text: &str) -> Option<i8> {
let (negative, unsigned) = text
.strip_prefix('-')
.map_or((false, text), |text| (true, text));
let (whole, fraction) = unsigned.split_once('.').unwrap_or((unsigned, ""));
if whole.is_empty()
|| !whole.bytes().all(|byte| byte.is_ascii_digit())
|| !fraction.bytes().all(|byte| byte.is_ascii_digit())
|| (unsigned.contains('.') && fraction.is_empty())
{
return None;
}
if whole
.bytes()
.chain(fraction.bytes())
.all(|byte| byte == b'0')
{
Some(0)
} else {
Some(if negative { -1 } else { 1 })
}
}
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"), true),
(None, None, Some("0.5"), true),
(None, None, Some("0"), false),
(None, None, Some("-0.5"), 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 zero_origin_holds_zero_at_any_canonical_step_precision() {
let fine = format!("0.{}1", "0".repeat(399));
let large = format!("1{}.0", "0".repeat(400));
for step in [&fine, &large] {
assert!(range_holds_zero(Some("0.0"), None, Some(step)));
assert!(range_holds_zero(None, Some("1.0"), Some(step)));
assert!(!range_holds_zero(None, None, Some(&format!("-{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(),
links: Vec::new(),
see: Vec::new(),
since: Vec::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
);
}
}