From f4a45717d2bc952a545f7cee28e941a65744604b Mon Sep 17 00:00:00 2001 From: John Turner Date: Thu, 13 Nov 2025 18:00:15 +0000 Subject: [PATCH] impl version comparison algorithm --- src/atom/mod.rs | 321 +++++++++++++++++++++++++++++++++++++++++++- src/atom/parsers.rs | 38 +++++- 2 files changed, 346 insertions(+), 13 deletions(-) diff --git a/src/atom/mod.rs b/src/atom/mod.rs index eb7537b..0dea415 100644 --- a/src/atom/mod.rs +++ b/src/atom/mod.rs @@ -2,6 +2,7 @@ use core::{ fmt::{self}, option::Option, }; +use std::cmp::Ordering; use crate::useflag::UseFlag; @@ -36,7 +37,10 @@ pub struct Name(#[get(method = "get", kind = "deref")] String); #[derive(Clone, Debug, Get)] pub struct VersionNumber(#[get(method = "get", kind = "deref")] String); -#[derive(Clone, Copy, Debug)] +#[derive(Debug, Clone, Get)] +struct VersionNumbers(#[get(method = "get", kind = "deref")] Vec); + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum VersionSuffixKind { Alpha, Beta, @@ -51,13 +55,14 @@ pub struct VersionSuffix { number: Option, } +#[derive(Debug, Clone, Get)] +pub struct VersionSuffixes(#[get(method = "get", kind = "deref")] Vec); + #[derive(Clone, Debug, Get)] pub struct Version { - #[get(kind = "deref")] - numbers: Vec, + numbers: VersionNumbers, letter: Option, - #[get(kind = "deref")] - suffixes: Vec, + suffixes: VersionSuffixes, rev: Option, } @@ -103,7 +108,7 @@ pub struct UseDep { condition: Option, } -#[derive(Clone, Debug, Get)] +#[derive(Clone, Debug, Get, PartialEq, Eq)] pub struct Atom { blocker: Option, category: Category, @@ -123,6 +128,239 @@ impl Atom { } } +impl PartialEq for VersionSuffix { + fn eq(&self, other: &Self) -> bool { + self.kind == other.kind + && match dbg!((&self.number, &other.number)) { + (Some(a), Some(b)) => a.0 == b.0, + (Some(a), None) if a.get().chars().all(|c| c == '0') => true, + (None, Some(b)) if b.get().chars().all(|c| c == '0') => true, + (Some(_), None) => false, + (None, Some(_)) => false, + (None, None) => true, + } + } +} + +impl Eq for VersionSuffix {} + +impl PartialOrd for VersionSuffix { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for VersionSuffix { + fn cmp(&self, other: &Self) -> Ordering { + match &self.kind.cmp(&other.kind) { + Ordering::Less => Ordering::Less, + Ordering::Greater => Ordering::Greater, + Ordering::Equal => match (&self.number, &other.number) { + (Some(a), Some(b)) => { + a.0.parse::() + .unwrap() + .cmp(&b.0.parse::().unwrap()) + } + (Some(a), None) if a.get().chars().all(|c| c == '0') => Ordering::Equal, + (None, Some(b)) if b.get().chars().all(|c| c == '0') => Ordering::Equal, + (Some(_), None) => Ordering::Greater, + (None, Some(_)) => Ordering::Less, + (None, None) => Ordering::Equal, + }, + } + } +} + +impl PartialEq for VersionSuffixes { + fn eq(&self, other: &Self) -> bool { + let mut a = self.get().iter(); + let mut b = other.get().iter(); + + loop { + match (a.next(), b.next()) { + (Some(a), Some(b)) if a == b => continue, + (Some(_), Some(_)) => break false, + (None, None) => break true, + _ => break false, + } + } + } +} + +impl Eq for VersionSuffixes {} + +impl PartialOrd for VersionSuffixes { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for VersionSuffixes { + fn cmp(&self, other: &Self) -> Ordering { + let mut a = self.get().iter(); + let mut b = other.get().iter(); + + loop { + match dbg!((a.next(), b.next())) { + (Some(a), Some(b)) => match a.cmp(b) { + Ordering::Less => break Ordering::Less, + Ordering::Greater => break Ordering::Greater, + Ordering::Equal => continue, + }, + (Some(a), None) if matches!(a.kind, VersionSuffixKind::P) => { + break Ordering::Greater; + } + (Some(_), None) => break Ordering::Less, + (None, Some(b)) if matches!(b.kind, VersionSuffixKind::P) => break Ordering::Less, + (None, Some(_)) => break Ordering::Greater, + (None, None) => break Ordering::Equal, + } + } + } +} + +impl PartialEq for VersionNumbers { + fn eq(&self, other: &Self) -> bool { + self.get().first().unwrap().get().parse::().unwrap() + == other.get().first().unwrap().get().parse().unwrap() + && { + let mut a = self.get().iter().skip(1); + let mut b = other.get().iter().skip(1); + + loop { + match (a.next(), b.next()) { + (Some(a), Some(b)) if a.get().starts_with("0") => { + let a = a.get().trim_end_matches("0"); + let b = b.get().trim_end_matches("0"); + + if a != b { + break false; + } + } + (Some(a), Some(b)) => { + if a.get().parse::().unwrap() != b.get().parse::().unwrap() { + break false; + } + } + (Some(a), None) if a.get().chars().all(|c| c == '0') => continue, + (None, Some(b)) if b.get().chars().all(|c| c == '0') => continue, + (None, None) => break true, + _ => break false, + } + } + } + } +} + +impl Eq for VersionNumbers {} + +impl PartialOrd for VersionNumbers { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for VersionNumbers { + fn cmp(&self, other: &Self) -> Ordering { + match self + .get() + .first() + .unwrap() + .get() + .parse::() + .unwrap() + .cmp(&other.get().first().unwrap().get().parse::().unwrap()) + { + Ordering::Less => return Ordering::Less, + Ordering::Greater => return Ordering::Greater, + Ordering::Equal => { + let mut a = self.get().iter().skip(1); + let mut b = other.get().iter().skip(1); + + loop { + match (a.next(), b.next()) { + (Some(a), Some(b)) if a.get().starts_with("0") => { + let a = a.get().trim_end_matches("0"); + let b = b.get().trim_end_matches("0"); + + match a.cmp(b) { + Ordering::Less => break Ordering::Less, + Ordering::Greater => break Ordering::Greater, + Ordering::Equal => continue, + } + } + (Some(a), Some(b)) => match a + .get() + .parse::() + .unwrap() + .cmp(&b.get().parse::().unwrap()) + { + Ordering::Less => break Ordering::Less, + Ordering::Greater => break Ordering::Greater, + Ordering::Equal => continue, + }, + (Some(a), None) if a.get().chars().all(|c| c == '0') => continue, + (None, Some(b)) if b.get().chars().all(|c| c == '0') => continue, + (Some(_), None) => break Ordering::Greater, + (None, Some(_)) => break Ordering::Less, + (None, None) => break Ordering::Equal, + } + } + } + } + } +} + +impl PartialEq for Version { + fn eq(&self, other: &Self) -> bool { + self.numbers == other.numbers + && self.suffixes == other.suffixes + && match (&self.rev, &other.rev) { + (Some(a), Some(b)) => { + a.get().parse::().unwrap() == b.get().parse::().unwrap() + } + (Some(a), None) if a.get().chars().all(|c| c == '0') => true, + (Some(_), None) => false, + (None, Some(b)) if b.get().chars().all(|c| c == '0') => true, + (None, Some(_)) => false, + (None, None) => true, + } + } +} + +impl Eq for Version {} + +impl PartialOrd for Version { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Version { + fn cmp(&self, other: &Self) -> Ordering { + match self.numbers.cmp(&other.numbers) { + Ordering::Less => Ordering::Less, + Ordering::Greater => Ordering::Greater, + Ordering::Equal => match self.suffixes.cmp(&other.suffixes) { + Ordering::Less => Ordering::Less, + Ordering::Greater => Ordering::Greater, + Ordering::Equal => match (&self.rev, &other.rev) { + (Some(a), Some(b)) => a + .get() + .parse::() + .unwrap() + .cmp(&b.get().parse().unwrap()), + (Some(a), None) if a.get().chars().all(|c| c == '0') => Ordering::Equal, + (Some(_), None) => Ordering::Greater, + (None, Some(b)) if b.get().chars().all(|c| c == '0') => Ordering::Equal, + (None, Some(_)) => Ordering::Less, + (None, None) => Ordering::Equal, + }, + }, + } + } +} + impl fmt::Display for Blocker { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -191,6 +429,7 @@ impl fmt::Display for Version { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let numbers = self .numbers + .get() .iter() .map(|n| n.get()) .intersperse(".") @@ -198,6 +437,7 @@ impl fmt::Display for Version { let suffixes = self .suffixes + .get() .iter() .map(|s| s.to_string()) .intersperse("_".to_string()) @@ -346,6 +586,22 @@ mod test { use crate::Parseable; + macro_rules! assert_eq_display { + ($a:expr, $b:expr) => { + if $a != $b { + panic!("{} != {}", $a, $b); + } + }; + } + + macro_rules! assert_cmp_display { + ($a:expr, $b:expr, $ordering:expr) => { + if $a.cmp(&$b) != $ordering { + panic!("{} ~ {} != {:?}", $a, $b, $ordering) + } + }; + } + #[test] fn test_version_display() { let s = "1.0.0_alpha1_beta1-r1"; @@ -361,4 +617,57 @@ mod test { assert_eq!(atom.to_string().as_str(), s); } + + #[test] + fn test_version_suffix_eq() { + let a = VersionSuffix::parser() + .parse_finished(InputIter::new("alpha0")) + .unwrap(); + let b = VersionSuffix::parser() + .parse_finished(InputIter::new("alpha")) + .unwrap(); + + assert_eq_display!(a, b); + } + + #[test] + fn test_version_eq() { + let versions = [ + ("1", "1"), + ("1", "1.0.0"), + ("1.0", "1.0.0"), + ("1.0.0_alpha0", "1.0.0_alpha"), + ("1.0.0", "1.0.0-r0"), + ]; + + for (a, b) in versions.map(|(a, b)| { + ( + Version::parser().parse_finished(InputIter::new(a)).unwrap(), + Version::parser().parse_finished(InputIter::new(b)).unwrap(), + ) + }) { + assert_eq_display!(a, b); + } + } + + #[test] + fn test_version_cmp() { + let versions = [ + ("1.0.1", "1.0", Ordering::Greater), + ("1.0.0", "1.0.0_alpha", Ordering::Greater), + ("1.0.0_alpha", "1.0.0_alpha_p", Ordering::Less), + ("1.0.0-r0", "1.0.0", Ordering::Equal), + ("1.0.0-r0000", "1.0.0", Ordering::Equal), + ]; + + for (a, b, ordering) in versions.iter().map(|(a, b, ordering)| { + ( + Version::parser().parse_finished(InputIter::new(a)).unwrap(), + Version::parser().parse_finished(InputIter::new(b)).unwrap(), + ordering, + ) + }) { + assert_cmp_display!(a, b, *ordering); + } + } } diff --git a/src/atom/parsers.rs b/src/atom/parsers.rs index fd212cd..0f95d16 100644 --- a/src/atom/parsers.rs +++ b/src/atom/parsers.rs @@ -6,8 +6,8 @@ use crate::{ Parseable, atom::{ Atom, Blocker, Category, Name, Slot, SlotName, SlotOperator, UseDep, UseDepCondition, - UseDepNegate, UseDepSign, Version, VersionNumber, VersionOperator, VersionSuffix, - VersionSuffixKind, + UseDepNegate, UseDepSign, Version, VersionNumber, VersionNumbers, VersionOperator, + VersionSuffix, VersionSuffixKind, VersionSuffixes, }, useflag::UseFlag, }; @@ -67,22 +67,45 @@ impl<'a> Parseable<'a, &'a str> for VersionSuffix { } } +impl<'a> Parseable<'a, &'a str> for VersionNumbers { + type Parser = impl Parser<&'a str, Output = Self>; + + fn parser() -> Self::Parser { + VersionNumber::parser() + .followed_by(tag("*").opt()) + .recognize() + .map(|output: &str| VersionNumber(output.to_string())) + .separated_by(tag(".")) + .at_least(1) + .map(|numbers| VersionNumbers(numbers)) + } +} + +impl<'a> Parseable<'a, &'a str> for VersionSuffixes { + type Parser = impl Parser<&'a str, Output = Self>; + + fn parser() -> Self::Parser { + VersionSuffix::parser() + .separated_by(tag("_")) + .many() + .map(|suffixes| VersionSuffixes(suffixes)) + } +} + impl<'a> Parseable<'a, &'a str> for Version { type Parser = impl Parser<&'a str, Output = Self>; fn parser() -> Self::Parser { - let numbers = VersionNumber::parser().separated_by(tag(".")).at_least(1); - let suffixes = VersionSuffix::parser().separated_by(tag("_")).many(); let rev = VersionNumber::parser().preceded_by(tag("-r")); - numbers + VersionNumbers::parser() .and(r#if(|c: &char| c.is_ascii_alphabetic() && c.is_ascii_lowercase()).opt()) - .and(suffixes.preceded_by(tag("_")).opt()) + .and(VersionSuffixes::parser().preceded_by(tag("_")).opt()) .and(rev.opt()) .map(|(((numbers, letter), suffixes), rev)| Version { numbers, letter, - suffixes: suffixes.unwrap_or(Vec::new()), + suffixes: suffixes.unwrap_or(VersionSuffixes(Vec::new())), rev, }) } @@ -298,6 +321,7 @@ impl<'a> Parseable<'a, &'a str> for Atom { Some((_, version)) if !version .numbers() + .get() .iter() .any(|number| number.get().contains("*")) => {