use std::{
cmp::Ordering,
iter::Peekable,
str::{CharIndices, Chars, FromStr},
};
use crate::PackageVersion;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum VersionSegment<'a> {
Segment {
text: &'a str,
delimiter_count: usize,
},
SubSegment {
text: &'a str,
},
}
impl<'a> VersionSegment<'a> {
pub fn text(&self) -> &str {
match self {
VersionSegment::Segment { text, .. } | VersionSegment::SubSegment { text } => text,
}
}
pub fn is_empty(&self) -> bool {
match self {
VersionSegment::Segment { text, .. } | VersionSegment::SubSegment { text } => {
text.is_empty()
}
}
}
pub fn chars(&self) -> Chars<'a> {
match self {
VersionSegment::Segment { text, .. } | VersionSegment::SubSegment { text } => {
text.chars()
}
}
}
pub fn parse<T: FromStr>(&self) -> Result<T, T::Err> {
match self {
VersionSegment::Segment { text, .. } | VersionSegment::SubSegment { text } => {
FromStr::from_str(text)
}
}
}
pub fn str_cmp(&self, other: &VersionSegment) -> Ordering {
match self {
VersionSegment::Segment { text, .. } | VersionSegment::SubSegment { text } => {
text.cmp(&other.text())
}
}
}
}
#[derive(Debug)]
pub struct VersionSegments<'a> {
version: &'a str,
version_chars: Peekable<CharIndices<'a>>,
in_segment: bool,
}
impl<'a> VersionSegments<'a> {
pub fn new(version: &'a str) -> Self {
VersionSegments {
version,
version_chars: version.char_indices().peekable(),
in_segment: false,
}
}
}
impl<'a> Iterator for VersionSegments<'a> {
type Item = VersionSegment<'a>;
fn next(&mut self) -> Option<VersionSegment<'a>> {
let mut delimiter_count = 0;
while let Some((_, char)) = self.version_chars.peek() {
if char.is_alphanumeric() {
break;
}
self.version_chars.next();
delimiter_count += 1;
self.in_segment = false;
continue;
}
let Some((first_index, first_char)) = self.version_chars.next() else {
if delimiter_count == 0 {
return None;
}
return Some(VersionSegment::Segment {
text: "",
delimiter_count,
});
};
let mut last_char = first_char;
let mut last_char_index = first_index;
let is_numeric = first_char.is_numeric();
if is_numeric {
#[allow(clippy::while_let_on_iterator)]
while let Some((index, next_char)) =
self.version_chars.next_if(|(_, peek)| peek.is_numeric())
{
last_char_index = index;
last_char = next_char;
}
} else {
#[allow(clippy::while_let_on_iterator)]
while let Some((index, next_char)) =
self.version_chars.next_if(|(_, peek)| peek.is_alphabetic())
{
last_char_index = index;
last_char = next_char;
}
}
let segment_slice = &self.version[first_index..(last_char_index + last_char.len_utf8())];
if !self.in_segment {
self.in_segment = true;
Some(VersionSegment::Segment {
text: segment_slice,
delimiter_count,
})
} else {
Some(VersionSegment::SubSegment {
text: segment_slice,
})
}
}
}
impl Ord for PackageVersion {
fn cmp(&self, other: &Self) -> Ordering {
if self.inner() == other.inner() {
return Ordering::Equal;
}
let mut self_segments = self.segments();
let mut other_segments = other.segments();
loop {
let self_segment = self_segments.next();
let other_segment = other_segments.next();
let (self_segment, other_segment) = match (self_segment, other_segment) {
(Some(self_seg), Some(other_seg)) => (self_seg, other_seg),
(None, None) => return Ordering::Equal,
(Some(seg), None) => {
let text = match seg {
VersionSegment::Segment { .. } => return Ordering::Greater,
VersionSegment::SubSegment { text } => text,
};
if !text.is_empty() && text.chars().all(char::is_alphabetic) {
return Ordering::Less;
}
return Ordering::Greater;
}
(None, Some(seg)) => {
let text = match seg {
VersionSegment::Segment { .. } => return Ordering::Less,
VersionSegment::SubSegment { text } => text,
};
if !text.is_empty() && text.chars().all(char::is_alphabetic) {
return Ordering::Greater;
}
if !text.is_empty() && text.chars().all(char::is_alphabetic) {
return Ordering::Greater;
}
return Ordering::Less;
}
};
if other_segment.is_empty() && self_segment.is_empty() {
return Ordering::Equal;
} else if self_segment.is_empty() {
if other_segment.chars().all(char::is_alphabetic) {
return Ordering::Greater;
}
return Ordering::Less;
} else if other_segment.is_empty() {
if self_segment.chars().all(char::is_alphabetic) {
return Ordering::Less;
}
return Ordering::Greater;
}
let (self_text, other_text) = match (self_segment, other_segment) {
(
VersionSegment::Segment {
delimiter_count: self_count,
text: self_text,
},
VersionSegment::Segment {
delimiter_count: other_count,
text: other_text,
},
) => {
if self_count != other_count {
return self_count.cmp(&other_count);
}
(self_text, other_text)
}
(VersionSegment::Segment { .. }, VersionSegment::SubSegment { .. }) => {
return Ordering::Greater;
}
(VersionSegment::SubSegment { .. }, VersionSegment::Segment { .. }) => {
return Ordering::Less;
}
(
VersionSegment::SubSegment { text: self_text },
VersionSegment::SubSegment { text: other_text },
) => (self_text, other_text),
};
let self_is_numeric = !self_text.is_empty() && self_text.chars().all(char::is_numeric);
let other_is_numeric =
!other_text.is_empty() && other_text.chars().all(char::is_numeric);
if self_is_numeric && !other_is_numeric {
return Ordering::Greater;
} else if !self_is_numeric && other_is_numeric {
return Ordering::Less;
} else if self_is_numeric && other_is_numeric {
let ordering = self_text
.parse::<usize>()
.unwrap()
.cmp(&other_text.parse::<usize>().unwrap());
match ordering {
Ordering::Less => return Ordering::Less,
Ordering::Greater => return Ordering::Greater,
Ordering::Equal => continue,
}
}
match self_text.cmp(other_text) {
Ordering::Less => return Ordering::Less,
Ordering::Greater => return Ordering::Greater,
Ordering::Equal => continue,
}
}
}
}
impl PartialOrd for PackageVersion {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for PackageVersion {
fn eq(&self, other: &Self) -> bool {
self.cmp(other).is_eq()
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
#[case("1.0.0", vec![
VersionSegment::Segment{ text:"1", delimiter_count: 0},
VersionSegment::Segment{ text:"0", delimiter_count: 1},
VersionSegment::Segment{ text:"0", delimiter_count: 1},
])]
#[case("1..0", vec![
VersionSegment::Segment{ text:"1", delimiter_count: 0},
VersionSegment::Segment{ text:"0", delimiter_count: 2},
])]
#[case("1.0.", vec![
VersionSegment::Segment{ text:"1", delimiter_count: 0},
VersionSegment::Segment{ text:"0", delimiter_count: 1},
VersionSegment::Segment{ text:"", delimiter_count: 1},
])]
#[case("1..", vec![
VersionSegment::Segment{ text:"1", delimiter_count: 0},
VersionSegment::Segment{ text:"", delimiter_count: 2},
])]
#[case("1...", vec![
VersionSegment::Segment{ text:"1", delimiter_count: 0},
VersionSegment::Segment{ text:"", delimiter_count: 3},
])]
#[case("1.🗻lol.0", vec![
VersionSegment::Segment{ text:"1", delimiter_count: 0},
VersionSegment::Segment{ text:"lol", delimiter_count: 2},
VersionSegment::Segment{ text:"0", delimiter_count: 1},
])]
#[case("1.🗻lol.", vec![
VersionSegment::Segment{ text:"1", delimiter_count: 0},
VersionSegment::Segment{ text:"lol", delimiter_count: 2},
VersionSegment::Segment{ text:"", delimiter_count: 1},
])]
#[case("20220202", vec![
VersionSegment::Segment{ text:"20220202", delimiter_count: 0},
])]
#[case("some_string", vec![
VersionSegment::Segment{ text:"some", delimiter_count: 0},
VersionSegment::Segment{ text:"string", delimiter_count: 1}
])]
#[case("alpha7654numeric321", vec![
VersionSegment::Segment{ text:"alpha", delimiter_count: 0},
VersionSegment::SubSegment{ text:"7654"},
VersionSegment::SubSegment{ text:"numeric"},
VersionSegment::SubSegment{ text:"321"},
])]
fn version_segment_iterator(
#[case] version: &str,
#[case] expected_segments: Vec<VersionSegment>,
) {
let version = PackageVersion(version.to_string());
let mut segments_iter = version.segments();
let mut expected_iter = expected_segments.clone().into_iter();
loop {
let next_segment = segments_iter.next();
assert_eq!(
next_segment,
expected_iter.next(),
"Failed for segment {next_segment:?} in version string {version}:\nsegments: {:?}\n expected: {:?}",
version.segments().collect::<Vec<VersionSegment>>(),
expected_segments,
);
if next_segment.is_none() {
break;
}
}
}
}