use crate::value::Value;
const PAD: usize = 20;
const RELEASE: char = '~';
const PRERELEASE: char = '!';
pub fn semver_key(raw: &str) -> Option<String> {
let s = raw.trim();
if s.is_empty() || !s.is_ascii() {
return None;
}
let s = s.strip_prefix(['v', 'V']).unwrap_or(s);
let s = s.split('+').next().unwrap_or(s);
let (core, pre) = match s.split_once('-') {
Some((c, p)) => (c, Some(p)),
None => (s, None),
};
let mut key = String::new();
let mut parts = 0usize;
for part in core.split('.') {
parts += 1;
if parts > 3 || part.is_empty() || !part.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
let n: u64 = part.parse().ok()?;
if parts > 1 {
key.push('.');
}
key.push_str(&format!("{n:0PAD$}"));
}
if parts < 2 {
return None;
}
for _ in parts..3 {
key.push('.');
key.push_str(&format!("{:0PAD$}", 0));
}
match pre {
None => key.push(RELEASE),
Some(p) => {
key.push(PRERELEASE);
key.push_str(&encode_pre(p)?);
}
}
Some(key)
}
fn encode_pre(pre: &str) -> Option<String> {
let mut out = String::new();
for (i, id) in pre.split('.').enumerate() {
if id.is_empty() || !id.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-') {
return None;
}
if i > 0 {
out.push('.');
}
if id.bytes().all(|b| b.is_ascii_digit()) {
let n: u64 = id.parse().ok()?;
out.push('0');
out.push_str(&format!("{n:0PAD$}"));
} else {
out.push('1');
out.push_str(id);
}
}
Some(out)
}
const JUNK_KEY: &str = "~1";
const EMPTY_KEY: &str = "~2";
pub fn column_sort_key(v: &Value) -> String {
if v.is_empty() {
return EMPTY_KEY.to_string();
}
value_semver_key(v).unwrap_or_else(|| JUNK_KEY.to_string())
}
pub fn value_semver_key(v: &Value) -> Option<String> {
match v {
Value::Str(s) => semver_key(s),
_ => None,
}
}
pub fn column_is_semver<'a>(values: impl Iterator<Item = &'a Value>) -> bool {
let mut saw_one = false;
for v in values {
if v.is_empty() {
continue;
}
if value_semver_key(v).is_none() {
return false;
}
saw_one = true;
}
saw_one
}
#[cfg(test)]
mod tests {
use super::*;
fn lt(a: &str, b: &str) {
let (ka, kb) = (semver_key(a).unwrap(), semver_key(b).unwrap());
assert!(ka < kb, "expected {a} < {b} (keys {ka:?} vs {kb:?})");
assert!(kb > ka, "expected {b} > {a} (asymmetry)");
}
fn eq(a: &str, b: &str) {
assert_eq!(semver_key(a).unwrap(), semver_key(b).unwrap(), "{a} == {b}");
}
#[test]
fn orders_core_numerically_not_lexically() {
lt("1.0.23", "1.2.12");
lt("1.2.12", "1.19.20");
lt("1.9.0", "1.10.0");
lt("2.0.0", "10.0.0");
}
#[test]
fn lenient_forms_normalise() {
eq("v1.2.3", "1.2.3");
eq("V1.2.3", "1.2.3");
eq("1.2", "1.2.0");
eq(" 1.2.3 ", "1.2.3");
eq("1.2.3+build.5", "1.2.3");
eq("1.2.3+exp.sha.5114f85", "1.2.3");
}
#[test]
fn prerelease_sorts_before_its_release() {
lt("1.0.0-rc1", "1.0.0");
lt("1.0.0-alpha", "1.0.0-beta");
lt("1.0.0", "1.0.1-alpha");
}
#[test]
fn semver_spec_precedence_example() {
let ordered = [
"1.0.0-alpha",
"1.0.0-alpha.1",
"1.0.0-alpha.beta",
"1.0.0-beta",
"1.0.0-beta.2",
"1.0.0-beta.11",
"1.0.0-rc.1",
"1.0.0",
];
for pair in ordered.windows(2) {
lt(pair[0], pair[1]);
}
}
#[test]
fn numeric_prerelease_ids_compare_numerically() {
lt("1.0.0-beta.2", "1.0.0-beta.11");
lt("1.0.0-1", "1.0.0-2");
lt("1.0.0-9", "1.0.0-10");
}
#[test]
fn rejects_non_versions() {
for s in [
"",
" ",
"abc",
"1", "42", "1.2.3.4", "1..2", "1.x", "1.2-", "1.2-rc/1", "1.2.3-café", "99999999999999999999999", ] {
assert!(semver_key(s).is_none(), "expected {s:?} to be rejected");
}
}
#[test]
fn keys_are_ascii() {
for s in ["1.2.3", "v1.0.0-rc.1", "1.2", "1.0.0-alpha.beta"] {
assert!(semver_key(s).unwrap().is_ascii(), "{s} produced non-ASCII");
}
}
#[test]
fn column_detection_requires_all_non_empty_to_parse() {
let ver = |s: &str| Value::Str(s.into());
assert!(column_is_semver([ver("1.2.3"), ver("1.10.0")].iter()));
assert!(column_is_semver(
[ver("1.2.3"), Value::Null, ver("2.0.0")].iter()
));
assert!(!column_is_semver([ver("1.2.3"), ver("nightly")].iter()));
assert!(!column_is_semver([Value::Null].iter()));
assert!(!column_is_semver([].iter()));
}
#[test]
fn numbers_are_not_versions() {
assert!(value_semver_key(&Value::Number(1.5)).is_none());
assert!(!column_is_semver(
[Value::Number(1.5), Value::Number(1.10)].iter()
));
}
}