use std::cmp::Ordering;
fn normalize_version(version: &str) -> String {
if let Some(last_dash) = version.rfind('-') {
let suffix = &version[last_dash + 1..];
if suffix.chars().all(|c| c.is_ascii_digit()) {
return version[..last_dash].to_string();
}
}
version.to_string()
}
#[must_use]
pub fn compare_versions(a: &str, b: &str) -> Ordering {
let a_normalized = normalize_version(a);
let b_normalized = normalize_version(b);
let a_parts: Vec<&str> = a_normalized.split(['.', '-']).collect();
let b_parts: Vec<&str> = b_normalized.split(['.', '-']).collect();
let len = a_parts.len().max(b_parts.len());
for idx in 0..len {
let a_seg = a_parts.get(idx).copied().unwrap_or("0");
let b_seg = b_parts.get(idx).copied().unwrap_or("0");
let a_num_end = a_seg
.char_indices()
.find(|(_, c)| !c.is_ascii_digit())
.map_or(a_seg.len(), |(i, _)| i);
let b_num_end = b_seg
.char_indices()
.find(|(_, c)| !c.is_ascii_digit())
.map_or(b_seg.len(), |(i, _)| i);
let a_num_str = &a_seg[..a_num_end];
let b_num_str = &b_seg[..b_num_end];
let a_suffix = &a_seg[a_num_end..];
let b_suffix = &b_seg[b_num_end..];
match (a_num_str.parse::<i64>(), b_num_str.parse::<i64>()) {
(Ok(a_num), Ok(b_num)) => {
match a_num.cmp(&b_num) {
Ordering::Equal => {
match (a_suffix.is_empty(), b_suffix.is_empty()) {
(true, true) => {} (true, false) => return Ordering::Greater, (false, true) => return Ordering::Less, (false, false) => {
match a_suffix.cmp(b_suffix) {
Ordering::Equal => {}
ord => return ord,
}
}
}
}
ord => return ord,
}
}
(Ok(_), Err(_)) => {
return Ordering::Less;
}
(Err(_), Ok(_)) => {
return Ordering::Greater;
}
(Err(_), Err(_)) => {
match a_seg.cmp(b_seg) {
Ordering::Equal => {}
ord => return ord,
}
}
}
}
Ordering::Equal
}
#[must_use]
pub fn version_satisfies(version: &str, requirement: &str) -> bool {
if requirement.is_empty() {
return true;
}
let (op, req_version) = if let Some(rest) = requirement.strip_prefix(">=") {
(">=", rest)
} else if let Some(rest) = requirement.strip_prefix("<=") {
("<=", rest)
} else if let Some(rest) = requirement.strip_prefix("=") {
("=", rest)
} else if let Some(rest) = requirement.strip_prefix(">") {
(">", rest)
} else if let Some(rest) = requirement.strip_prefix("<") {
("<", rest)
} else {
return true;
};
let comparison = compare_versions(version, req_version);
match op {
">=" => matches!(comparison, Ordering::Equal | Ordering::Greater),
"<=" => matches!(comparison, Ordering::Equal | Ordering::Less),
"=" => comparison == Ordering::Equal,
">" => comparison == Ordering::Greater,
"<" => comparison == Ordering::Less,
_ => true, }
}
#[must_use]
pub fn extract_major_component(version: &str) -> Option<u64> {
let normalized = normalize_version(version);
let token = normalized.split(['.', '-']).next()?;
token.parse::<u64>().ok()
}
#[must_use]
pub fn is_major_version_bump(old: &str, new: &str) -> bool {
match (extract_major_component(old), extract_major_component(new)) {
(Some(old_major), Some(new_major)) => new_major > old_major,
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normalize_version() {
assert_eq!(normalize_version("1.2.3-1"), "1.2.3");
assert_eq!(normalize_version("1.2.3-42"), "1.2.3");
assert_eq!(normalize_version("2.0.0-1"), "2.0.0");
assert_eq!(normalize_version("1.2.3-alpha"), "1.2.3-alpha");
assert_eq!(normalize_version("1.2.3-beta1"), "1.2.3-beta1");
assert_eq!(normalize_version("1.2.3"), "1.2.3");
assert_eq!(normalize_version("2.0"), "2.0");
}
#[test]
fn test_compare_versions_basic() {
assert_eq!(compare_versions("1.0.0", "1.0.1"), Ordering::Less);
assert_eq!(compare_versions("1.0.1", "1.0.0"), Ordering::Greater);
assert_eq!(compare_versions("1.0.0", "1.0.0"), Ordering::Equal);
assert_eq!(compare_versions("2.0.0", "1.9.9"), Ordering::Greater);
assert_eq!(compare_versions("1.9.9", "2.0.0"), Ordering::Less);
}
#[test]
fn test_compare_versions_missing_segments() {
assert_eq!(compare_versions("1.0", "1.0.0"), Ordering::Equal);
assert_eq!(compare_versions("1.2", "1.2.0"), Ordering::Equal);
assert_eq!(compare_versions("1", "1.0.0"), Ordering::Equal);
assert_eq!(compare_versions("1.2", "1.2.1"), Ordering::Less);
}
#[test]
fn test_compare_versions_pkgrel() {
assert_eq!(compare_versions("1.2.3-1", "1.2.3-2"), Ordering::Equal);
assert_eq!(compare_versions("1.2.3-1", "1.2.3"), Ordering::Equal);
assert_eq!(compare_versions("1.2.3-10", "1.2.4-1"), Ordering::Less);
}
#[test]
fn test_compare_versions_text_segments() {
assert_eq!(compare_versions("1.2.3", "1.2.3alpha"), Ordering::Greater);
assert_eq!(compare_versions("1.2.3alpha", "1.2.3"), Ordering::Less);
assert_eq!(compare_versions("1.2.3alpha", "1.2.3beta"), Ordering::Less);
assert_eq!(
compare_versions("1.2.3beta", "1.2.3alpha"),
Ordering::Greater
);
}
#[test]
fn test_compare_versions_mixed() {
assert_eq!(compare_versions("1.2.3", "1.2.4"), Ordering::Less);
assert_eq!(compare_versions("1.2.3alpha", "1.2.3beta"), Ordering::Less);
assert_eq!(compare_versions("1.2.3", "1.2.3alpha"), Ordering::Greater);
assert_eq!(compare_versions("1.2.3alpha", "1.2.4"), Ordering::Less);
}
#[test]
fn test_compare_versions_edge_cases() {
assert_eq!(compare_versions("", ""), Ordering::Equal);
assert_eq!(compare_versions("0", "0.0.0"), Ordering::Equal);
assert_eq!(compare_versions("10.0.0", "9.9.9"), Ordering::Greater);
assert_eq!(compare_versions("1.10.0", "1.9.9"), Ordering::Greater);
}
#[test]
fn test_version_satisfies_greater_equal() {
assert!(version_satisfies("2.0", ">=1.5"));
assert!(version_satisfies("1.5", ">=1.5"));
assert!(!version_satisfies("1.0", ">=1.5"));
assert!(version_satisfies("1.5.1", ">=1.5"));
assert!(version_satisfies("2.0.0", ">=1.5.0"));
}
#[test]
fn test_version_satisfies_less_equal() {
assert!(version_satisfies("1.0", "<=1.5"));
assert!(version_satisfies("1.5", "<=1.5"));
assert!(!version_satisfies("2.0", "<=1.5"));
assert!(version_satisfies("1.4.9", "<=1.5"));
}
#[test]
fn test_version_satisfies_equal() {
assert!(version_satisfies("1.5", "=1.5"));
assert!(!version_satisfies("1.6", "=1.5"));
assert!(!version_satisfies("1.4", "=1.5"));
assert!(version_satisfies("1.5.0", "=1.5"));
}
#[test]
fn test_version_satisfies_greater() {
assert!(version_satisfies("1.6", ">1.5"));
assert!(!version_satisfies("1.5", ">1.5"));
assert!(!version_satisfies("1.4", ">1.5"));
assert!(version_satisfies("2.0", ">1.5"));
}
#[test]
fn test_version_satisfies_less() {
assert!(version_satisfies("1.4", "<1.5"));
assert!(!version_satisfies("1.5", "<1.5"));
assert!(!version_satisfies("1.6", "<1.5"));
assert!(version_satisfies("1.0", "<1.5"));
}
#[test]
fn test_version_satisfies_empty() {
assert!(version_satisfies("2.0", ""));
assert!(version_satisfies("1.0", ""));
assert!(version_satisfies("any-version", ""));
}
#[test]
fn test_version_satisfies_no_operator() {
assert!(version_satisfies("2.0", "n/a"));
assert!(version_satisfies("1.0", "some-text"));
}
#[test]
fn test_version_satisfies_pkgrel() {
assert!(version_satisfies("1.2.3-1", ">=1.2.3"));
assert!(version_satisfies("1.2.3-10", ">=1.2.3"));
assert!(version_satisfies("1.2.3", ">=1.2.3-1"));
assert!(version_satisfies("1.2.3-5", "=1.2.3-1")); }
#[test]
fn test_extract_major_component() {
assert_eq!(extract_major_component("1.2.3"), Some(1));
assert_eq!(extract_major_component("2.0.0"), Some(2));
assert_eq!(extract_major_component("10.5.2"), Some(10));
assert_eq!(extract_major_component("2.0.0-alpha"), Some(2));
assert_eq!(extract_major_component("1.2.3-1"), Some(1));
assert_eq!(extract_major_component("alpha"), None);
assert_eq!(extract_major_component(""), None);
}
#[test]
fn test_is_major_version_bump() {
assert!(is_major_version_bump("1.2.3", "2.0.0"));
assert!(is_major_version_bump("1.0.0", "2.0.0"));
assert!(is_major_version_bump("0.9.9", "1.0.0"));
assert!(!is_major_version_bump("1.2.3", "1.3.0"));
assert!(!is_major_version_bump("1.2.3", "1.2.4"));
assert!(!is_major_version_bump("1.0.0", "1.9.9"));
assert!(!is_major_version_bump("2.0.0", "1.9.9"));
assert!(!is_major_version_bump("2.0.0", "1.0.0"));
assert!(!is_major_version_bump("alpha", "1.0.0"));
assert!(!is_major_version_bump("1.0.0", "beta"));
assert!(!is_major_version_bump("", "1.0.0"));
}
#[test]
fn test_is_major_version_bump_pkgrel() {
assert!(is_major_version_bump("1.2.3-1", "2.0.0-1"));
assert!(!is_major_version_bump("1.2.3-1", "1.3.0-1"));
}
}