Skip to content

Commit

Permalink
Reimport libversion correctly
Browse files Browse the repository at this point in the history
  • Loading branch information
AMDmi3 committed Sep 9, 2024
1 parent 0eeae15 commit 19f0847
Show file tree
Hide file tree
Showing 10 changed files with 752 additions and 1 deletion.
1 change: 0 additions & 1 deletion libversion
Submodule libversion deleted from ce9642
1 change: 1 addition & 0 deletions libversion/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/target
13 changes: 13 additions & 0 deletions libversion/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "libversion"
description = "Advanced version string comparison algorithm"
version = "0.2.0"
authors = ["Dmitry Marakasov <[email protected]>"]
repository = "https://github.com/repology/libversion-rs"
readme = "README.md"
categories = ["algorithms", "version", "comparison"]
license = "MIT"
edition = "2021"

[dependencies]
bitflags = "2.6.0"
19 changes: 19 additions & 0 deletions libversion/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Copyright (c) 2024 Dmitry Marakasov <[email protected]>

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.
131 changes: 131 additions & 0 deletions libversion/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# libversion

![CI](https://github.com/repology/libversion-rs/workflows/CI/badge.svg)
[![Coverage Status](https://coveralls.io/repos/github/repology/libversion-rs/badge.svg?branch=master)](https://coveralls.io/github/repology/libversion-rs?branch=master)
[![Github commits (since latest release)](https://img.shields.io/github/commits-since/repology/libversion-rs/latest.svg)](https://github.com/repology/libversion-rs)

Advanced version string comparison library.

Need to compare software, package or whatever versions? Comparing
`1.0` and `1.1` could be easy, but are you ready for more
complex cases like `1.2-x.3~alpha4`? **libversion** is, which
is proven by using the library in [Repology](https://repology.org/)
project which relies on comparing software version strings, even
if they are written in different formats.

## Features

A short list of version features libversion handles for you:

* Simple versions, obviously: `0.9 < 1.0 < 1.1`
* Omitting insignificant components: `1.0 == 1.0.0`
* Leading zeroes: `1.001 == 1.1`
* Unusual separators: `1_2~3 == 1.2.3`
* Letter suffixes: `1.2 < 1.2a < 1.2b < 1.3`
* Alphanumeric prerelease components:
* `1.0alpha1 == 1.0.alpha1 == 1.0a1 == 1.0.a1`
* `1.0alpha1 < 1.0alpha2 < 1.0beta1 < 1.0rc1 < 1.0`
* Awareness of prerelease keywords: while `1.0 < 1.0a-1` (_a_ treated
as version addendum), but `1.0alpha-1 < 1.0` (_alpha_ is treated
as prerelease marker)
* Awareness of _patch_, _post_ and _pl_ keywords: while `1.0alpha1 < 1.0`
(_alpha_ is pre-release), but `1.0 < 1.0patch1 < 1.1` (_patch_ is post-release)
* Customizable handling of _p_ keyword (it may mean either _patch_ or _pre_,
and since libversion cannot guess, this is controlled with an external flag)

See [ALGORITHM.md](https://github.com/repology/libversion/blob/master/doc/ALGORITHM.md) for more elaborate description
of inner logic.

## API

### Version comparison

```
fn version_compare2(v1: &str, v2: &str);
fn version_compare4(v1: &str, v2: &str, v1_flags: Flags, int v2_flags: Flags);
```

Compares version strings `v1` and `v2`.

Returns **-1** if `v1` is lower than `v2`, **0** if `v1` is equal to `v2` and **1** if `v1` is higher than `v2`.

Thread safe, does not produce errors, does not allocate dynamic memory (this needs to checked for rust implementation),
O(N) computational complexity, O(1) stack memory requirements.

4-argument form allows specifying flags for each version argument to
tune comparison behavior in specific cases. Currently supported `flags`
values are:

* `Flags:PIsPatch` _p_ letter is treated as _patch_
(post-release) instead of _pre_ (pre-release).
* `Flags::AnyIsPatch` any letter sequence is treated as
post-release (useful for handling patchsets as in
`1.2foopatchset3.barpatchset4`).
* `Flags::LowerBound` derive lowest possible version with
the given prefix. For example, lower bound for `1.0` is such
imaginary version `?` that it's higher than any release before
`1.0` and lower than any prerelease of `1.0`.
E.g. `0.999` < lower bound(`1.0`) < `1.0alpha0`.
* `Flags::UpperBound` derive highest possible version with
the given prefix. Opposite of `Flags::LowerBound`.

If both `flags` are zero, `version_compare4` acts exactly the same
as `version_compare2`.

## Example

(TODO: check that this compiles after publishing the crate)

```c
use libversion::{Flags, version_compare2, version_compare4};

int main() {
// 0.99 < 1.11
assert_eq!(version_compare2("0.99", "1.11"), -1);

// 1.0 == 1.0.0
assert_eq!(version_compare2("1.0", "1.0.0"), 0);

// 1.0alpha1 < 1.0.rc1
assert_eq!(version_compare2("1.0alpha1", "1.0.rc1"), -1);

// 1.0 > 1.0.rc1
assert_eq!(version_compare2("1.0", "1.0-rc1"), 1);

// 1.2.3alpha4 is the same as 1.2.3~a4
assert_eq!(version_compare2("1.2.3alpha4", "1.2.3~a4"), 0);

// by default, `p' is treated as `pre'...
assert_eq!(version_compare2("1.0p1", "1.0pre1"), 0);
assert_eq!(version_compare2("1.0p1", "1.0post1"), -1);
assert_eq!(version_compare2("1.0p1", "1.0patch1"), -1);

// ...but this is tunable: here it's handled as `patch`
assert_eq!(version_compare4("1.0p1", "1.0pre1", Flags::PIsPatch, Flags::empty()), 1);
assert_eq!(version_compare4("1.0p1", "1.0post1", Flags::PIsPatch, Flags::empty()), 0);
assert_eq!(version_compare4("1.0p1", "1.0patch1", Flags::PIsPatch, Flags::empty()), 0);

// a way to check that the version belongs to a given release
assert_eq!(version_compare4("1.0alpha1", "1.0", Flags::empty(), Flags::LowerBound), 1);
assert_eq!(version_compare4("1.0alpha1", "1.0", Flags::empty(), Flags::UpperBound), -1);

assert_eq!(version_compare4("1.0.1", "1.0", Flags::empty(), Flags::LowerBound), 1);
assert_eq!(version_compare4("1.0.1", "1.0", Flags::empty(), Flags::UpperBound), -1);
// 1.0alpha1 and 1.0.1 belong to 1.0 release, e.g. they lie between
// (lowest possible version in 1.0) and (highest possible version in 1.0)
}
```

## Bindings and compatible implementations

* Python: [py-libversion](https://github.com/repology/py-libversion) by @AMDmi3
* Go: [golibversion](https://github.com/saenai255/golibversion) by @saenai255
* Rust: [libversion-rs](https://github.com/repology/libversion-rs) by @AMDmi3

## Author

* [Dmitry Marakasov](https://github.com/AMDmi3) <[email protected]>

## License

* [MIT](COPYING)
38 changes: 38 additions & 0 deletions libversion/src/component.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#[derive(Debug, PartialEq, Eq)]
pub enum Component<'a> {
LowerBound,
PreRelease(u8),
Zero,
PostRelease(u8),
NonZero(&'a str),
LetterSuffix(u8),
UpperBound,
}

impl Component<'_> {
fn discriminant(&self) -> u8 {
unsafe { *(self as *const Self as *const u8) }
}
}

impl Ord for Component<'_> {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.discriminant()
.cmp(&other.discriminant())
.then_with(|| match (self, other) {
(Component::PreRelease(a), Component::PreRelease(b)) => a.cmp(&b),
(Component::PostRelease(a), Component::PostRelease(b)) => a.cmp(&b),
(Component::NonZero(a), Component::NonZero(b)) => {
a.len().cmp(&b.len()).then_with(|| a.cmp(&b))
}
(Component::LetterSuffix(a), Component::LetterSuffix(b)) => a.cmp(&b),
_ => std::cmp::Ordering::Equal,
})
}
}

impl PartialOrd for Component<'_> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
45 changes: 45 additions & 0 deletions libversion/src/iter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use crate::component::Component;
use crate::parse::{get_next_version_component, SomeComponents};
use crate::Flags;
use std::mem;

pub struct VersionComponentIterator<'a> {
rest_of_version: &'a str,
carried_component: Option<Component<'a>>,
flags: Flags,
}

impl VersionComponentIterator<'_> {
pub fn new<'a>(version: &'a str, flags: Flags) -> VersionComponentIterator<'a> {
return VersionComponentIterator {
rest_of_version: version,
carried_component: None,
flags,
};
}

pub fn next(&mut self) -> Component {
if let Some(component) = mem::take(&mut self.carried_component) {
return component;
}

let (components, rest_of_version) =
get_next_version_component(self.rest_of_version, self.flags);

self.rest_of_version = rest_of_version;

match components {
SomeComponents::One(component) => {
return component;
}
SomeComponents::Two(component1, component2) => {
self.carried_component = Some(component2);
return component1;
}
}
}

pub fn is_exhausted(&self) -> bool {
return self.rest_of_version.is_empty() && self.carried_component.is_none();
}
}
Loading

0 comments on commit 19f0847

Please sign in to comment.