Search code examples
rustmacrostraits

Understanding Ord and PartialOrd macros


So, I have this working code:

use std::cmp::Ordering;
use std::fmt;

mod errors;
use errors::VersionParseError;

#[derive(Debug, Eq, PartialEq)]
pub struct Version {
    epoch: u64,
    version: String,
    release: u64,
}

impl Version {
    pub fn new(epoch: u64, version: impl Into<String>, release: u64) -> Self {
        Self {
            epoch,
            version: version.into(),
            release,
        }
    }

    pub fn from_string(version: impl Into<String>) -> Result<Self, VersionParseError> {
        let (epoch, version) = parse_epoch(&version.into())?;
        let (version, release) = parse_release(&version)?;
        Ok(Self::new(epoch, version, release))
    }
}

impl PartialOrd for Version {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        match self.epoch.cmp(&other.epoch) {
            Ordering::Equal => match compare(&self.version, &other.version) {
                Ordering::Equal => Some(self.release.cmp(&other.release)),
                result => Some(result),
            },
            result => Some(result),
        }
    }
}

impl Ord for Version {
    fn cmp(&self, other: &Self) -> Ordering {
        self.partial_cmp(other).unwrap()
    }
}

impl fmt::Display for Version {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if self.epoch == 0 {
            write!(f, "{}-{}", self.version, self.release)
        } else {
            write!(f, "{}:{}-{}", self.epoch, self.version, self.release)
        }
    }
}

fn parse_epoch(version: &String) -> Result<(u64, String), VersionParseError> {
    match version.split_once(':') {
        Some((epoch, remainder)) => match epoch.parse::<u64>() {
            Ok(epoch) => Ok((epoch, String::from(remainder))),
            Err(_) => Err(VersionParseError::InvalidEpoch),
        },
        None => Ok((0, String::from(version))),
    }
}

fn parse_release(version: &String) -> Result<(String, u64), VersionParseError> {
    match version.split_once('-') {
        Some((remainder, release)) => match release.parse::<u64>() {
            Ok(release) => Ok((String::from(remainder), release)),
            Err(_) => Err(VersionParseError::InvalidRelease),
        },
        None => Err(VersionParseError::NoReleaseSpecified),
    }
}

fn compare(lhs: &String, rhs: &String) -> Ordering {
    let l_segments = segments(lhs);
    let r_segments = segments(rhs);

    for (l_segment, r_segment) in l_segments.iter().zip(r_segments.iter()) {
        match compare_segments(l_segment, r_segment) {
            Ordering::Greater => {
                return Ordering::Greater;
            }
            Ordering::Less => {
                return Ordering::Less;
            }
            _ => {
                continue;
            }
        }
    }

    l_segments.len().cmp(&r_segments.len())
}

fn segments(version: &String) -> Vec<String> {
    normalize(version)
        .split(".")
        .map(|segment| String::from(segment))
        .collect()
}

fn normalize(version: &String) -> String {
    version
        .chars()
        .map(|chr| if chr.is_alphanumeric() { chr } else { '.' })
        .collect()
}

fn compare_segments(lhs: &str, rhs: &str) -> Ordering {
    let l_blocks = blocks(lhs);
    let r_blocks = blocks(rhs);
    let mut last: usize = 0;

    for (index, (l_block, r_block)) in l_blocks.iter().zip(r_blocks.iter()).enumerate() {
        last = index;

        match compare_blocks(l_block, r_block) {
            Ordering::Equal => {
                continue;
            }
            ordering => {
                return ordering;
            }
        }
    }

    match l_blocks
        .iter()
        .nth(last + 1)
        .unwrap_or(&String::new())
        .chars()
        .nth(0)
    {
        Some(chr) => {
            if chr.is_ascii_digit() {
                Ordering::Greater
            } else {
                Ordering::Less
            }
        }
        None => match r_blocks
            .iter()
            .nth(last + 1)
            .unwrap_or(&String::new())
            .chars()
            .nth(0)
        {
            Some(chr) => {
                if chr.is_ascii_digit() {
                    Ordering::Less
                } else {
                    Ordering::Greater
                }
            }
            None => Ordering::Equal,
        },
    }
}

fn blocks(segment: &str) -> Vec<String> {
    let mut result = Vec::new();
    let mut block = String::new();

    for chr in segment.chars() {
        match block.chars().nth(0) {
            Some(current) => {
                if same_type(&chr, &current) {
                    block.push(chr);
                } else {
                    result.push(block.clone());
                    block.clear();
                    block.push(chr);
                }
            }
            None => {
                block.push(chr);
            }
        }
    }

    if !block.is_empty() {
        result.push(block.clone());
    }

    result
}

fn same_type(lhs: &char, rhs: &char) -> bool {
    lhs.is_ascii_digit() == rhs.is_ascii_digit()
}

fn compare_blocks(lhs: &String, rhs: &String) -> Ordering {
    if lhs == rhs {
        return Ordering::Equal;
    }

    let l_is_number = lhs.chars().all(|chr| chr.is_ascii_digit());
    let r_is_number = rhs.chars().all(|chr| chr.is_ascii_digit());

    if l_is_number && r_is_number {
        compare_alpha(lhs, rhs)
    } else if l_is_number && !r_is_number {
        Ordering::Greater
    } else if !l_is_number && r_is_number {
        Ordering::Less
    } else {
        lhs.cmp(rhs)
    }
}

fn compare_alpha(lhs: &str, rhs: &str) -> Ordering {
    let lhs = lhs.trim_start_matches('0');
    let rhs = rhs.trim_start_matches('0');

    match lhs.len().cmp(&rhs.len()) {
        Ordering::Equal => lhs.cmp(&rhs),
        ordering => ordering,
    }
}

I also tried to get rid of the presumably redundant implementation of Ord and use the appropriate macro to derive the default. However, when I remove the implementation of Ord and add the macro to the derive list, the behaviour of the comparison operations (<, ==, >) changes. Why is that? What is the default implementation of cmp() derived by the Ord macro? I could not find it in the standard library, since it is a compiler built-in.

Since the implementation of partial_cmp() is complete, I also tried to implement Ord with cmp() analog to the current partial_cmp (except the Option) and derive PartialOrd with the appropriate macro. However, this also changes, i.e. breaks the behaviour of the comparison operators.

So my question is how Ord and PartialOrd play together and wether I really must implement both traits manually as I did above?

Addendum: Here's the complete project:

Cargo.toml:

`Cargo.toml`:
```toml
[package]
name = "librucman"
version = "0.1.1"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

[dev-dependencies]
once_cell = "*"

./tests/vercmp.rs:

use librucman::version::Version;
use std::cmp::Ordering;

mod common;
use common::{load_version_pair, EQUAL, GREATER_THAN, LESS_THAN, VERSIONS};

#[test]
fn version_parsing() {
    for (string, version) in VERSIONS.iter() {
        assert_eq!(*version, Version::from_string(*string).unwrap());
    }
}

#[test]
fn version_comparison() {
    for (lhs, rhs) in EQUAL.map(load_version_pair) {
        assert_eq!(lhs, rhs);
        assert_eq!(Ordering::Equal, lhs.cmp(&rhs))
    }

    for (lhs, rhs) in GREATER_THAN.map(load_version_pair) {
        assert!(lhs > rhs);
        assert_eq!(Ordering::Greater, lhs.cmp(&rhs))
    }

    for (lhs, rhs) in LESS_THAN.map(load_version_pair) {
        assert!(lhs < rhs);
        assert_eq!(Ordering::Less, lhs.cmp(&rhs))
    }
}

./tests/common/mod.rs:

use once_cell::sync::Lazy;
use std::collections::HashMap;

use librucman::version::Version;

pub static VERSIONS: Lazy<HashMap<&'static str, Version>> = Lazy::new(|| {
    HashMap::from([
        (
            "1:2.3.5+r3+gd9d61d87f-3",
            Version::new(1, "2.3.5+r3+gd9d61d87f", 3),
        ),
        ("2.28.3-1", Version::new(0, "2.28.3", 1)),
        ("2.3.2.post1-1", Version::new(0, "2.3.2.post1", 1)),
        ("20220913.f09bebf-1", Version::new(0, "20220913.f09bebf", 1)),
        (
            "2:2.06.r322.gd9b4638c5-4",
            Version::new(2, "2.06.r322.gd9b4638c5", 4),
        ),
        ("4.3-3", Version::new(0, "4.3", 3)),
        (
            "6.04.pre2.r11.gbf6db5b4-3",
            Version::new(0, "6.04.pre2.r11.gbf6db5b4", 3),
        ),
        ("7.4.3-1", Version::new(0, "7.4.3", 1)),
        ("r2322+3aebf69d-1", Version::new(0, "r2322+3aebf69d", 1)),
        ("0.4.4-1", Version::new(0, "0.4.4", 1)),
        ("2.14.2-363", Version::new(0, "2.14.2", 363)),
    ])
});

pub const EQUAL: [(&str, &str); 10] = [
    ("0.4.4-1", "0.4.4-1"),
    ("2.3.2.post1-1", "2.3.2.post1-1"),
    ("1:2.3.5+r3+gd9d61d87f-3", "1:2.3.5+r3+gd9d61d87f-3"),
    ("4.3-3", "4.3-3"),
    ("2.28.3-1", "2.28.3-1"),
    ("r2322+3aebf69d-1", "r2322+3aebf69d-1"),
    ("2:2.06.r322.gd9b4638c5-4", "2:2.06.r322.gd9b4638c5-4"),
    ("6.04.pre2.r11.gbf6db5b4-3", "6.04.pre2.r11.gbf6db5b4-3"),
    ("7.4.3-1", "7.4.3-1"),
    ("20220913.f09bebf-1", "20220913.f09bebf-1"),
];

pub const LESS_THAN: [(&str, &str); 48] = [
    ("0.4.4-1", "2.3.2.post1-1"),
    ("0.4.4-1", "1:2.3.5+r3+gd9d61d87f-3"),
    ("0.4.4-1", "4.3-3"),
    ("0.4.4-1", "2.28.3-1"),
    ("0.4.4-1", "2:2.06.r322.gd9b4638c5-4"),
    ("0.4.4-1", "6.04.pre2.r11.gbf6db5b4-3"),
    ("0.4.4-1", "7.4.3-1"),
    ("0.4.4-1", "20220913.f09bebf-1"),
    ("2.3.2.post1-1", "1:2.3.5+r3+gd9d61d87f-3"),
    ("2.3.2.post1-1", "4.3-3"),
    ("2.3.2.post1-1", "2.28.3-1"),
    ("2.3.2.post1-1", "2:2.06.r322.gd9b4638c5-4"),
    ("2.3.2.post1-1", "6.04.pre2.r11.gbf6db5b4-3"),
    ("2.3.2.post1-1", "7.4.3-1"),
    ("2.3.2.post1-1", "20220913.f09bebf-1"),
    ("1:2.3.5+r3+gd9d61d87f-3", "2:2.06.r322.gd9b4638c5-4"),
    ("4.3-3", "1:2.3.5+r3+gd9d61d87f-3"),
    ("4.3-3", "2:2.06.r322.gd9b4638c5-4"),
    ("4.3-3", "6.04.pre2.r11.gbf6db5b4-3"),
    ("4.3-3", "7.4.3-1"),
    ("4.3-3", "20220913.f09bebf-1"),
    ("2.28.3-1", "1:2.3.5+r3+gd9d61d87f-3"),
    ("2.28.3-1", "4.3-3"),
    ("2.28.3-1", "2:2.06.r322.gd9b4638c5-4"),
    ("2.28.3-1", "6.04.pre2.r11.gbf6db5b4-3"),
    ("2.28.3-1", "7.4.3-1"),
    ("2.28.3-1", "20220913.f09bebf-1"),
    ("r2322+3aebf69d-1", "0.4.4-1"),
    ("r2322+3aebf69d-1", "2.3.2.post1-1"),
    ("r2322+3aebf69d-1", "1:2.3.5+r3+gd9d61d87f-3"),
    ("r2322+3aebf69d-1", "4.3-3"),
    ("r2322+3aebf69d-1", "2.28.3-1"),
    ("r2322+3aebf69d-1", "2:2.06.r322.gd9b4638c5-4"),
    ("r2322+3aebf69d-1", "6.04.pre2.r11.gbf6db5b4-3"),
    ("r2322+3aebf69d-1", "7.4.3-1"),
    ("r2322+3aebf69d-1", "20220913.f09bebf-1"),
    ("6.04.pre2.r11.gbf6db5b4-3", "1:2.3.5+r3+gd9d61d87f-3"),
    ("6.04.pre2.r11.gbf6db5b4-3", "2:2.06.r322.gd9b4638c5-4"),
    ("6.04.pre2.r11.gbf6db5b4-3", "7.4.3-1"),
    ("6.04.pre2.r11.gbf6db5b4-3", "20220913.f09bebf-1"),
    ("7.4.3-1", "1:2.3.5+r3+gd9d61d87f-3"),
    ("7.4.3-1", "2:2.06.r322.gd9b4638c5-4"),
    ("7.4.3-1", "20220913.f09bebf-1"),
    ("20220913.f09bebf-1", "1:2.3.5+r3+gd9d61d87f-3"),
    ("20220913.f09bebf-1", "2:2.06.r322.gd9b4638c5-4"),
    ("41.1-2", "42beta+r14+g2d9d76c-2"),
    ("1.14.50-1", "2022d-1"),
    ("5.15.6+kde+r50-1", "5.15.6+kde+r177-1"),
];

pub const GREATER_THAN: [(&str, &str); 52] = [
    ("0.4.4-1", "r2322+3aebf69d-1"),
    ("2.3.2.post1-1", "0.4.4-1"),
    ("2.3.2.post1-1", "r2322+3aebf69d-1"),
    ("1:2.3.5+r3+gd9d61d87f-3", "0.4.4-1"),
    ("1:2.3.5+r3+gd9d61d87f-3", "2.3.2.post1-1"),
    ("1:2.3.5+r3+gd9d61d87f-3", "4.3-3"),
    ("1:2.3.5+r3+gd9d61d87f-3", "2.28.3-1"),
    ("1:2.3.5+r3+gd9d61d87f-3", "r2322+3aebf69d-1"),
    ("1:2.3.5+r3+gd9d61d87f-3", "6.04.pre2.r11.gbf6db5b4-3"),
    ("1:2.3.5+r3+gd9d61d87f-3", "7.4.3-1"),
    ("1:2.3.5+r3+gd9d61d87f-3", "20220913.f09bebf-1"),
    ("4.3-3", "0.4.4-1"),
    ("4.3-3", "2.3.2.post1-1"),
    ("4.3-3", "2.28.3-1"),
    ("4.3-3", "r2322+3aebf69d-1"),
    ("2.28.3-1", "0.4.4-1"),
    ("2.28.3-1", "2.3.2.post1-1"),
    ("2.28.3-1", "r2322+3aebf69d-1"),
    ("2:2.06.r322.gd9b4638c5-4", "0.4.4-1"),
    ("2:2.06.r322.gd9b4638c5-4", "2.3.2.post1-1"),
    ("2:2.06.r322.gd9b4638c5-4", "1:2.3.5+r3+gd9d61d87f-3"),
    ("2:2.06.r322.gd9b4638c5-4", "4.3-3"),
    ("2:2.06.r322.gd9b4638c5-4", "2.28.3-1"),
    ("2:2.06.r322.gd9b4638c5-4", "r2322+3aebf69d-1"),
    ("2:2.06.r322.gd9b4638c5-4", "6.04.pre2.r11.gbf6db5b4-3"),
    ("2:2.06.r322.gd9b4638c5-4", "7.4.3-1"),
    ("2:2.06.r322.gd9b4638c5-4", "20220913.f09bebf-1"),
    ("6.04.pre2.r11.gbf6db5b4-3", "0.4.4-1"),
    ("6.04.pre2.r11.gbf6db5b4-3", "2.3.2.post1-1"),
    ("6.04.pre2.r11.gbf6db5b4-3", "4.3-3"),
    ("6.04.pre2.r11.gbf6db5b4-3", "2.28.3-1"),
    ("6.04.pre2.r11.gbf6db5b4-3", "r2322+3aebf69d-1"),
    ("7.4.3-1", "0.4.4-1"),
    ("7.4.3-1", "2.3.2.post1-1"),
    ("7.4.3-1", "4.3-3"),
    ("7.4.3-1", "2.28.3-1"),
    ("7.4.3-1", "r2322+3aebf69d-1"),
    ("7.4.3-1", "6.04.pre2.r11.gbf6db5b4-3"),
    ("20220913.f09bebf-1", "0.4.4-1"),
    ("20220913.f09bebf-1", "2.3.2.post1-1"),
    ("20220913.f09bebf-1", "4.3-3"),
    ("20220913.f09bebf-1", "2.28.3-1"),
    ("20220913.f09bebf-1", "r2322+3aebf69d-1"),
    ("20220913.f09bebf-1", "6.04.pre2.r11.gbf6db5b4-3"),
    ("20220913.f09bebf-1", "7.4.3-1"),
    ("1.4rc5-14", "1.0.3.1-6"),
    ("2.38.0-2", "2.038ro+1.058it+1.018var-1"),
    ("1.21.0-1", "1.3-1"),
    ("0.8.2-5", "0.8-5"),
    ("3.2.13-1", "3.02a09-5"),
    ("9.0.2-1", "9.0p1-1"),
    ("3.2.2-2", "3.02a09-5"),
];

pub fn load_version_pair((lhs, rhs): (&str, &str)) -> (Version, Version) {
    (
        Version::from_string(lhs).unwrap(),
        Version::from_string(rhs).unwrap(),
    )
}

./src/version/errors.rs:

use std::fmt;

#[derive(Debug, Eq, PartialEq)]
pub enum VersionParseError {
    InvalidEpoch,
    InvalidRelease,
    NoReleaseSpecified,
}

impl VersionParseError {
    pub fn to_string(&self) -> &str {
        match self {
            Self::InvalidEpoch => "invalid epoch",
            Self::InvalidRelease => "invalid release",
            Self::NoReleaseSpecified => "no release specified",
        }
    }
}

impl fmt::Display for VersionParseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.to_string())
    }
}

./src/version.rs:

use std::cmp::Ordering;
use std::fmt;

mod errors;
use errors::VersionParseError;

#[derive(Debug, Eq, PartialEq)]
pub struct Version {
    epoch: u64,
    version: String,
    release: u64,
}

impl Version {
    pub fn new(epoch: u64, version: impl Into<String>, release: u64) -> Self {
        Self {
            epoch,
            version: version.into(),
            release,
        }
    }

    pub fn from_string(version: impl Into<String>) -> Result<Self, VersionParseError> {
        let (epoch, version) = parse_epoch(&version.into())?;
        let (version, release) = parse_release(&version)?;
        Ok(Self::new(epoch, version, release))
    }
}

impl PartialOrd for Version {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        match self.epoch.cmp(&other.epoch) {
            Ordering::Equal => match compare(&self.version, &other.version) {
                Ordering::Equal => Some(self.release.cmp(&other.release)),
                result => Some(result),
            },
            result => Some(result),
        }
    }
}

impl Ord for Version {
    fn cmp(&self, other: &Self) -> Ordering {
        self.partial_cmp(other).unwrap()
    }
}

impl fmt::Display for Version {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if self.epoch == 0 {
            write!(f, "{}-{}", self.version, self.release)
        } else {
            write!(f, "{}:{}-{}", self.epoch, self.version, self.release)
        }
    }
}

fn parse_epoch(version: &String) -> Result<(u64, String), VersionParseError> {
    match version.split_once(':') {
        Some((epoch, remainder)) => match epoch.parse::<u64>() {
            Ok(epoch) => Ok((epoch, String::from(remainder))),
            Err(_) => Err(VersionParseError::InvalidEpoch),
        },
        None => Ok((0, String::from(version))),
    }
}

fn parse_release(version: &String) -> Result<(String, u64), VersionParseError> {
    match version.split_once('-') {
        Some((remainder, release)) => match release.parse::<u64>() {
            Ok(release) => Ok((String::from(remainder), release)),
            Err(_) => Err(VersionParseError::InvalidRelease),
        },
        None => Err(VersionParseError::NoReleaseSpecified),
    }
}

fn compare(lhs: &String, rhs: &String) -> Ordering {
    let l_segments = segments(lhs);
    let r_segments = segments(rhs);

    for (l_segment, r_segment) in l_segments.iter().zip(r_segments.iter()) {
        match compare_segments(l_segment, r_segment) {
            Ordering::Greater => {
                return Ordering::Greater;
            }
            Ordering::Less => {
                return Ordering::Less;
            }
            _ => {
                continue;
            }
        }
    }

    l_segments.len().cmp(&r_segments.len())
}

fn segments(version: &String) -> Vec<String> {
    normalize(version)
        .split(".")
        .map(|segment| String::from(segment))
        .collect()
}

fn normalize(version: &String) -> String {
    version
        .chars()
        .map(|chr| if chr.is_alphanumeric() { chr } else { '.' })
        .collect()
}

fn compare_segments(lhs: &str, rhs: &str) -> Ordering {
    let l_blocks = blocks(lhs);
    let r_blocks = blocks(rhs);
    let mut last: usize = 0;

    for (index, (l_block, r_block)) in l_blocks.iter().zip(r_blocks.iter()).enumerate() {
        last = index;

        match compare_blocks(l_block, r_block) {
            Ordering::Equal => {
                continue;
            }
            ordering => {
                return ordering;
            }
        }
    }

    match l_blocks
        .iter()
        .nth(last + 1)
        .unwrap_or(&String::new())
        .chars()
        .nth(0)
    {
        Some(chr) => {
            if chr.is_ascii_digit() {
                Ordering::Greater
            } else {
                Ordering::Less
            }
        }
        None => match r_blocks
            .iter()
            .nth(last + 1)
            .unwrap_or(&String::new())
            .chars()
            .nth(0)
        {
            Some(chr) => {
                if chr.is_ascii_digit() {
                    Ordering::Less
                } else {
                    Ordering::Greater
                }
            }
            None => Ordering::Equal,
        },
    }
}

fn blocks(segment: &str) -> Vec<String> {
    let mut result = Vec::new();
    let mut block = String::new();

    for chr in segment.chars() {
        match block.chars().nth(0) {
            Some(current) => {
                if same_type(&chr, &current) {
                    block.push(chr);
                } else {
                    result.push(block.clone());
                    block.clear();
                    block.push(chr);
                }
            }
            None => {
                block.push(chr);
            }
        }
    }

    if !block.is_empty() {
        result.push(block.clone());
    }

    result
}

fn same_type(lhs: &char, rhs: &char) -> bool {
    lhs.is_ascii_digit() == rhs.is_ascii_digit()
}

fn compare_blocks(lhs: &String, rhs: &String) -> Ordering {
    if lhs == rhs {
        return Ordering::Equal;
    }

    let l_is_number = lhs.chars().all(|chr| chr.is_ascii_digit());
    let r_is_number = rhs.chars().all(|chr| chr.is_ascii_digit());

    if l_is_number && r_is_number {
        compare_alpha(lhs, rhs)
    } else if l_is_number && !r_is_number {
        Ordering::Greater
    } else if !l_is_number && r_is_number {
        Ordering::Less
    } else {
        lhs.cmp(rhs)
    }
}

fn compare_alpha(lhs: &str, rhs: &str) -> Ordering {
    let lhs = lhs.trim_start_matches('0');
    let rhs = rhs.trim_start_matches('0');

    match lhs.len().cmp(&rhs.len()) {
        Ordering::Equal => lhs.cmp(&rhs),
        ordering => ordering,
    }
}

./src/lib.rs:

extern crate core;

pub mod version;

Solution

  • I used https://play.rust-lang.org/ to have a look at the expanded macros (Tools -> Expand macros).

    The auto implementation for PartialOrd is:

    #[automatically_derived]
    impl ::core::cmp::PartialOrd for Version {
        #[inline]
        fn partial_cmp(&self, other: &Version)
            -> ::core::option::Option<::core::cmp::Ordering> {
            match ::core::cmp::PartialOrd::partial_cmp(&self.epoch, &other.epoch)
                {
                ::core::option::Option::Some(::core::cmp::Ordering::Equal) =>
                    match ::core::cmp::PartialOrd::partial_cmp(&self.version,
                            &other.version) {
                        ::core::option::Option::Some(::core::cmp::Ordering::Equal)
                            =>
                            ::core::cmp::PartialOrd::partial_cmp(&self.release,
                                &other.release),
                        cmp => cmp,
                    },
                cmp => cmp,
            }
        }
    }
    

    and for Ord:

    #[automatically_derived]
    impl ::core::cmp::Ord for Version {
        #[inline]
        fn cmp(&self, other: &Version) -> ::core::cmp::Ordering {
            match ::core::cmp::Ord::cmp(&self.epoch, &other.epoch) {
                ::core::cmp::Ordering::Equal =>
                    match ::core::cmp::Ord::cmp(&self.version, &other.version) {
                        ::core::cmp::Ordering::Equal =>
                            ::core::cmp::Ord::cmp(&self.release, &other.release),
                        cmp => cmp,
                    },
                cmp => cmp,
            }
        }
    }
    

    This explains, why the (actually sane) default implementations break my test cases.

    To prevent this, I will wrap the version string in a custom struct and implement cmp() for it. This should solve my issue.