use std::borrow::Borrow;
use std::cmp::Ordering;
use std::fmt;
use std::iter::Peekable;
use std::slice::Iter;
use crate::{Cmp, Manifest, Part};
#[derive(Clone, Eq)]
pub struct Version<'a> {
version: &'a str,
parts: Vec<Part<'a>>,
manifest: Option<&'a Manifest>,
}
impl<'a> Version<'a> {
pub fn from(version: &'a str) -> Option<Self> {
Some(Version {
version,
parts: split_version_str(version, None)?,
manifest: None,
})
}
pub fn from_parts(version: &'a str, parts: Vec<Part<'a>>) -> Self {
Version {
version,
parts,
manifest: None,
}
}
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),
})
}
pub fn manifest(&self) -> Option<&Manifest> {
self.manifest
}
pub fn has_manifest(&self) -> bool {
self.manifest().is_some()
}
pub fn set_manifest(&mut self, manifest: Option<&'a Manifest>) {
self.manifest = manifest;
}
pub fn as_str(&self) -> &str {
self.version
}
#[allow(clippy::result_map_unit_fn)]
pub fn part(&self, index: usize) -> Result<Part<'a>, ()> {
if index >= self.parts.len() {
return Err(());
}
Ok(self.parts[index])
}
pub fn parts(&self) -> &[Part<'a>] {
self.parts.as_slice()
}
pub fn compare<V>(&self, other: V) -> Cmp
where
V: Borrow<Version<'a>>,
{
compare_iter(
self.parts.iter().peekable(),
other.borrow().parts.iter().peekable(),
)
}
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)
}
}
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)
}
}
}
impl<'a> PartialOrd for Version<'a> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.compare(other).ord().unwrap())
}
}
impl<'a> PartialEq for Version<'a> {
fn eq(&self, other: &Self) -> bool {
self.compare_to(other, Cmp::Eq)
}
}
fn split_version_str<'a>(
version: &'a str,
manifest: Option<&'a Manifest>,
) -> Option<Vec<Part<'a>>> {
let split = version.split(|c| !char::is_alphanumeric(c));
let mut parts = Vec::new();
let mut used_manifest = &Manifest::default();
if let Some(m) = manifest {
used_manifest = m;
}
for part in split {
if used_manifest.max_depth.is_some() && parts.len() >= used_manifest.max_depth.unwrap_or(0)
{
break;
}
if part.is_empty() {
continue;
}
match part.parse::<i32>() {
Ok(number) => {
parts.push(Part::Number(number));
}
Err(_) => {
if used_manifest.ignore_text {
continue;
}
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;
}
parts.push(Part::Text(part))
}
}
}
if !parts.is_empty() && !parts.iter().any(|p| matches!(p, Part::Number(_))) {
return None;
}
Some(parts)
}
fn compare_iter<'a>(
mut iter: Peekable<Iter<Part<'a>>>,
mut other_iter: Peekable<Iter<Part<'a>>>,
) -> Cmp {
let mut other_part: Option<&Part>;
for part in &mut iter {
other_part = other_iter.next();
if other_part.is_none() {
match part {
Part::Number(num) => {
if *num == 0 {
continue;
}
}
Part::Text(_) => return Cmp::Lt,
}
return Cmp::Gt;
}
if let Part::Number(num) = part {
if let Part::Number(other) = other_part.unwrap() {
match num {
n if n < other => return Cmp::Lt,
n if n > other => return Cmp::Gt,
_ => continue,
}
}
}
else if let Part::Text(val) = part {
if let Part::Text(other_val) = other_part.unwrap() {
let (val_lwr, other_val_lwr) = (val.to_lowercase(), other_val.to_lowercase());
#[allow(clippy::comparison_chain)]
if val_lwr < other_val_lwr {
return Cmp::Lt;
} else if val_lwr > other_val_lwr {
return Cmp::Gt;
}
}
}
}
match other_iter.peek() {
Some(_) => compare_iter(other_iter, iter).flip(),
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]
fn from() {
for version in VERSIONS {
assert!(Version::from(version.0).is_some());
}
for version in VERSIONS_ERROR {
assert!(Version::from(version.0).is_none());
}
}
#[test]
fn from_manifest() {
let manifest = Manifest::default();
for version in VERSIONS {
assert_eq!(
Version::from_manifest(version.0, &manifest)
.unwrap()
.manifest,
Some(&manifest)
);
}
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() {
for version in VERSIONS {
assert_eq!(Version::from(version.0).unwrap().as_str(), version.0);
}
}
#[test]
fn part() {
for version in VERSIONS {
let ver = Version::from(version.0).unwrap();
for i in 0..version.1 {
assert_eq!(ver.part(i), Ok(ver.parts[i]));
}
assert!(ver.part(version.1).is_err());
}
}
#[test]
fn parts() {
for version in VERSIONS {
assert_eq!(Version::from(version.0).unwrap().parts().len(), version.1);
}
}
#[test]
fn parts_max_depth() {
let mut manifest = Manifest::default();
for depth in 0..5 {
manifest.max_depth = if depth > 0 { Some(depth) } else { None };
for version in VERSIONS {
let ver = Version::from_manifest(&version.0, &manifest);
if ver.is_none() {
continue;
}
let count = ver.unwrap().parts().len();
if depth == 0 {
assert_eq!(count, version.1);
} else {
assert_eq!(count, cmp::min(version.1, depth));
}
}
}
}
#[test]
fn parts_ignore_text() {
let mut manifest = Manifest::default();
for ignore in vec![true, false] {
manifest.ignore_text = ignore;
let mut had_text = false;
for version in VERSIONS {
let ver = Version::from_manifest(&version.0, &manifest).unwrap();
for part in ver.parts() {
match part {
Part::Text(_) => {
had_text = true;
if !ignore {
break;
}
}
_ => {}
}
}
}
assert_eq!(had_text, !ignore);
}
}
#[test]
fn compare() {
for entry in COMBIS {
let a = Version::from(entry.0).unwrap();
let b = Version::from(entry.1).unwrap();
assert_eq!(
a.compare(b),
entry.2.clone(),
"Testing that {} is {} {}",
entry.0,
entry.2.sign(),
entry.1,
);
}
}
#[test]
fn compare_to() {
for entry in COMBIS {
let a = Version::from(entry.0).unwrap();
let b = Version::from(entry.1).unwrap();
assert!(a.compare_to(&b, entry.2));
assert!(!a.compare_to(b, entry.2.invert()));
}
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() {
for entry in COMBIS {
let a = Version::from(entry.0).unwrap();
let b = Version::from(entry.1).unwrap();
match entry.2 {
Cmp::Eq => assert!(a == b),
Cmp::Lt => assert!(a < b),
Cmp::Gt => assert!(a > b),
_ => {}
}
}
}
#[test]
fn partial_eq() {
for entry in COMBIS {
match entry.2 {
Cmp::Le | Cmp::Ge => continue,
_ => {}
}
let a = Version::from(entry.0).unwrap();
let b = Version::from(entry.1).unwrap();
let result = match entry.2 {
Cmp::Eq => true,
_ => false,
};
assert_eq!(a == b, result);
}
assert!(Version::from("1.2").unwrap() != Version::from("1.2.3").unwrap());
}
}