#[must_use]
pub fn split_version(s: &str) -> Vec<String> {
let mut parts = Vec::new();
let mut current = String::new();
let mut prev_digit: Option<bool> = None;
for ch in s.chars() {
if ch == '.' || ch == '-' {
if !current.is_empty() {
parts.push(std::mem::take(&mut current));
}
prev_digit = None;
} else {
let is_digit = ch.is_ascii_digit();
if let Some(was_digit) = prev_digit
&& is_digit != was_digit
&& !current.is_empty()
{
parts.push(std::mem::take(&mut current));
}
current.push(ch);
prev_digit = Some(is_digit);
}
}
if !current.is_empty() {
parts.push(current);
}
parts
}
#[must_use]
pub fn compare_versions(a: &str, b: &str) -> i64 {
let pa = split_version(a);
let pb = split_version(b);
let max_len = pa.len().max(pb.len());
for i in 0..max_len {
let ca = pa.get(i).map(String::as_str).unwrap_or("");
let cb = pb.get(i).map(String::as_str).unwrap_or("");
let ord = match (ca.parse::<i64>(), cb.parse::<i64>()) {
(Ok(na), Ok(nb)) => na.cmp(&nb),
_ => match (ca, cb) {
("pre", "pre") => std::cmp::Ordering::Equal,
("pre", _) => std::cmp::Ordering::Less,
(_, "pre") => std::cmp::Ordering::Greater,
_ => ca.cmp(cb),
},
};
if ord != std::cmp::Ordering::Equal {
return if ord == std::cmp::Ordering::Less { -1 } else { 1 };
}
}
0
}
#[must_use]
pub fn cppnix_format_float(f: f64) -> String {
if f.is_nan() {
return "NaN".to_string();
}
if f.is_infinite() {
return if f > 0.0 { "inf".to_string() } else { "-inf".to_string() };
}
if f == 0.0 {
return "0".to_string();
}
let exp = f.abs().log10().floor() as i32;
if (-4..6).contains(&exp) {
let after_decimal = (5 - exp).max(0) as usize;
let raw = format!("{f:.*}", after_decimal);
if let Some((whole, frac)) = raw.split_once('.') {
let trimmed = frac.trim_end_matches('0');
if trimmed.is_empty() {
whole.to_string()
} else {
format!("{whole}.{trimmed}")
}
} else {
raw
}
} else {
let raw = format!("{f:.5e}");
if let Some((mantissa, exp_part)) = raw.split_once('e') {
let mantissa_trimmed =
if let Some((w, frac)) = mantissa.split_once('.') {
let trimmed = frac.trim_end_matches('0');
if trimmed.is_empty() {
w.to_string()
} else {
format!("{w}.{trimmed}")
}
} else {
mantissa.to_string()
};
let exp_part_signed = if exp_part.starts_with('-') {
exp_part.to_string()
} else {
format!("+{exp_part}")
};
format!("{mantissa_trimmed}e{exp_part_signed}")
} else {
raw
}
}
}
#[must_use]
pub fn parse_drv_name(s: &str) -> (String, String) {
let bytes = s.as_bytes();
for i in (0..bytes.len()).rev() {
if bytes[i] == b'-'
&& i + 1 < bytes.len()
&& bytes[i + 1].is_ascii_digit()
{
return (s[..i].to_string(), s[i + 1..].to_string());
}
}
(s.to_string(), String::new())
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
#[test]
fn rc_orders_above_pre() {
assert_eq!(compare_versions("1.0-rc1", "1.0-pre1"), 1);
assert_eq!(compare_versions("1.0-pre1", "1.0-rc1"), -1);
}
#[test]
fn numeric_components_compare_numerically() {
assert_eq!(compare_versions("1.10", "1.2"), 1);
assert_eq!(compare_versions("1.2", "1.10"), -1);
assert_eq!(compare_versions("1.0", "1.0"), 0);
}
#[test]
fn missing_components_order_below_present_components() {
assert_eq!(compare_versions("1.0", "1.0.0"), -1);
assert_eq!(compare_versions("1.0.0", "1.0"), 1);
}
#[test]
fn pre_below_everything_except_pre() {
assert_eq!(compare_versions("1.0-pre1", "1.0-pre1"), 0);
assert_eq!(compare_versions("1.0-pre1", "1.0"), -1);
assert_eq!(compare_versions("1.0", "1.0-pre1"), 1);
assert_eq!(compare_versions("1.0-pre", "1.0-alpha"), -1);
assert_eq!(compare_versions("1.0-pre", "1.0-beta"), -1);
assert_eq!(compare_versions("1.0-pre", "1.0-rc"), -1);
}
#[test]
fn split_version_basic_shapes() {
assert_eq!(split_version("1.0-rc1"), vec!["1", "0", "rc", "1"]);
assert_eq!(split_version("1.0.0-pre"), vec!["1", "0", "0", "pre"]);
assert_eq!(split_version("2024a"), vec!["2024", "a"]);
}
#[test]
fn cppnix_format_float_known_outputs() {
assert_eq!(cppnix_format_float(1.0 / 3.0), "0.333333");
assert_eq!(cppnix_format_float(3.14159), "3.14159");
assert_eq!(cppnix_format_float(1.5), "1.5");
assert_eq!(cppnix_format_float(3.0), "3");
assert_eq!(cppnix_format_float(0.0), "0");
assert_eq!(cppnix_format_float(-3.14), "-3.14");
assert_eq!(cppnix_format_float(-3.0), "-3");
}
#[test]
fn cppnix_format_float_nan_and_infinity() {
assert_eq!(cppnix_format_float(f64::NAN), "NaN");
assert_eq!(cppnix_format_float(f64::INFINITY), "inf");
assert_eq!(cppnix_format_float(f64::NEG_INFINITY), "-inf");
}
#[test]
fn parse_drv_name_recovers_split() {
let (n, v) = parse_drv_name("hello-1.2.3");
assert_eq!(n, "hello");
assert_eq!(v, "1.2.3");
let (n, v) = parse_drv_name("nix-darwin-config");
assert_eq!(n, "nix-darwin-config");
assert_eq!(v, "");
}
proptest! {
#[test]
fn compare_versions_antisymmetric(
a in "[0-9a-z.-]{1,20}",
b in "[0-9a-z.-]{1,20}",
) {
let ab = compare_versions(&a, &b);
let ba = compare_versions(&b, &a);
prop_assert_eq!(ab, -ba);
}
#[test]
fn compare_versions_reflexive(a in "[0-9a-z.-]{1,20}") {
prop_assert_eq!(compare_versions(&a, &a), 0);
}
}
}