version-compare 0.1.1

Rust library to easily compare version numbers with no specific format, and test against various comparison operators.
Documentation
//! Version module, which provides the `Version` struct as parsed version representation.
//!
//! Version numbers in the form of a string are parsed to a `Version` first, before any comparison
//! is made. This struct provides many methods and features for easy comparison, probing and other
//! things.

use std::borrow::Borrow;
use std::cmp::Ordering;
use std::fmt;
use std::iter::Peekable;
use std::slice::Iter;

use crate::{Cmp, Manifest, Part};

/// Version struct, wrapping a string, providing useful comparison functions.
///
/// A version in string format can be parsed using methods like `Version::from("1.2.3");`,
/// returning a `Result` with the parse result.
///
/// The original version string can be accessed using `version.as_str()`. A `Version` that isn't
/// derrived from a version string returns a generated string.
///
/// The struct provides many methods for easy comparison and probing.
///
/// # Examples
///
/// ```
/// use version_compare::{Version};
///
/// let ver = Version::from("1.2.3").unwrap();
/// ```
#[derive(Clone, Eq)]
pub struct Version<'a> {
    version: &'a str,
    parts: Vec<Part<'a>>,
    manifest: Option<&'a Manifest>,
}

impl<'a> Version<'a> {
    /// Create a `Version` instance from a version string.
    ///
    /// The version string should be passed to the `version` parameter.
    ///
    /// # Examples
    ///
    /// ```
    /// use version_compare::{Cmp, Version};
    ///
    /// let a = Version::from("1.2.3").unwrap();
    /// let b = Version::from("1.3.0").unwrap();
    ///
    /// assert_eq!(a.compare(b), Cmp::Lt);
    /// ```
    pub fn from(version: &'a str) -> Option<Self> {
        Some(Version {
            version,
            parts: split_version_str(version, None)?,
            manifest: None,
        })
    }

    /// Create a `Version` instance from already existing parts
    ///
    ///
    /// # Examples
    ///
    /// ```
    /// use version_compare::{Cmp, Version, Part};
    ///
    /// let ver = Version::from_parts("1.0", vec![Part::Number(1), Part::Number(0)]);
    /// ```
    pub fn from_parts(version: &'a str, parts: Vec<Part<'a>>) -> Self {
        Version {
            version,
            parts,
            manifest: None,
        }
    }

    /// Create a `Version` instance from a version string with the given `manifest`.
    ///
    /// The version string should be passed to the `version` parameter.
    ///
    /// # Examples
    ///
    /// ```
    /// use version_compare::{Cmp, Version, Manifest};
    ///
    /// let manifest = Manifest::default();
    /// let ver = Version::from_manifest("1.2.3", &manifest).unwrap();
    ///
    /// assert_eq!(ver.compare(Version::from("1.2.3").unwrap()), Cmp::Eq);
    /// ```
    pub fn from_manifest(version: &'a str, manifest: &'a Manifest) -> Option<Self> {
        Some(Version {
            version,
            parts: split_version_str(version, Some(manifest))?,
            manifest: Some(manifest),
        })
    }

    /// Get the version manifest, if available.
    ///
    /// # Examples
    ///
    /// ```
    /// use version_compare::Version;
    ///
    /// let version = Version::from("1.2.3").unwrap();
    ///
    /// if version.has_manifest() {
    ///     println!(
    ///         "Maximum version part depth is {} for this version",
    ///         version.manifest().unwrap().max_depth.unwrap_or(0),
    ///     );
    /// } else {
    ///     println!("Version has no manifest");
    /// }
    /// ```
    pub fn manifest(&self) -> Option<&Manifest> {
        self.manifest
    }

    /// Check whether this version has a manifest.
    ///
    /// # Examples
    ///
    /// ```
    /// use version_compare::Version;
    ///
    /// let version = Version::from("1.2.3").unwrap();
    ///
    /// if version.has_manifest() {
    ///     println!("This version does have a manifest");
    /// } else {
    ///     println!("This version does not have a manifest");
    /// }
    /// ```
    pub fn has_manifest(&self) -> bool {
        self.manifest().is_some()
    }

    /// Set the version manifest.
    ///
    /// # Examples
    ///
    /// ```
    /// use version_compare::{Version, Manifest};
    ///
    /// let manifest = Manifest::default();
    /// let mut version = Version::from("1.2.3").unwrap();
    ///
    /// version.set_manifest(Some(&manifest));
    /// ```
    pub fn set_manifest(&mut self, manifest: Option<&'a Manifest>) {
        self.manifest = manifest;

        // TODO: Re-parse the version string, because the manifest might have changed.
    }

    /// Get the original version string.
    ///
    /// # Examples
    ///
    /// ```
    /// use version_compare::Version;
    ///
    /// let ver = Version::from("1.2.3").unwrap();
    ///
    /// assert_eq!(ver.as_str(), "1.2.3");
    /// ```
    pub fn as_str(&self) -> &str {
        self.version
    }

    /// Get a specific version part by it's `index`.
    /// An error is returned if the given index is out of bound.
    ///
    /// # Examples
    ///
    /// ```
    /// use version_compare::{Version, Part};
    ///
    /// let ver = Version::from("1.2.3").unwrap();
    ///
    /// assert_eq!(ver.part(0), Ok(Part::Number(1)));
    /// assert_eq!(ver.part(1), Ok(Part::Number(2)));
    /// assert_eq!(ver.part(2), Ok(Part::Number(3)));
    /// ```
    #[allow(clippy::result_map_unit_fn)]
    pub fn part(&self, index: usize) -> Result<Part<'a>, ()> {
        // Make sure the index is in-bound
        if index >= self.parts.len() {
            return Err(());
        }

        Ok(self.parts[index])
    }

    /// Get a vector of all version parts.
    ///
    /// # Examples
    ///
    /// ```
    /// use version_compare::{Version, Part};
    ///
    /// let ver = Version::from("1.2.3").unwrap();
    ///
    /// assert_eq!(ver.parts(), [
    ///     Part::Number(1),
    ///     Part::Number(2),
    ///     Part::Number(3)
    /// ]);
    /// ```
    pub fn parts(&self) -> &[Part<'a>] {
        self.parts.as_slice()
    }

    /// Compare this version to the given `other` version.
    ///
    /// This method returns one of the following comparison operators:
    ///
    /// * `Lt`
    /// * `Eq`
    /// * `Gt`
    ///
    /// Other comparison operators can be used when comparing, but aren't returned by this method.
    ///
    /// # Examples:
    ///
    /// ```
    /// use version_compare::{Cmp, Version};
    ///
    /// let a = Version::from("1.2").unwrap();
    /// let b = Version::from("1.3.2").unwrap();
    ///
    /// assert_eq!(a.compare(&b), Cmp::Lt);
    /// assert_eq!(b.compare(&a), Cmp::Gt);
    /// assert_eq!(a.compare(&a), Cmp::Eq);
    /// ```
    pub fn compare<V>(&self, other: V) -> Cmp
    where
        V: Borrow<Version<'a>>,
    {
        compare_iter(
            self.parts.iter().peekable(),
            other.borrow().parts.iter().peekable(),
        )
    }

    /// Compare this version to the given `other` version,
    /// and check whether the given comparison operator is valid.
    ///
    /// All comparison operators can be used.
    ///
    /// # Examples:
    ///
    /// ```
    /// use version_compare::{Cmp, Version};
    ///
    /// let a = Version::from("1.2").unwrap();
    /// let b = Version::from("1.3.2").unwrap();
    ///
    /// assert!(a.compare_to(&b, Cmp::Lt));
    /// assert!(a.compare_to(&b, Cmp::Le));
    /// assert!(a.compare_to(&a, Cmp::Eq));
    /// assert!(a.compare_to(&a, Cmp::Le));
    /// ```
    pub fn compare_to<V>(&self, other: V, operator: Cmp) -> bool
    where
        V: Borrow<Version<'a>>,
    {
        match self.compare(other) {
            Cmp::Eq => matches!(operator, Cmp::Eq | Cmp::Le | Cmp::Ge),
            Cmp::Lt => matches!(operator, Cmp::Ne | Cmp::Lt | Cmp::Le),
            Cmp::Gt => matches!(operator, Cmp::Ne | Cmp::Gt | Cmp::Ge),
            _ => unreachable!(),
        }
    }
}

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

// Show just the version component parts as debug output
impl<'a> fmt::Debug for Version<'a> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        if f.alternate() {
            write!(f, "{:#?}", self.parts)
        } else {
            write!(f, "{:?}", self.parts)
        }
    }
}

/// Implement the partial ordering trait for the version struct, to easily allow version comparison.
impl<'a> PartialOrd for Version<'a> {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.compare(other).ord().unwrap())
    }
}

/// Implement the partial equality trait for the version struct, to easily allow version comparison.
impl<'a> PartialEq for Version<'a> {
    fn eq(&self, other: &Self) -> bool {
        self.compare_to(other, Cmp::Eq)
    }
}

/// Split the given version string, in it's version parts.
fn split_version_str<'a>(
    version: &'a str,
    manifest: Option<&'a Manifest>,
) -> Option<Vec<Part<'a>>> {
    // Split the version string, and create a vector to put the parts in
    let split = version.split(|c| !char::is_alphanumeric(c));
    let mut parts = Vec::new();

    // Get the manifest to follow
    let mut used_manifest = &Manifest::default();
    if let Some(m) = manifest {
        used_manifest = m;
    }

    // Loop over the parts, and parse them
    for part in split {
        // We may not go over the maximum depth
        if used_manifest.max_depth.is_some() && parts.len() >= used_manifest.max_depth.unwrap_or(0)
        {
            break;
        }

        // Skip empty parts
        if part.is_empty() {
            continue;
        }

        // Try to parse the value as an number
        match part.parse::<i32>() {
            Ok(number) => {
                // Push the number part to the vector
                parts.push(Part::Number(number));
            }
            Err(_) => {
                // Ignore text parts if specified
                if used_manifest.ignore_text {
                    continue;
                }

                // Numbers suffixed by text should be split into a number and text as well,
                // if the number overflows, handle it as text
                let split_at = part
                    .char_indices()
                    .take(part.len() - 1)
                    .take_while(|(_, c)| c.is_ascii_digit())
                    .map(|(i, c)| (i, c, part.chars().nth(i + 1).unwrap()))
                    .filter(|(_, _, b)| b.is_alphabetic())
                    .map(|(i, _, _)| i)
                    .next();
                if let Some(at) = split_at {
                    if let Ok(n) = part[..=at].parse() {
                        parts.push(Part::Number(n));
                        parts.push(Part::Text(&part[at + 1..]));
                    } else {
                        parts.push(Part::Text(part));
                    }
                    continue;
                }

                // Push the text part to the vector
                parts.push(Part::Text(part))
            }
        }
    }

    // The version must contain a number part if any part was parsed
    if !parts.is_empty() && !parts.iter().any(|p| matches!(p, Part::Number(_))) {
        return None;
    }

    // Return the list of parts
    Some(parts)
}

/// Compare two version numbers based on the iterators of their version parts.
///
/// This method returns one of the following comparison operators:
///
/// * `Lt`
/// * `Eq`
/// * `Gt`
///
/// Other comparison operators can be used when comparing, but aren't returned by this method.
fn compare_iter<'a>(
    mut iter: Peekable<Iter<Part<'a>>>,
    mut other_iter: Peekable<Iter<Part<'a>>>,
) -> Cmp {
    // Iterate through the parts of this version
    let mut other_part: Option<&Part>;

    // Iterate over the iterator, without consuming it
    for part in &mut iter {
        // Get the part for the other version
        other_part = other_iter.next();

        // If there are no parts left in the other version, try to determine the result
        if other_part.is_none() {
            // In the main version: if the current part is zero, continue to the next one
            match part {
                Part::Number(num) => {
                    if *num == 0 {
                        continue;
                    }
                }
                Part::Text(_) => return Cmp::Lt,
            }

            // The main version is greater
            return Cmp::Gt;
        }

        // Match both parts as numbers to destruct their numerical values
        if let Part::Number(num) = part {
            if let Part::Number(other) = other_part.unwrap() {
                // Compare the numbers
                match num {
                    n if n < other => return Cmp::Lt,
                    n if n > other => return Cmp::Gt,
                    _ => continue,
                }
            }
        }
        // Match both parts as strings
        else if let Part::Text(val) = part {
            if let Part::Text(other_val) = other_part.unwrap() {
                // normalize case
                let (val_lwr, other_val_lwr) = (val.to_lowercase(), other_val.to_lowercase());
                // compare text: for instance, "RC1" will be less than "RC2", so this works out.
                #[allow(clippy::comparison_chain)]
                if val_lwr < other_val_lwr {
                    return Cmp::Lt;
                } else if val_lwr > other_val_lwr {
                    return Cmp::Gt;
                }
            }
        }
    }

    // Check whether we should iterate over the other iterator, if it has any items left
    match other_iter.peek() {
        // Compare based on the other iterator
        Some(_) => compare_iter(other_iter, iter).flip(),

        // Nothing more to iterate over, the versions should be equal
        None => Cmp::Eq,
    }
}

#[cfg_attr(tarpaulin, skip)]
#[cfg(test)]
mod tests {
    use std::cmp;

    use crate::test::{COMBIS, VERSIONS, VERSIONS_ERROR};
    use crate::{Cmp, Manifest, Part};

    use super::Version;

    #[test]
    // TODO: This doesn't really test whether this method fully works
    fn from() {
        // Test whether parsing works for each test version
        for version in VERSIONS {
            assert!(Version::from(version.0).is_some());
        }

        // Test whether parsing works for each test invalid version
        for version in VERSIONS_ERROR {
            assert!(Version::from(version.0).is_none());
        }
    }

    #[test]
    // TODO: This doesn't really test whether this method fully works
    fn from_manifest() {
        // Create a manifest
        let manifest = Manifest::default();

        // Test whether parsing works for each test version
        for version in VERSIONS {
            assert_eq!(
                Version::from_manifest(version.0, &manifest)
                    .unwrap()
                    .manifest,
                Some(&manifest)
            );
        }

        // Test whether parsing works for each test invalid version
        for version in VERSIONS_ERROR {
            assert!(Version::from_manifest(version.0, &manifest).is_none());
        }
    }

    #[test]
    fn manifest() {
        let manifest = Manifest::default();
        let mut version = Version::from("1.2.3").unwrap();

        version.manifest = Some(&manifest);
        assert_eq!(version.manifest(), Some(&manifest));

        version.manifest = None;
        assert_eq!(version.manifest(), None);
    }

    #[test]
    fn has_manifest() {
        let manifest = Manifest::default();
        let mut version = Version::from("1.2.3").unwrap();

        version.manifest = Some(&manifest);
        assert!(version.has_manifest());

        version.manifest = None;
        assert!(!version.has_manifest());
    }

    #[test]
    fn set_manifest() {
        let manifest = Manifest::default();
        let mut version = Version::from("1.2.3").unwrap();

        version.set_manifest(Some(&manifest));
        assert_eq!(version.manifest, Some(&manifest));

        version.set_manifest(None);
        assert_eq!(version.manifest, None);
    }

    #[test]
    fn as_str() {
        // Test for each test version
        for version in VERSIONS {
            // The input version string must be the same as the returned string
            assert_eq!(Version::from(version.0).unwrap().as_str(), version.0);
        }
    }

    #[test]
    fn part() {
        // Test for each test version
        for version in VERSIONS {
            // Create a version object
            let ver = Version::from(version.0).unwrap();

            // Loop through each part
            for i in 0..version.1 {
                assert_eq!(ver.part(i), Ok(ver.parts[i]));
            }

            // A value outside the range must return an error
            assert!(ver.part(version.1).is_err());
        }
    }

    #[test]
    fn parts() {
        // Test for each test version
        for version in VERSIONS {
            // The number of parts must match
            assert_eq!(Version::from(version.0).unwrap().parts().len(), version.1);
        }
    }

    #[test]
    fn parts_max_depth() {
        // Create a manifest
        let mut manifest = Manifest::default();

        // Loop through a range of numbers
        for depth in 0..5 {
            // Set the maximum depth
            manifest.max_depth = if depth > 0 { Some(depth) } else { None };

            // Test for each test version with the manifest
            for version in VERSIONS {
                // Create a version object, and count it's parts
                let ver = Version::from_manifest(&version.0, &manifest);

                // Some versions might be none, because not all of the start with a number when the
                // maximum depth is 1. A version string with only text isn't allowed,
                // resulting in none.
                if ver.is_none() {
                    continue;
                }

                // Get the part count
                let count = ver.unwrap().parts().len();

                // The number of parts must match
                if depth == 0 {
                    assert_eq!(count, version.1);
                } else {
                    assert_eq!(count, cmp::min(version.1, depth));
                }
            }
        }
    }

    #[test]
    fn parts_ignore_text() {
        // Create a manifest
        let mut manifest = Manifest::default();

        // Try this for true and false
        for ignore in vec![true, false] {
            // Set to ignore text
            manifest.ignore_text = ignore;

            // Keep track whether any version passed with text
            let mut had_text = false;

            // Test each test version
            for version in VERSIONS {
                // Create a version instance, and get it's parts
                let ver = Version::from_manifest(&version.0, &manifest).unwrap();

                // Loop through all version parts
                for part in ver.parts() {
                    match part {
                        Part::Text(_) => {
                            // Set the flag
                            had_text = true;

                            // Break the loop if we already reached text when not ignored
                            if !ignore {
                                break;
                            }
                        }
                        _ => {}
                    }
                }
            }

            // Assert had text
            assert_eq!(had_text, !ignore);
        }
    }

    #[test]
    fn compare() {
        // Compare each version in the version set
        for entry in COMBIS {
            // Get both versions
            let a = Version::from(entry.0).unwrap();
            let b = Version::from(entry.1).unwrap();

            // Compare them
            assert_eq!(
                a.compare(b),
                entry.2.clone(),
                "Testing that {} is {} {}",
                entry.0,
                entry.2.sign(),
                entry.1,
            );
        }
    }

    #[test]
    fn compare_to() {
        // Compare each version in the version set
        for entry in COMBIS {
            // Get both versions
            let a = Version::from(entry.0).unwrap();
            let b = Version::from(entry.1).unwrap();

            // Test normally and inverse
            assert!(a.compare_to(&b, entry.2));
            assert!(!a.compare_to(b, entry.2.invert()));
        }

        // Assert an exceptional case, compare to not equal
        assert!(Version::from("1.2")
            .unwrap()
            .compare_to(Version::from("1.2.3").unwrap(), Cmp::Ne,));
    }

    #[test]
    fn display() {
        assert_eq!(format!("{}", Version::from("1.2.3").unwrap()), "1.2.3");
    }

    #[test]
    fn debug() {
        assert_eq!(
            format!("{:?}", Version::from("1.2.3").unwrap()),
            "[Number(1), Number(2), Number(3)]",
        );
        assert_eq!(
            format!("{:#?}", Version::from("1.2.3").unwrap()),
            "[\n    Number(\n        1,\n    ),\n    Number(\n        2,\n    ),\n    Number(\n        3,\n    ),\n]",
        );
    }

    #[test]
    fn partial_cmp() {
        // Compare each version in the version set
        for entry in COMBIS {
            // Get both versions
            let a = Version::from(entry.0).unwrap();
            let b = Version::from(entry.1).unwrap();

            // Compare and assert
            match entry.2 {
                Cmp::Eq => assert!(a == b),
                Cmp::Lt => assert!(a < b),
                Cmp::Gt => assert!(a > b),
                _ => {}
            }
        }
    }

    #[test]
    fn partial_eq() {
        // Compare each version in the version set
        for entry in COMBIS {
            // Skip entries that are less or equal, or greater or equal
            match entry.2 {
                Cmp::Le | Cmp::Ge => continue,
                _ => {}
            }

            // Get both versions
            let a = Version::from(entry.0).unwrap();
            let b = Version::from(entry.1).unwrap();

            // Determine what the result should be
            let result = match entry.2 {
                Cmp::Eq => true,
                _ => false,
            };

            // Test
            assert_eq!(a == b, result);
        }

        // Assert an exceptional case, compare to not equal
        assert!(Version::from("1.2").unwrap() != Version::from("1.2.3").unwrap());
    }
}