use std::cmp::Ordering;
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Version {
pub major: u64,
pub minor: u64,
pub patch: u64,
pub pre_release: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseVersionError(String);
impl fmt::Display for ParseVersionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "invalid schema version: {}", self.0)
}
}
impl std::error::Error for ParseVersionError {}
impl FromStr for Version {
type Err = ParseVersionError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let core_and_pre = s.split_once('+').map(|(a, _)| a).unwrap_or(s);
let (core, pre_release) = match core_and_pre.split_once('-') {
Some((c, p)) if !p.is_empty() => (c, Some(p.to_string())),
Some(_) => return Err(ParseVersionError(s.to_string())),
None => (core_and_pre, None),
};
let mut parts = core.split('.');
let major = parse_numeric(parts.next(), s)?;
let minor = parse_numeric(parts.next(), s)?;
let patch = parse_numeric(parts.next(), s)?;
if parts.next().is_some() {
return Err(ParseVersionError(s.to_string()));
}
Ok(Version {
major,
minor,
patch,
pre_release,
})
}
}
fn parse_numeric(part: Option<&str>, full: &str) -> Result<u64, ParseVersionError> {
part
.filter(|p| !p.is_empty())
.and_then(|p| p.parse::<u64>().ok())
.ok_or_else(|| ParseVersionError(full.to_string()))
}
impl Ord for Version {
fn cmp(&self, other: &Self) -> Ordering {
self
.major
.cmp(&other.major)
.then_with(|| self.minor.cmp(&other.minor))
.then_with(|| self.patch.cmp(&other.patch))
.then_with(|| compare_pre_release(self.pre_release.as_deref(), other.pre_release.as_deref()))
}
}
impl PartialOrd for Version {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
if let Some(pre) = &self.pre_release {
write!(f, "-{}", pre)?;
}
Ok(())
}
}
fn compare_pre_release(a: Option<&str>, b: Option<&str>) -> Ordering {
match (a, b) {
(None, None) => Ordering::Equal,
(None, Some(_)) => Ordering::Greater,
(Some(_), None) => Ordering::Less,
(Some(a), Some(b)) => {
let mut a_parts = a.split('.');
let mut b_parts = b.split('.');
loop {
match (a_parts.next(), b_parts.next()) {
(None, None) => return Ordering::Equal,
(None, Some(_)) => return Ordering::Less,
(Some(_), None) => return Ordering::Greater,
(Some(ai), Some(bi)) => {
let ord = match (ai.parse::<u64>(), bi.parse::<u64>()) {
(Ok(an), Ok(bn)) => an.cmp(&bn),
(Ok(_), Err(_)) => Ordering::Less,
(Err(_), Ok(_)) => Ordering::Greater,
(Err(_), Err(_)) => ai.cmp(bi),
};
if ord != Ordering::Equal {
return ord;
}
}
}
}
}
}
}
pub fn compare(a: &str, b: &str) -> Result<Ordering, ParseVersionError> {
Ok(Version::from_str(a)?.cmp(&Version::from_str(b)?))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_release() {
let v = Version::from_str("2.3.0").unwrap();
assert_eq!(v.major, 2);
assert_eq!(v.minor, 3);
assert_eq!(v.patch, 0);
assert_eq!(v.pre_release, None);
}
#[test]
fn parses_pre_release() {
let v = Version::from_str("2.3.0-rc1").unwrap();
assert_eq!(v.pre_release.as_deref(), Some("rc1"));
}
#[test]
fn parses_double_digit_minor() {
let v = Version::from_str("1.11.0").unwrap();
assert_eq!((v.major, v.minor, v.patch), (1, 11, 0));
}
#[test]
fn strips_build_metadata() {
let v = Version::from_str("2.3.0+sha.abc").unwrap();
assert_eq!(v.pre_release, None);
}
#[test]
fn rejects_garbage() {
assert!(Version::from_str("not-a-version").is_err());
assert!(Version::from_str("1.0").is_err());
assert!(Version::from_str("1.0.0.0").is_err());
assert!(Version::from_str("1.0.0-").is_err());
}
#[test]
fn compare_release_numeric() {
assert_eq!(compare("1.11.0", "2.0.0").unwrap(), Ordering::Less);
assert_eq!(compare("2.1.1", "2.1.0").unwrap(), Ordering::Greater);
assert_eq!(compare("2.2.0", "2.2.0").unwrap(), Ordering::Equal);
assert_eq!(compare("1.11.0", "1.2.0").unwrap(), Ordering::Greater);
}
#[test]
fn compare_pre_release_vs_release() {
assert_eq!(compare("2.3.0-rc1", "2.3.0").unwrap(), Ordering::Less);
assert_eq!(compare("2.3.0", "2.3.0-rc1").unwrap(), Ordering::Greater);
}
#[test]
fn compare_pre_release_identifiers() {
assert_eq!(
compare("2.3.0-alpha", "2.3.0-beta").unwrap(),
Ordering::Less
);
assert_eq!(compare("2.3.0-rc.1", "2.3.0-rc.2").unwrap(), Ordering::Less);
assert_eq!(
compare("2.3.0-rc.1", "2.3.0-rc.alpha").unwrap(),
Ordering::Less
);
assert_eq!(compare("2.3.0-rc", "2.3.0-rc.1").unwrap(), Ordering::Less);
}
#[test]
fn ord_impl_sorts_correctly() {
let mut versions = vec![
Version::from_str("2.3.0").unwrap(),
Version::from_str("1.11.0").unwrap(),
Version::from_str("2.3.0-rc1").unwrap(),
Version::from_str("2.0.0").unwrap(),
];
versions.sort();
let rendered: Vec<String> = versions.iter().map(|v| v.to_string()).collect();
assert_eq!(rendered, vec!["1.11.0", "2.0.0", "2.3.0-rc1", "2.3.0"]);
}
}