From 82c2a37ae580e67fc719d1a32774092eb3e9abb6 Mon Sep 17 00:00:00 2001 From: DongHun Kwak Date: Thu, 6 Apr 2023 10:10:07 +0900 Subject: [PATCH 1/1] Import version-compare 0.1.1 --- .cargo_vcs_info.json | 6 + Cargo.lock | 5 + Cargo.toml | 39 +++ Cargo.toml.orig | 14 + LICENSE | 20 ++ README.md | 137 ++++++++ examples/example.rs | 46 +++ examples/minimal.rs | 19 ++ src/cmp.rs | 436 +++++++++++++++++++++++++ src/compare.rs | 122 +++++++ src/lib.rs | 103 ++++++ src/manifest.rs | 76 +++++ src/part.rs | 44 +++ src/test.rs | 116 +++++++ src/version.rs | 755 +++++++++++++++++++++++++++++++++++++++++++ 15 files changed, 1938 insertions(+) create mode 100644 .cargo_vcs_info.json create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 Cargo.toml.orig create mode 100644 LICENSE create mode 100644 README.md create mode 100644 examples/example.rs create mode 100644 examples/minimal.rs create mode 100644 src/cmp.rs create mode 100644 src/compare.rs create mode 100644 src/lib.rs create mode 100644 src/manifest.rs create mode 100644 src/part.rs create mode 100644 src/test.rs create mode 100644 src/version.rs diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json new file mode 100644 index 0000000..edf75a9 --- /dev/null +++ b/.cargo_vcs_info.json @@ -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 index 0000000..b356177 --- /dev/null +++ b/Cargo.lock @@ -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 index 0000000..64f001a --- /dev/null +++ b/Cargo.toml @@ -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 index 0000000..68fa5c4 --- /dev/null +++ b/Cargo.toml.orig @@ -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 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 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 index 0000000..6859f06 --- /dev/null +++ b/examples/example.rs @@ -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 index 0000000..37413c8 --- /dev/null +++ b/examples/minimal.rs @@ -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 index 0000000..1e3ef2f --- /dev/null +++ b/src/cmp.rs @@ -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>(sign: S) -> Result { + 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>(sign: S) -> Result { + 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 { + 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 index 0000000..e42ab92 --- /dev/null +++ b/src/compare.rs @@ -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: A, b: B) -> Result +where + A: AsRef, + B: AsRef, +{ + 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: A, b: B, operator: Cmp) -> Result +where + A: AsRef, + B: AsRef, +{ + 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 index 0000000..c7d3596 --- /dev/null +++ b/src/lib.rs @@ -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 index 0000000..2dc347d --- /dev/null +++ b/src/manifest.rs @@ -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, + + /// 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 index 0000000..ecfac32 --- /dev/null +++ b/src/part.rs @@ -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 index 0000000..ff4ac32 --- /dev/null +++ b/src/test.rs @@ -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 index 0000000..2dcb27c --- /dev/null +++ b/src/version.rs @@ -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>, + 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 { + 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>) -> 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 { + 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, ()> { + // 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(&self, other: V) -> Cmp + where + V: Borrow>, + { + 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(&self, other: V, operator: Cmp) -> bool + where + V: Borrow>, + { + 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 { + 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>> { + // 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::() { + 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>>, + mut other_iter: Peekable>>, +) -> 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()); + } +} -- 2.34.1