--- /dev/null
+{
+ "git": {
+ "sha1": "9a965b448749892c921808946420d6372de0958f"
+ },
+ "path_in_vcs": ""
+}
\ No newline at end of file
--- /dev/null
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+[[package]]
+name = "version-compare"
+version = "0.1.1"
--- /dev/null
+# 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"
--- /dev/null
+[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"]
--- /dev/null
+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.
+
--- /dev/null
+[![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.
--- /dev/null
+//! 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!(),
+ }
+}
--- /dev/null
+//! 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"),
+ }
+}
--- /dev/null
+//! 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));
+ }
+}
--- /dev/null
+//! 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());
+ }
+}
--- /dev/null
+//! 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;
--- /dev/null
+//! 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());
+ }
+}
--- /dev/null
+//! 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");
+ }
+}
--- /dev/null
+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),
+];
--- /dev/null
+//! 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());
+ }
+}