Import version-compare 0.1.1 upstream upstream/0.1.1
authorDongHun Kwak <dh0128.kwak@samsung.com>
Thu, 6 Apr 2023 01:10:07 +0000 (10:10 +0900)
committerDongHun Kwak <dh0128.kwak@samsung.com>
Thu, 6 Apr 2023 01:10:07 +0000 (10:10 +0900)
15 files changed:
.cargo_vcs_info.json [new file with mode: 0644]
Cargo.lock [new file with mode: 0644]
Cargo.toml [new file with mode: 0644]
Cargo.toml.orig [new file with mode: 0644]
LICENSE [new file with mode: 0644]
README.md [new file with mode: 0644]
examples/example.rs [new file with mode: 0644]
examples/minimal.rs [new file with mode: 0644]
src/cmp.rs [new file with mode: 0644]
src/compare.rs [new file with mode: 0644]
src/lib.rs [new file with mode: 0644]
src/manifest.rs [new file with mode: 0644]
src/part.rs [new file with mode: 0644]
src/test.rs [new file with mode: 0644]
src/version.rs [new file with mode: 0644]

diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json
new file mode 100644 (file)
index 0000000..edf75a9
--- /dev/null
@@ -0,0 +1,6 @@
+{
+  "git": {
+    "sha1": "9a965b448749892c921808946420d6372de0958f"
+  },
+  "path_in_vcs": ""
+}
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644 (file)
index 0000000..b356177
--- /dev/null
@@ -0,0 +1,5 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+[[package]]
+name = "version-compare"
+version = "0.1.1"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644 (file)
index 0000000..64f001a
--- /dev/null
@@ -0,0 +1,39 @@
+# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g., crates.io) dependencies.
+#
+# If you are reading this file be aware that the original Cargo.toml
+# will likely look very different (and much more reasonable).
+# See Cargo.toml.orig for the original contents.
+
+[package]
+edition = "2018"
+name = "version-compare"
+version = "0.1.1"
+authors = ["Tim Visee <3a4fb3964f@sinenomine.email>"]
+include = [
+    "/src",
+    "/examples",
+    "Cargo.toml",
+    "LICENSE",
+    "README.md",
+]
+description = "Rust library to easily compare version numbers with no specific format, and test against various comparison operators."
+homepage = "https://timvisee.com/projects/version-compare/"
+documentation = "https://docs.rs/version-compare"
+readme = "README.md"
+keywords = [
+    "version",
+    "compare",
+    "comparison",
+    "comparing",
+]
+categories = [
+    "parser-implementations",
+    "parsing",
+]
+license = "MIT"
+repository = "https://gitlab.com/timvisee/version-compare"
diff --git a/Cargo.toml.orig b/Cargo.toml.orig
new file mode 100644 (file)
index 0000000..68fa5c4
--- /dev/null
@@ -0,0 +1,14 @@
+[package]
+name = "version-compare"
+version = "0.1.1"
+authors = ["Tim Visee <3a4fb3964f@sinenomine.email>"]
+license = "MIT"
+readme = "README.md"
+homepage = "https://timvisee.com/projects/version-compare/"
+repository = "https://gitlab.com/timvisee/version-compare"
+documentation = "https://docs.rs/version-compare"
+description = "Rust library to easily compare version numbers with no specific format, and test against various comparison operators."
+keywords = ["version", "compare", "comparison", "comparing"]
+categories = ["parser-implementations", "parsing"]
+edition = "2018"
+include = ["/src", "/examples", "Cargo.toml", "LICENSE", "README.md"]
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..4a49e78
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2017 Tim Visée
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..602992f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,137 @@
+[![Build status on GitLab CI][gitlab-ci-master-badge]][gitlab-ci-link]
+[![Crate version][crate-version-badge]][crate-link]
+[![Documentation][docs-badge]][docs]
+[![Download statistics][crate-download-badge]][crate-link]
+[![Coverage status][coverage-badge]][coverage-link]
+[![Dependencies][dependency-badge]][crate-link]
+[![License][crate-license-badge]][crate-link]
+
+[coverage-badge]: https://gitlab.com/timvisee/version-compare/badges/master/coverage.svg
+[coverage-link]: https://coveralls.io/gitlab/timvisee/version-compare
+[crate-download-badge]: https://img.shields.io/crates/d/version-compare.svg
+[crate-license-badge]: https://img.shields.io/crates/l/version-compare.svg
+[crate-link]: https://crates.io/crates/version-compare
+[crate-version-badge]: https://img.shields.io/crates/v/version-compare.svg
+[dependency-badge]: https://img.shields.io/badge/dependencies-none!-green.svg
+[docs-badge]: https://img.shields.io/docsrs/version-compare
+[docs]: https://docs.rs/version-compare
+[gitlab-ci-link]: https://gitlab.com/timvisee/version-compare/pipelines
+[gitlab-ci-master-badge]: https://gitlab.com/timvisee/version-compare/badges/master/pipeline.svg
+
+# Rust library: version-compare
+
+> Rust library to easily compare version numbers with no specific format, and test against various comparison operators.
+
+Comparing version numbers is hard, especially with weird version number formats.
+
+This library helps you to easily compare any kind of version number with no
+specific format using a best-effort approach.
+Two version numbers can be compared to each other to get a comparison operator
+(`<`, `==`, `>`), or test them against a comparison operator.
+
+Along with version comparison, the library provides various other tools for
+working with version numbers.
+
+Inspired by PHPs [version_compare()](http://php.net/manual/en/function.version-compare.php).
+
+_Note: Still a work in progress. Configurability is currently very limited. Things will change._
+
+### Formats
+
+Version numbers that would parse successfully include:  
+`1`, `3.10.4.1`, `1.2.alpha`, `1.2.dev.4`, ` `, ` .   -32 . 1`, `MyApp 3.2.0 / build 0932` ...
+
+See a list of how version numbers compare [here](https://github.com/timvisee/version-compare/blob/411ed7135741ed7cf2fcf4919012fb5412dc122b/src/test.rs#L50-L103).
+
+## Example
+
+This library is very easy to use. Here's a basic usage example:
+
+`Cargo.toml`:
+```toml
+[dependencies]
+version-compare = "0.1"
+```
+
+[`example.rs`](examples/example.rs):
+```rust
+use version_compare::{compare, compare_to, Cmp, Version};
+
+fn main() {
+    let a = "1.2";
+    let b = "1.5.1";
+
+    // The following comparison operators are used:
+    // - Cmp::Eq -> Equal
+    // - Cmp::Ne -> Not equal
+    // - Cmp::Lt -> Less than
+    // - Cmp::Le -> Less than or equal
+    // - Cmp::Ge -> Greater than or equal
+    // - Cmp::Gt -> Greater than
+
+    // Easily compare version strings
+    assert_eq!(compare(a, b), Ok(Cmp::Lt));
+    assert_eq!(compare_to(a, b, Cmp::Le), Ok(true));
+    assert_eq!(compare_to(a, b, Cmp::Gt), Ok(false));
+
+    // Parse and wrap version strings as a Version
+    let a = Version::from(a).unwrap();
+    let b = Version::from(b).unwrap();
+
+    // The Version can easily be compared with
+    assert_eq!(a < b, true);
+    assert_eq!(a <= b, true);
+    assert_eq!(a > b, false);
+    assert_eq!(a != b, true);
+    assert_eq!(a.compare(&b), Cmp::Lt);
+    assert_eq!(a.compare_to(&b, Cmp::Lt), true);
+
+    // Or match the comparison operators
+    match a.compare(b) {
+        Cmp::Lt => println!("Version a is less than b"),
+        Cmp::Eq => println!("Version a is equal to b"),
+        Cmp::Gt => println!("Version a is greater than b"),
+        _ => unreachable!(),
+    }
+}
+```
+
+See the [`examples`](examples) directory for more.
+
+## Features
+
+* Compare version numbers, get: `<`, `==`, `>`
+* Compare against a comparison operator
+  (`<`, `<=`, `==`, `!=`, `>=`, `>`)
+* Parse complex and unspecified formats
+* Static, standalone methods to easily compare version strings in a single line
+  of code
+
+#### Future ideas
+
+* Version ranges
+* Support for [npm-style](https://semver.npmjs.com/) operators (e.g. `^1.0` or `~1.0`)
+* Manifest: extend `Manifest` for to support a wide set of constraints
+* Building blocks for building your own specific version number parser
+* Batch comparisons
+
+#### Semver
+
+Version numbers using the [semver](http://semver.org/) format are compared
+correctly with no additional configuration.
+
+If your version number strings follow this exact format you may be better off
+using the [`semver`](https://crates.io/crates/semver) crate for more format
+specific features.
+
+If that isn't certain however, `version-compare` makes comparing a breeze.
+
+## Builds
+
+This library is automatically build and tested every day and for each commit using CI services.
+
+See the current status here: https://gitlab.com/timvisee/version-compare/-/pipelines
+
+## License
+
+This project is released under the MIT license. Check out the [LICENSE](LICENSE) file for more information.
diff --git a/examples/example.rs b/examples/example.rs
new file mode 100644 (file)
index 0000000..6859f06
--- /dev/null
@@ -0,0 +1,46 @@
+//! Usage examples of the version-compare crate.
+//!
+//! This shows various ways this library provides for comparing version numbers.
+//! The `assert_eq!(...)` macros are used to assert and show the expected output.
+//!
+//! Run this example by invoking `cargo run --example example`.
+
+use version_compare::{compare, compare_to, Cmp, Version};
+
+fn main() {
+    let a = "1.2";
+    let b = "1.5.1";
+
+    // The following comparison operators are used:
+    // - Cmp::Eq -> Equal
+    // - Cmp::Ne -> Not equal
+    // - Cmp::Lt -> Less than
+    // - Cmp::Le -> Less than or equal
+    // - Cmp::Ge -> Greater than or equal
+    // - Cmp::Gt -> Greater than
+
+    // Easily compare version strings
+    assert_eq!(compare(a, b), Ok(Cmp::Lt));
+    assert_eq!(compare_to(a, b, Cmp::Le), Ok(true));
+    assert_eq!(compare_to(a, b, Cmp::Gt), Ok(false));
+
+    // Parse and wrap version strings as a Version
+    let a = Version::from(a).unwrap();
+    let b = Version::from(b).unwrap();
+
+    // The Version can easily be compared with
+    assert_eq!(a < b, true);
+    assert_eq!(a <= b, true);
+    assert_eq!(a > b, false);
+    assert_eq!(a != b, true);
+    assert_eq!(a.compare(&b), Cmp::Lt);
+    assert_eq!(a.compare_to(&b, Cmp::Lt), true);
+
+    // Or match the comparison operators
+    match a.compare(b) {
+        Cmp::Lt => println!("Version a is less than b"),
+        Cmp::Eq => println!("Version a is equal to b"),
+        Cmp::Gt => println!("Version a is greater than b"),
+        _ => unreachable!(),
+    }
+}
diff --git a/examples/minimal.rs b/examples/minimal.rs
new file mode 100644 (file)
index 0000000..37413c8
--- /dev/null
@@ -0,0 +1,19 @@
+//! A minimal usage example of the version-compare crate.
+//!
+//! This compares two given version number strings, and outputs which is greater.
+//!
+//! Run this example by invoking `cargo run --example minimal`.
+
+use version_compare::{compare, Cmp};
+
+fn main() {
+    let a = "1.3";
+    let b = "1.2.4";
+
+    match compare(a, b) {
+        Ok(Cmp::Lt) => println!("Version a is less than b"),
+        Ok(Cmp::Eq) => println!("Version a is equal to b"),
+        Ok(Cmp::Gt) => println!("Version a is greater than b"),
+        _ => panic!("Invalid version number"),
+    }
+}
diff --git a/src/cmp.rs b/src/cmp.rs
new file mode 100644 (file)
index 0000000..1e3ef2f
--- /dev/null
@@ -0,0 +1,436 @@
+//! Module with all supported comparison operators.
+//!
+//! This module provides an enum with all comparison operators that can be used with this library.
+//! The enum provides various useful helper functions to inverse or flip an operator.
+//!
+//! Methods like `Cmp::from_sign(">");` can be used to get a comparison operator by it's logical
+//! sign from a string.
+
+use std::cmp::Ordering;
+
+/// Comparison operators enum.
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
+pub enum Cmp {
+    /// Equal (`==`, `=`).
+    /// When version `A` is equal to `B`.
+    Eq,
+
+    /// Not equal (`!=`, `!`, `<>`).
+    /// When version `A` is not equal to `B`.
+    Ne,
+
+    /// Less than (`<`).
+    /// When version `A` is less than `B` but not equal.
+    Lt,
+
+    /// Less or equal (`<=`).
+    /// When version `A` is less than or equal to `B`.
+    Le,
+
+    /// Greater or equal (`>=`).
+    /// When version `A` is greater than or equal to `B`.
+    Ge,
+
+    /// Greater than (`>`).
+    /// When version `A` is greater than `B` but not equal.
+    Gt,
+}
+
+impl Cmp {
+    /// Get a comparison operator by it's sign.
+    /// Whitespaces are stripped from the sign string.
+    /// An error is returned if the sign isn't recognized.
+    ///
+    /// The following signs are supported:
+    ///
+    /// * `==` _or_ `=` -> `Eq`
+    /// * `!=` _or_ `!` _or_ `<>` -> `Ne`
+    /// * `< ` -> `Lt`
+    /// * `<=` -> `Le`
+    /// * `>=` -> `Ge`
+    /// * `> ` -> `Gt`
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use version_compare::Cmp;
+    ///
+    /// assert_eq!(Cmp::from_sign("=="), Ok(Cmp::Eq));
+    /// assert_eq!(Cmp::from_sign("<"), Ok(Cmp::Lt));
+    /// assert_eq!(Cmp::from_sign("  >=   "), Ok(Cmp::Ge));
+    /// assert!(Cmp::from_sign("*").is_err());
+    /// ```
+    #[allow(clippy::result_map_unit_fn)]
+    pub fn from_sign<S: AsRef<str>>(sign: S) -> Result<Cmp, ()> {
+        match sign.as_ref().trim() {
+            "==" | "=" => Ok(Cmp::Eq),
+            "!=" | "!" | "<>" => Ok(Cmp::Ne),
+            "<" => Ok(Cmp::Lt),
+            "<=" => Ok(Cmp::Le),
+            ">=" => Ok(Cmp::Ge),
+            ">" => Ok(Cmp::Gt),
+            _ => Err(()),
+        }
+    }
+
+    /// Get a comparison operator by it's name.
+    /// Names are case-insensitive, and whitespaces are stripped from the string.
+    /// An error is returned if the name isn't recognized.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use version_compare::Cmp;
+    ///
+    /// assert_eq!(Cmp::from_name("eq"), Ok(Cmp::Eq));
+    /// assert_eq!(Cmp::from_name("lt"), Ok(Cmp::Lt));
+    /// assert_eq!(Cmp::from_name("  Ge   "), Ok(Cmp::Ge));
+    /// assert!(Cmp::from_name("abc").is_err());
+    /// ```
+    #[allow(clippy::result_map_unit_fn)]
+    pub fn from_name<S: AsRef<str>>(sign: S) -> Result<Cmp, ()> {
+        match sign.as_ref().trim().to_lowercase().as_str() {
+            "eq" => Ok(Cmp::Eq),
+            "ne" => Ok(Cmp::Ne),
+            "lt" => Ok(Cmp::Lt),
+            "le" => Ok(Cmp::Le),
+            "ge" => Ok(Cmp::Ge),
+            "gt" => Ok(Cmp::Gt),
+            _ => Err(()),
+        }
+    }
+
+    /// Get the comparison operator from Rusts `Ordering` enum.
+    ///
+    /// The following comparison operators are returned:
+    ///
+    /// * `Ordering::Less` -> `Lt`
+    /// * `Ordering::Equal` -> `Eq`
+    /// * `Ordering::Greater` -> `Gt`
+    pub fn from_ord(ord: Ordering) -> Cmp {
+        match ord {
+            Ordering::Less => Cmp::Lt,
+            Ordering::Equal => Cmp::Eq,
+            Ordering::Greater => Cmp::Gt,
+        }
+    }
+
+    /// Get the name of this comparison operator.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use version_compare::Cmp;
+    ///
+    /// assert_eq!(Cmp::Eq.name(), "eq");
+    /// assert_eq!(Cmp::Lt.name(), "lt");
+    /// assert_eq!(Cmp::Ge.name(), "ge");
+    /// ```
+    pub fn name<'a>(self) -> &'a str {
+        match self {
+            Cmp::Eq => "eq",
+            Cmp::Ne => "ne",
+            Cmp::Lt => "lt",
+            Cmp::Le => "le",
+            Cmp::Ge => "ge",
+            Cmp::Gt => "gt",
+        }
+    }
+
+    /// Get the inverted comparison operator.
+    ///
+    /// This uses the following bidirectional rules:
+    ///
+    /// * `Eq` <-> `Ne`
+    /// * `Lt` <-> `Ge`
+    /// * `Le` <-> `Gt`
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use version_compare::Cmp;
+    ///
+    /// assert_eq!(Cmp::Eq.invert(), Cmp::Ne);
+    /// assert_eq!(Cmp::Lt.invert(), Cmp::Ge);
+    /// assert_eq!(Cmp::Gt.invert(), Cmp::Le);
+    /// ```
+    #[must_use]
+    pub fn invert(self) -> Self {
+        match self {
+            Cmp::Eq => Cmp::Ne,
+            Cmp::Ne => Cmp::Eq,
+            Cmp::Lt => Cmp::Ge,
+            Cmp::Le => Cmp::Gt,
+            Cmp::Ge => Cmp::Lt,
+            Cmp::Gt => Cmp::Le,
+        }
+    }
+
+    /// Get the opposite comparison operator.
+    ///
+    /// This uses the following bidirectional rules:
+    ///
+    /// * `Eq` <-> `Ne`
+    /// * `Lt` <-> `Gt`
+    /// * `Le` <-> `Ge`
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use version_compare::Cmp;
+    ///
+    /// assert_eq!(Cmp::Eq.opposite(), Cmp::Ne);
+    /// assert_eq!(Cmp::Lt.opposite(), Cmp::Gt);
+    /// assert_eq!(Cmp::Ge.opposite(), Cmp::Le);
+    /// ```
+    #[must_use]
+    pub fn opposite(self) -> Self {
+        match self {
+            Cmp::Eq => Cmp::Ne,
+            Cmp::Ne => Cmp::Eq,
+            Cmp::Lt => Cmp::Gt,
+            Cmp::Le => Cmp::Ge,
+            Cmp::Ge => Cmp::Le,
+            Cmp::Gt => Cmp::Lt,
+        }
+    }
+
+    /// Get the flipped comparison operator.
+    ///
+    /// This uses the following bidirectional rules:
+    ///
+    /// * `Lt` <-> `Gt`
+    /// * `Le` <-> `Ge`
+    /// * Other operators are returned as is.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use version_compare::Cmp;
+    ///
+    /// assert_eq!(Cmp::Eq.flip(), Cmp::Eq);
+    /// assert_eq!(Cmp::Lt.flip(), Cmp::Gt);
+    /// assert_eq!(Cmp::Ge.flip(), Cmp::Le);
+    /// ```
+    #[must_use]
+    pub fn flip(self) -> Self {
+        match self {
+            Cmp::Lt => Cmp::Gt,
+            Cmp::Le => Cmp::Ge,
+            Cmp::Ge => Cmp::Le,
+            Cmp::Gt => Cmp::Lt,
+            _ => self,
+        }
+    }
+
+    /// Get the sign for this comparison operator.
+    ///
+    /// The following signs are returned:
+    ///
+    /// * `Eq` -> `==`
+    /// * `Ne` -> `!=`
+    /// * `Lt` -> `< `
+    /// * `Le` -> `<=`
+    /// * `Ge` -> `>=`
+    /// * `Gt` -> `> `
+    ///
+    /// Note: Some comparison operators also support other signs,
+    /// such as `=` for `Eq` and `!` for `Ne`,
+    /// these are never returned by this method however as the table above is used.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use version_compare::Cmp;
+    ///
+    /// assert_eq!(Cmp::Eq.sign(), "==");
+    /// assert_eq!(Cmp::Lt.sign(), "<");
+    /// assert_eq!(Cmp::Ge.flip().sign(), "<=");
+    /// ```
+    pub fn sign(self) -> &'static str {
+        match self {
+            Cmp::Eq => "==",
+            Cmp::Ne => "!=",
+            Cmp::Lt => "<",
+            Cmp::Le => "<=",
+            Cmp::Ge => ">=",
+            Cmp::Gt => ">",
+        }
+    }
+
+    /// Get a factor (number) for this comparison operator.
+    /// These factors can be useful for quick calculations.
+    ///
+    /// The following factor numbers are returned:
+    ///
+    /// * `Eq` _or_ `Ne` -> ` 0`
+    /// * `Lt` _or_ `Le` -> `-1`
+    /// * `Gt` _or_ `Ge` -> ` 1`
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use version_compare::Version;
+    ///
+    /// let a = Version::from("1.2.3").unwrap();
+    /// let b = Version::from("1.3").unwrap();
+    ///
+    /// assert_eq!(a.compare(&b).factor(), -1);
+    /// assert_eq!(10 * b.compare(a).factor(), 10);
+    /// ```
+    pub fn factor(self) -> i8 {
+        match self {
+            Cmp::Eq | Cmp::Ne => 0,
+            Cmp::Lt | Cmp::Le => -1,
+            Cmp::Gt | Cmp::Ge => 1,
+        }
+    }
+
+    /// Get Rust's ordering for this comparison operator.
+    ///
+    /// The following comparison operators are supported:
+    ///
+    /// * `Eq` -> `Ordering::Equal`
+    /// * `Lt` -> `Ordering::Less`
+    /// * `Gt` -> `Ordering::Greater`
+    ///
+    /// For other comparison operators `None` is returned.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use std::cmp::Ordering;
+    /// use version_compare::Version;
+    ///
+    /// let a = Version::from("1.2.3").unwrap();
+    /// let b = Version::from("1.3").unwrap();
+    ///
+    /// assert_eq!(a.compare(b).ord().unwrap(), Ordering::Less);
+    /// ```
+    pub fn ord(self) -> Option<Ordering> {
+        match self {
+            Cmp::Eq => Some(Ordering::Equal),
+            Cmp::Lt => Some(Ordering::Less),
+            Cmp::Gt => Some(Ordering::Greater),
+            _ => None,
+        }
+    }
+}
+
+#[cfg_attr(tarpaulin, skip)]
+#[cfg(test)]
+mod tests {
+    use std::cmp::Ordering;
+
+    use super::Cmp;
+
+    #[test]
+    fn from_sign() {
+        // Normal signs
+        assert_eq!(Cmp::from_sign("==").unwrap(), Cmp::Eq);
+        assert_eq!(Cmp::from_sign("=").unwrap(), Cmp::Eq);
+        assert_eq!(Cmp::from_sign("!=").unwrap(), Cmp::Ne);
+        assert_eq!(Cmp::from_sign("!").unwrap(), Cmp::Ne);
+        assert_eq!(Cmp::from_sign("<>").unwrap(), Cmp::Ne);
+        assert_eq!(Cmp::from_sign("<").unwrap(), Cmp::Lt);
+        assert_eq!(Cmp::from_sign("<=").unwrap(), Cmp::Le);
+        assert_eq!(Cmp::from_sign(">=").unwrap(), Cmp::Ge);
+        assert_eq!(Cmp::from_sign(">").unwrap(), Cmp::Gt);
+
+        // Exceptional cases
+        assert_eq!(Cmp::from_sign("  <=  ").unwrap(), Cmp::Le);
+        assert_eq!(Cmp::from_sign("*"), Err(()));
+    }
+
+    #[test]
+    fn from_name() {
+        // Normal names
+        assert_eq!(Cmp::from_name("eq").unwrap(), Cmp::Eq);
+        assert_eq!(Cmp::from_name("ne").unwrap(), Cmp::Ne);
+        assert_eq!(Cmp::from_name("lt").unwrap(), Cmp::Lt);
+        assert_eq!(Cmp::from_name("le").unwrap(), Cmp::Le);
+        assert_eq!(Cmp::from_name("ge").unwrap(), Cmp::Ge);
+        assert_eq!(Cmp::from_name("gt").unwrap(), Cmp::Gt);
+
+        // Exceptional cases
+        assert_eq!(Cmp::from_name("  Le  ").unwrap(), Cmp::Le);
+        assert_eq!(Cmp::from_name("abc"), Err(()));
+    }
+
+    #[test]
+    fn from_ord() {
+        assert_eq!(Cmp::from_ord(Ordering::Less), Cmp::Lt);
+        assert_eq!(Cmp::from_ord(Ordering::Equal), Cmp::Eq);
+        assert_eq!(Cmp::from_ord(Ordering::Greater), Cmp::Gt);
+    }
+
+    #[test]
+    fn name() {
+        assert_eq!(Cmp::Eq.name(), "eq");
+        assert_eq!(Cmp::Ne.name(), "ne");
+        assert_eq!(Cmp::Lt.name(), "lt");
+        assert_eq!(Cmp::Le.name(), "le");
+        assert_eq!(Cmp::Ge.name(), "ge");
+        assert_eq!(Cmp::Gt.name(), "gt");
+    }
+
+    #[test]
+    fn invert() {
+        assert_eq!(Cmp::Ne.invert(), Cmp::Eq);
+        assert_eq!(Cmp::Eq.invert(), Cmp::Ne);
+        assert_eq!(Cmp::Ge.invert(), Cmp::Lt);
+        assert_eq!(Cmp::Gt.invert(), Cmp::Le);
+        assert_eq!(Cmp::Lt.invert(), Cmp::Ge);
+        assert_eq!(Cmp::Le.invert(), Cmp::Gt);
+    }
+
+    #[test]
+    fn opposite() {
+        assert_eq!(Cmp::Eq.opposite(), Cmp::Ne);
+        assert_eq!(Cmp::Ne.opposite(), Cmp::Eq);
+        assert_eq!(Cmp::Lt.opposite(), Cmp::Gt);
+        assert_eq!(Cmp::Le.opposite(), Cmp::Ge);
+        assert_eq!(Cmp::Ge.opposite(), Cmp::Le);
+        assert_eq!(Cmp::Gt.opposite(), Cmp::Lt);
+    }
+
+    #[test]
+    fn flip() {
+        assert_eq!(Cmp::Eq.flip(), Cmp::Eq);
+        assert_eq!(Cmp::Ne.flip(), Cmp::Ne);
+        assert_eq!(Cmp::Lt.flip(), Cmp::Gt);
+        assert_eq!(Cmp::Le.flip(), Cmp::Ge);
+        assert_eq!(Cmp::Ge.flip(), Cmp::Le);
+        assert_eq!(Cmp::Gt.flip(), Cmp::Lt);
+    }
+
+    #[test]
+    fn sign() {
+        assert_eq!(Cmp::Eq.sign(), "==");
+        assert_eq!(Cmp::Ne.sign(), "!=");
+        assert_eq!(Cmp::Lt.sign(), "<");
+        assert_eq!(Cmp::Le.sign(), "<=");
+        assert_eq!(Cmp::Ge.sign(), ">=");
+        assert_eq!(Cmp::Gt.sign(), ">");
+    }
+
+    #[test]
+    fn factor() {
+        assert_eq!(Cmp::Eq.factor(), 0);
+        assert_eq!(Cmp::Ne.factor(), 0);
+        assert_eq!(Cmp::Lt.factor(), -1);
+        assert_eq!(Cmp::Le.factor(), -1);
+        assert_eq!(Cmp::Ge.factor(), 1);
+        assert_eq!(Cmp::Gt.factor(), 1);
+    }
+
+    #[test]
+    fn ord() {
+        assert_eq!(Cmp::Eq.ord(), Some(Ordering::Equal));
+        assert_eq!(Cmp::Ne.ord(), None);
+        assert_eq!(Cmp::Lt.ord(), Some(Ordering::Less));
+        assert_eq!(Cmp::Le.ord(), None);
+        assert_eq!(Cmp::Ge.ord(), None);
+        assert_eq!(Cmp::Gt.ord(), Some(Ordering::Greater));
+    }
+}
diff --git a/src/compare.rs b/src/compare.rs
new file mode 100644 (file)
index 0000000..e42ab92
--- /dev/null
@@ -0,0 +1,122 @@
+//! Version compare module, with useful static comparison methods.
+
+use crate::version::Version;
+use crate::Cmp;
+
+/// Compare two version number strings to each other.
+///
+/// This compares version `a` to version `b`, and returns whether version `a` is greater, less
+/// or equal to version `b`.
+///
+/// If either version number string is invalid an error is returned.
+///
+/// One of the following operators is returned:
+///
+/// * `Cmp::Eq`
+/// * `Cmp::Lt`
+/// * `Cmp::Gt`
+///
+/// # Examples
+///
+/// ```
+/// use version_compare::{Cmp, compare};
+///
+/// assert_eq!(compare("1.2.3", "1.2.3"), Ok(Cmp::Eq));
+/// assert_eq!(compare("1.2.3", "1.2.4"), Ok(Cmp::Lt));
+/// assert_eq!(compare("1", "0.1"), Ok(Cmp::Gt));
+/// ```
+#[allow(clippy::result_map_unit_fn)]
+pub fn compare<A, B>(a: A, b: B) -> Result<Cmp, ()>
+where
+    A: AsRef<str>,
+    B: AsRef<str>,
+{
+    let a = Version::from(a.as_ref()).ok_or(())?;
+    let b = Version::from(b.as_ref()).ok_or(())?;
+    Ok(a.compare(b))
+}
+
+/// Compare two version number strings to each other and test against the given comparison
+/// `operator`.
+///
+/// If either version number string is invalid an error is returned.
+///
+/// # Examples
+///
+/// ```
+/// use version_compare::{Cmp, compare_to};
+///
+/// assert!(compare_to("1.2.3", "1.2.3", Cmp::Eq).unwrap());
+/// assert!(compare_to("1.2.3", "1.2.3", Cmp::Le).unwrap());
+/// assert!(compare_to("1.2.3", "1.2.4", Cmp::Lt).unwrap());
+/// assert!(compare_to("1", "0.1", Cmp::Gt).unwrap());
+/// assert!(compare_to("1", "0.1", Cmp::Ge).unwrap());
+/// ```
+#[allow(clippy::result_map_unit_fn)]
+pub fn compare_to<A, B>(a: A, b: B, operator: Cmp) -> Result<bool, ()>
+where
+    A: AsRef<str>,
+    B: AsRef<str>,
+{
+    let a = Version::from(a.as_ref()).ok_or(())?;
+    let b = Version::from(b.as_ref()).ok_or(())?;
+    Ok(a.compare_to(b, operator))
+}
+
+#[cfg_attr(tarpaulin, skip)]
+#[cfg(test)]
+mod tests {
+    use crate::test::{COMBIS, COMBIS_ERROR};
+    use crate::Cmp;
+
+    #[test]
+    fn compare() {
+        // Compare each version in the version set
+        for entry in COMBIS {
+            assert_eq!(
+                super::compare(entry.0, entry.1),
+                Ok(entry.2),
+                "Testing that {} is {} {}",
+                entry.0,
+                entry.2.sign(),
+                entry.1,
+            );
+        }
+
+        // Compare each error version in the version set
+        for entry in COMBIS_ERROR {
+            let result = super::compare(entry.0, entry.1);
+
+            if result.is_ok() {
+                assert!(result != Ok(entry.2));
+            }
+        }
+    }
+
+    #[test]
+    fn compare_to() {
+        // Compare each version in the version set
+        for entry in COMBIS {
+            // Test
+            assert!(super::compare_to(entry.0, entry.1, entry.2).unwrap());
+
+            // Make sure the inverse operator is not correct
+            assert_eq!(
+                super::compare_to(entry.0, entry.1, entry.2.invert()).unwrap(),
+                false,
+            );
+        }
+
+        // Compare each error version in the version set
+        for entry in COMBIS_ERROR {
+            let result = super::compare_to(entry.0, entry.1, entry.2);
+
+            if result.is_ok() {
+                assert!(!result.unwrap())
+            }
+        }
+
+        // Assert an exceptional case, compare to not equal
+        assert!(super::compare_to("1.2.3", "1.2", Cmp::Ne).unwrap());
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644 (file)
index 0000000..c7d3596
--- /dev/null
@@ -0,0 +1,103 @@
+//! Rust library to easily compare version numbers with no specific format, and test against various comparison operators.
+//!
+//! Comparing version numbers is hard, especially with weird version number formats.
+//!
+//! This library helps you to easily compare any kind of version number with no
+//! specific format using a best-effort approach.
+//! Two version numbers can be compared to each other to get a comparison operator
+//! (`<`, `==`, `>`), or test them against a comparison operator.
+//!
+//! Along with version comparison, the library provides various other tools for
+//! working with version numbers.
+//!
+//! Inspired by PHPs [version_compare()](http://php.net/manual/en/function.version-compare.php).
+//!
+//! ### Formats
+//!
+//! Version numbers that would parse successfully include:  
+//! `1`, `3.10.4.1`, `1.2.alpha`, `1.2.dev.4`, ` `, ` .   -32 . 1`, `MyApp 3.2.0 / build 0932` ...
+//!
+//! See a list of how version numbers compare [here](https://github.com/timvisee/version-compare/blob/411ed7135741ed7cf2fcf4919012fb5412dc122b/src/test.rs#L50-L103).
+//!
+//! ## Examples
+//!
+//! [example.rs](examples/example.rs):
+//! ```rust
+//! use version_compare::{compare, compare_to, Cmp, Version};
+//!
+//! let a = "1.2";
+//! let b = "1.5.1";
+//!
+//! // The following comparison operators are used:
+//! // - Cmp::Eq -> Equal
+//! // - Cmp::Ne -> Not equal
+//! // - Cmp::Lt -> Less than
+//! // - Cmp::Le -> Less than or equal
+//! // - Cmp::Ge -> Greater than or equal
+//! // - Cmp::Gt -> Greater than
+//!
+//! // Easily compare version strings
+//! assert_eq!(compare(a, b), Ok(Cmp::Lt));
+//! assert_eq!(compare_to(a, b, Cmp::Le), Ok(true));
+//! assert_eq!(compare_to(a, b, Cmp::Gt), Ok(false));
+//!
+//! // Parse and wrap version strings as a Version
+//! let a = Version::from(a).unwrap();
+//! let b = Version::from(b).unwrap();
+//!
+//! // The Version can easily be compared with
+//! assert_eq!(a < b, true);
+//! assert_eq!(a <= b, true);
+//! assert_eq!(a > b, false);
+//! assert_eq!(a != b, true);
+//! assert_eq!(a.compare(&b), Cmp::Lt);
+//! assert_eq!(a.compare_to(&b, Cmp::Lt), true);
+//!
+//! // Or match the comparison operators
+//! match a.compare(b) {
+//!     Cmp::Lt => println!("Version a is less than b"),
+//!     Cmp::Eq => println!("Version a is equal to b"),
+//!     Cmp::Gt => println!("Version a is greater than b"),
+//!     _ => unreachable!(),
+//! }
+//! ```
+//!
+//! See the [`examples`](https://github.com/timvisee/version-compare/tree/master/examples) directory for more.
+//!
+//! ## Features
+//!
+//! * Compare version numbers, get: `<`, `==`, `>`
+//! * Compare against a comparison operator
+//!   (`<`, `<=`, `==`, `!=`, `>=`, `>`)
+//! * Parse complex and unspecified formats
+//! * Static, standalone methods to easily compare version strings in a single line
+//!   of code
+//!
+//! ### Semver
+//!
+//! Version numbers using the [semver](http://semver.org/) format are compared
+//! correctly with no additional configuration.
+//!
+//! If your version number strings follow this exact format you may be better off
+//! using the [`semver`](https://crates.io/crates/semver) crate for more format
+//! specific features.
+//!
+//! If that isn't certain however, `version-compare` makes comparing a breeze.
+//!
+//! _[View complete README](https://github.com/timvisee/version-compare/blob/master/README.md)_
+
+mod cmp;
+mod compare;
+mod manifest;
+mod part;
+mod version;
+
+#[cfg(test)]
+mod test;
+
+// Re-exports
+pub use crate::cmp::Cmp;
+pub use crate::compare::{compare, compare_to};
+pub use crate::manifest::Manifest;
+pub use crate::part::Part;
+pub use crate::version::Version;
diff --git a/src/manifest.rs b/src/manifest.rs
new file mode 100644 (file)
index 0000000..2dc347d
--- /dev/null
@@ -0,0 +1,76 @@
+//! Module for the version manifest.
+//!
+//! A version manifest can be used to configure and specify how versions are parsed and compared.
+//! For example, you can configure the maximum depth of a version number, and set whether text
+//! parts are ignored in a version string.
+
+/// Version manifest (configuration).
+///
+/// A manifest (configuration) that is used respectively when parsing and comparing version strings.
+///
+/// # Examples
+///
+/// ```rust
+/// use version_compare::{Manifest, Version};
+///
+/// // Create manifest with max depth of 2
+/// let mut manifest = Manifest::default();
+/// manifest.max_depth = Some(2);
+///
+/// // Version strings equal with manifest because we compare up-to 2 parts deep
+/// let a = Version::from_manifest("1.0.1", &manifest).unwrap();
+/// let b = Version::from_manifest("1.0.2", &manifest).unwrap();
+/// assert!(a == b);
+/// ```
+
+#[derive(Debug, Clone, PartialEq, Eq, Default)]
+pub struct Manifest {
+    /// The maximum depth of a version number.
+    ///
+    /// This specifies the maximum number of parts. There is no limit if `None` is set.
+    pub max_depth: Option<usize>,
+
+    /// Whether to ignore text parts in version strings.
+    pub ignore_text: bool,
+}
+
+/// Version manifest implementation.
+impl Manifest {
+    /// Check whether there's a maximum configured depth.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use version_compare::Manifest;
+    ///
+    /// let mut manifest = Manifest::default();
+    ///
+    /// assert!(!manifest.has_max_depth());
+    ///
+    /// manifest.max_depth = Some(3);
+    /// assert!(manifest.has_max_depth());
+    /// ```
+    pub fn has_max_depth(&self) -> bool {
+        self.max_depth.is_some() && self.max_depth.unwrap() > 0
+    }
+}
+
+#[cfg_attr(tarpaulin, skip)]
+#[cfg(test)]
+mod tests {
+    use super::Manifest;
+
+    #[test]
+    fn has_max_depth() {
+        let mut manifest = Manifest::default();
+
+        manifest.max_depth = Some(1);
+        assert!(manifest.has_max_depth());
+
+        manifest.max_depth = Some(3);
+        assert!(manifest.has_max_depth());
+
+        manifest.max_depth = None;
+        assert!(!manifest.has_max_depth());
+    }
+}
diff --git a/src/part.rs b/src/part.rs
new file mode 100644 (file)
index 0000000..ecfac32
--- /dev/null
@@ -0,0 +1,44 @@
+//! Version part module.
+//!
+//! A module that provides the `Part` enum, with the specification of all available version
+//! parts. Each version string is broken down into these version parts when being parsed to a
+//! `Version`.
+
+use std::fmt;
+
+/// Version string part enum.
+///
+/// Each version string is broken down into these version parts when being parsed to a `Version`.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Part<'a> {
+    /// Numeric part, most common in version strings.
+    ///
+    /// Holds the numerical value.
+    Number(i32),
+
+    /// A text part.
+    ///
+    /// These parts usually hold text with an yet unknown definition. Holds the string slice.
+    Text(&'a str),
+}
+
+impl<'a> fmt::Display for Part<'a> {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Part::Number(n) => write!(f, "{}", n),
+            Part::Text(t) => write!(f, "{}", t),
+        }
+    }
+}
+
+#[cfg_attr(tarpaulin, skip)]
+#[cfg(test)]
+mod tests {
+    use super::Part;
+
+    #[test]
+    fn display() {
+        assert_eq!(format!("{}", Part::Number(123)), "123");
+        assert_eq!(format!("{}", Part::Text("123")), "123");
+    }
+}
diff --git a/src/test.rs b/src/test.rs
new file mode 100644 (file)
index 0000000..ff4ac32
--- /dev/null
@@ -0,0 +1,116 @@
+use crate::Cmp;
+
+/// Struct containing a version number with some meta data.
+/// Such a set can be used for testing.
+///
+/// # Arguments
+///
+/// - `0`: The version string.
+/// - `1`: Number of version parts.
+pub struct Version(pub &'static str, pub usize);
+
+/// List of version numbers with metadata for dynamic tests
+pub const VERSIONS: &'static [Version] = &[
+    Version("1", 1),
+    Version("1.2", 2),
+    Version("1.2.3.4", 4),
+    Version("1.2.3.4.5.6.7.8", 8),
+    Version("0", 1),
+    Version("0.0.0", 3),
+    Version("1.0.0", 3),
+    Version("0.0.1", 3),
+    Version("", 0),
+    Version(".", 0),
+    Version("...", 0),
+    Version("1.2.dev", 3),
+    Version("1.2-dev", 3),
+    Version("1.2.alpha.4", 4),
+    Version("1.2-alpha-4", 4),
+    Version("snapshot.1.2", 3),
+    Version("snapshot-1.2", 3),
+    // Issue: https://github.com/timvisee/version-compare/issues/26
+    Version("0.0.1-test.0222426166a", 6),
+    Version("0.0.1-test.0222426166565421816516584651684351354", 5),
+    Version("0.0.1-test.02224261665a", 5),
+    Version("0.0.1-test.02224261665d7b1b689816d12f6bcacb", 5),
+];
+
+/// List of version numbers that contain errors with metadata for dynamic tests
+pub const VERSIONS_ERROR: &'static [Version] = &[
+    Version("abc", 1),
+    Version("alpha.dev.snapshot", 3),
+    Version("test. .snapshot", 3),
+];
+
+/// Struct containing two version numbers, and the comparison operator.
+/// Such a set can be used for testing.
+///
+/// # Arguments
+///
+/// - `0`: The main version.
+/// - `1`: The other version.
+/// - `2`: The comparison operator.
+pub struct VersionCombi(pub &'static str, pub &'static str, pub Cmp);
+
+/// List of version combinations for dynamic tests
+pub const COMBIS: &'static [VersionCombi] = &[
+    VersionCombi("1", "1", Cmp::Eq),
+    VersionCombi("1.0.0.0", "1", Cmp::Eq),
+    VersionCombi("1", "1.0.0.0", Cmp::Eq),
+    VersionCombi("0", "0", Cmp::Eq),
+    VersionCombi("0.0.0", "0", Cmp::Eq),
+    VersionCombi("0", "0.0.0", Cmp::Eq),
+    VersionCombi("", "", Cmp::Eq),
+    VersionCombi("", "0.0", Cmp::Eq),
+    VersionCombi("0.0", "", Cmp::Eq),
+    VersionCombi("", "0.1", Cmp::Lt),
+    VersionCombi("0.1", "", Cmp::Gt),
+    VersionCombi("1.2.3", "1.2.3", Cmp::Eq),
+    VersionCombi("1.2.3", "1.2.4", Cmp::Lt),
+    VersionCombi("1.0.0.1", "1.0.0.0", Cmp::Gt),
+    VersionCombi("1.0.0.0", "1.0.0.1", Cmp::Lt),
+    VersionCombi("1.2.3.4", "1.2", Cmp::Gt),
+    VersionCombi("1.2", "1.2.3.4", Cmp::Lt),
+    VersionCombi("1.2.3.4", "2", Cmp::Lt),
+    VersionCombi("2", "1.2.3.4", Cmp::Gt),
+    VersionCombi("123", "123", Cmp::Eq),
+    VersionCombi("123", "1.2.3", Cmp::Gt),
+    VersionCombi("1.2.3", "123", Cmp::Lt),
+    VersionCombi("1.1.2", "1.1.30-dev", Cmp::Lt),
+    VersionCombi("1.2.3", "1.2.3.alpha", Cmp::Gt),
+    VersionCombi("1.2.3", "1.2.3-dev", Cmp::Gt),
+    VersionCombi("1.2.3 RC0", "1.2.3 rc1", Cmp::Lt),
+    VersionCombi("1.2.3 rc2", "1.2.3 RC99", Cmp::Lt),
+    VersionCombi("1.2.3 RC3", "1.2.3 RC1", Cmp::Gt),
+    VersionCombi("1.2.3a", "1.2.3b", Cmp::Lt),
+    VersionCombi("1.2.3b", "1.2.3a", Cmp::Gt),
+    VersionCombi("1.2.3.dev", "1.2.3.alpha", Cmp::Gt),
+    VersionCombi("1.2.3-dev", "1.2.3-alpha", Cmp::Gt),
+    VersionCombi("1.2.3.dev.1", "1.2.3.alpha", Cmp::Gt),
+    VersionCombi("1.2.3-dev-1", "1.2.3-alpha", Cmp::Gt),
+    VersionCombi("version-compare 3.2.0 / build 0932", "3.2.5", Cmp::Lt),
+    VersionCombi("version-compare 3.2.0 / build 0932", "3.1.1", Cmp::Gt),
+    VersionCombi(
+        "version-compare 1.4.1 / build 0043",
+        "version-compare 1.4.1 / build 0043",
+        Cmp::Eq,
+    ),
+    VersionCombi(
+        "version-compare 1.4.1 / build 0042",
+        "version-compare 1.4.1 / build 0043",
+        Cmp::Lt,
+    ),
+    // Issue: https://github.com/timvisee/version-compare/issues/24
+    VersionCombi("7.2p1", "7.1", Cmp::Gt),
+    // TODO: inspect these cases
+    VersionCombi("snapshot.1.2.3", "1.2.3.alpha", Cmp::Lt),
+    VersionCombi("snapshot-1.2.3", "1.2.3-alpha", Cmp::Lt),
+];
+
+/// List of invalid version combinations for dynamic tests
+pub const COMBIS_ERROR: &'static [VersionCombi] = &[
+    VersionCombi("1.2.3", "1.2.3", Cmp::Lt),
+    VersionCombi("1.2", "1.2.0.0", Cmp::Ne),
+    VersionCombi("1.2.3.dev", "dev", Cmp::Eq),
+    VersionCombi("snapshot", "1", Cmp::Lt),
+];
diff --git a/src/version.rs b/src/version.rs
new file mode 100644 (file)
index 0000000..2dcb27c
--- /dev/null
@@ -0,0 +1,755 @@
+//! 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());
+    }
+}