given 0.1.6

A tool for interactively updating your Scala dependencies.
Documentation
use std::{cmp::Ordering, fmt::Display};

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Version {
    SemVer {
        major: u32,
        minor: u32,
        patch: u32,
        pre_release: Option<PreRelease>,
    },
    Other(String),
}

impl Display for Version {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Version::SemVer {
                major,
                minor,
                patch,
                pre_release,
            } => {
                if let Some(pre_release) = pre_release {
                    write!(f, "{}.{}.{}-{}", major, minor, patch, pre_release)
                } else {
                    write!(f, "{}.{}.{}", major, minor, patch)
                }
            }
            Version::Other(s) => write!(f, "{}", s),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum PreRelease {
    RC(u32),
    M(u32),
    Other(String),
}

impl Display for PreRelease {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            PreRelease::RC(v) => write!(f, "RC{}", v),
            PreRelease::M(v) => write!(f, "M{}", v),
            PreRelease::Other(s) => write!(f, "{}", s),
        }
    }
}

impl Ord for PreRelease {
    fn cmp(&self, other: &Self) -> Ordering {
        use PreRelease::*;
        match (self, other) {
            (RC(v1), RC(v2)) => v1.cmp(v2),
            (M(v1), M(v2)) => v1.cmp(v2),
            (RC(_), _) => Ordering::Greater,
            (_, RC(_)) => Ordering::Less,
            (M(_), _) => Ordering::Greater,
            (_, M(_)) => Ordering::Less,
            (Other(s1), Other(s2)) => s1.cmp(s2),
        }
    }
}

impl PartialOrd for PreRelease {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Version {
    pub fn new(value: &str) -> Self {
        if let Some((major, minor, patch, pre_release)) = Self::parse_semver(value) {
            Version::SemVer {
                major,
                minor,
                patch,
                pre_release,
            }
        } else {
            Version::Other(value.to_string())
        }
    }

    pub fn major(&self) -> Option<u32> {
        match self {
            Version::SemVer { major, .. } => Some(*major),
            Version::Other(_) => None,
        }
    }

    pub fn minor(&self) -> Option<u32> {
        match self {
            Version::SemVer { minor, .. } => Some(*minor),
            Version::Other(_) => None,
        }
    }

    pub fn patch(&self) -> Option<u32> {
        match self {
            Version::SemVer { patch, .. } => Some(*patch),
            Version::Other(_) => None,
        }
    }

    pub fn pre_release(&self) -> Option<PreRelease> {
        match self {
            Version::SemVer { pre_release, .. } => pre_release.clone(),
            Version::Other(_) => None,
        }
    }

    pub fn is_pre_release(&self) -> bool {
        matches!(
            self,
            Version::SemVer {
                pre_release: Some(_),
                ..
            } | Version::Other(_)
        )
    }

    fn parse_semver(value: &str) -> Option<(u32, u32, u32, Option<PreRelease>)> {
        let parts: Vec<&str> = value.splitn(3, '.').collect();
        match parts.len() {
            2 => {
                let major = parts[0].parse().ok()?;
                let minor_parts: Vec<&str> = parts[1].splitn(2, '-').collect();
                let minor = minor_parts[0].parse().ok()?;
                let pre_release = if minor_parts.len() > 1 {
                    Some(Self::parse_pre_release(minor_parts[1]))
                } else {
                    None
                };
                Some((major, minor, 0, pre_release))
            }
            3 => {
                let major = parts[0].parse().ok()?;
                let minor = parts[1].parse().ok()?;
                let patch_parts: Vec<&str> = parts[2].splitn(2, '-').collect();
                let patch = patch_parts[0].parse().ok()?;
                let pre_release = if patch_parts.len() > 1 {
                    Some(Self::parse_pre_release(patch_parts[1]))
                } else {
                    None
                };
                Some((major, minor, patch, pre_release))
            }
            _ => None,
        }
    }

    fn parse_pre_release(value: &str) -> PreRelease {
        if let Some(num) = value.strip_prefix("RC") {
            PreRelease::RC(num.parse().unwrap_or(0))
        } else if let Some(num) = value.strip_prefix('M') {
            PreRelease::M(num.parse().unwrap_or(0))
        } else {
            PreRelease::Other(value.to_string())
        }
    }
}

impl Ord for Version {
    fn cmp(&self, other: &Self) -> Ordering {
        match (self, other) {
            (
                Version::SemVer {
                    major: m1,
                    minor: n1,
                    patch: p1,
                    pre_release: pr1,
                },
                Version::SemVer {
                    major: m2,
                    minor: n2,
                    patch: p2,
                    pre_release: pr2,
                },
            ) => {
                let base_cmp = (m1, n1, p1).cmp(&(m2, n2, p2));
                if base_cmp == Ordering::Equal {
                    match (pr1, pr2) {
                        (Some(pr1), Some(pr2)) => pr1.cmp(pr2),
                        (Some(_), None) => Ordering::Less,
                        (None, Some(_)) => Ordering::Greater,
                        (None, None) => Ordering::Equal,
                    }
                } else {
                    base_cmp
                }
            }
            (Version::SemVer { .. }, Version::Other(_)) => Ordering::Less,
            (Version::Other(_), Version::SemVer { .. }) => Ordering::Greater,
            (Version::Other(v1), Version::Other(v2)) => v1.cmp(v2),
        }
    }
}

impl PartialOrd for Version {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use itertools::Itertools;
    use pretty_assertions::assert_eq;

    #[test]
    fn test_version_parsing() {
        let parsing_expectations = vec![
            (
                "1.0.0",
                Version::SemVer {
                    major: 1,
                    minor: 0,
                    patch: 0,
                    pre_release: None,
                },
            ),
            (
                "1.0.0-RC4",
                Version::SemVer {
                    major: 1,
                    minor: 0,
                    patch: 0,
                    pre_release: Some(PreRelease::RC(4)),
                },
            ),
            (
                "2.0-RC4",
                Version::SemVer {
                    major: 2,
                    minor: 0,
                    patch: 0,
                    pre_release: Some(PreRelease::RC(4)),
                },
            ),
            (
                "1.5.5-M1",
                Version::SemVer {
                    major: 1,
                    minor: 5,
                    patch: 5,
                    pre_release: Some(PreRelease::M(1)),
                },
            ),
            ("4.5.5.5", Version::Other("4.5.5.5".to_string())),
            ("i-hate-semver", Version::Other("i-hate-semver".to_string())),
            (
                "1.1.1-alpha",
                Version::SemVer {
                    major: 1,
                    minor: 1,
                    patch: 1,
                    pre_release: Some(PreRelease::Other("alpha".to_string())),
                },
            ),
            (
                "1.1.1-alpha.5",
                Version::SemVer {
                    major: 1,
                    minor: 1,
                    patch: 1,
                    pre_release: Some(PreRelease::Other("alpha.5".to_string())),
                },
            ),
            (
                "1.1.1-beta",
                Version::SemVer {
                    major: 1,
                    minor: 1,
                    patch: 1,
                    pre_release: Some(PreRelease::Other("beta".to_string())),
                },
            ),
            (
                "1.1.1-beta.5",
                Version::SemVer {
                    major: 1,
                    minor: 1,
                    patch: 1,
                    pre_release: Some(PreRelease::Other("beta.5".to_string())),
                },
            ),
        ];

        for (input, expected) in parsing_expectations {
            assert_eq!(Version::new(input), expected);
        }
    }

    #[test]
    fn test_version_ordering() {
        let ordering_expectations = vec![
            (
                vec!["1.1.1", "2.0.0", "0.0.5"],
                vec!["0.0.5", "1.1.1", "2.0.0"],
            ),
            (
                vec![
                    "0.5.0",
                    "0.1.0-RC12",
                    "0.1.0-RC13",
                    "1.0.0",
                    "1.0.0-RC2",
                    "1.0.0-RC1",
                    "1.0.0-M1",
                    "1.0.0-alpha",
                    "1.0.0-alpha.1",
                    "2.0.0",
                    "2.1.0",
                    "2.0.1",
                ],
                vec![
                    "0.1.0-RC12",
                    "0.1.0-RC13",
                    "0.5.0",
                    "1.0.0-alpha",
                    "1.0.0-alpha.1",
                    "1.0.0-M1",
                    "1.0.0-RC1",
                    "1.0.0-RC2",
                    "1.0.0",
                    "2.0.0",
                    "2.0.1",
                    "2.1.0",
                ],
            ),
        ];

        for (input, expected) in ordering_expectations {
            let input_versions: Vec<Version> = input.iter().map(|v| Version::new(v)).collect();
            let expected_versions: Vec<Version> =
                expected.iter().map(|v| Version::new(v)).collect();
            assert_eq!(
                input_versions.into_iter().sorted().collect::<Vec<_>>(),
                expected_versions
            );
        }
    }

    #[test]
    fn test_pre_release_ordering() {
        let pre_releases = vec![
            PreRelease::Other("alpha".to_string()),
            PreRelease::Other("alpha.1".to_string()),
            PreRelease::M(1),
            PreRelease::RC(1),
            PreRelease::RC(2),
        ];

        let expected_order = vec![
            PreRelease::Other("alpha".to_string()),
            PreRelease::Other("alpha.1".to_string()),
            PreRelease::M(1),
            PreRelease::RC(1),
            PreRelease::RC(2),
        ];

        let mut sorted_pre_releases = pre_releases.clone();
        sorted_pre_releases.sort();

        assert_eq!(sorted_pre_releases, expected_order);
    }
}