use std::cmp::Ordering;
use std::collections::Bound;
use std::ops::Deref;
use version_ranges::Ranges;
use crate::{
LocalVersion, LocalVersionSlice, Operator, Prerelease, Version, VersionSpecifier,
VersionSpecifiers,
};
impl From<VersionSpecifiers> for Ranges<Version> {
fn from(specifiers: VersionSpecifiers) -> Self {
let mut range = Self::full();
for specifier in specifiers {
range = range.intersection(&Self::from(specifier));
}
range
}
}
impl From<VersionSpecifier> for Ranges<Version> {
fn from(specifier: VersionSpecifier) -> Self {
let VersionSpecifier { operator, version } = specifier;
match operator {
Operator::Equal => match version.local() {
LocalVersionSlice::Segments(&[]) => {
let low = version;
let high = low.clone().with_local(LocalVersion::Max);
Self::between(low, high)
}
LocalVersionSlice::Segments(_) => Self::singleton(version),
LocalVersionSlice::Max => unreachable!(
"found `LocalVersionSlice::Sentinel`, which should be an internal-only value"
),
},
Operator::ExactEqual => Self::singleton(version),
Operator::NotEqual => Self::from(VersionSpecifier {
operator: Operator::Equal,
version,
})
.complement(),
Operator::TildeEqual => {
let release = version.release();
let [rest @ .., last, _] = &*release else {
unreachable!("~= must have at least two segments");
};
let upper = Version::new(rest.iter().chain([&(last + 1)]))
.with_epoch(version.epoch())
.with_dev(Some(0));
Self::from_range_bounds(version..upper)
}
Operator::LessThan => {
if version.any_prerelease() {
Self::strictly_lower_than(version)
} else if let Some(post) = version.post() {
let base = version.clone().with_post(None);
let lower = Self::strictly_lower_than(base.clone().with_min(Some(0)));
let upper = Self::from_range_bounds(base..version.with_post(Some(post)));
lower.union(&upper)
} else {
Self::strictly_lower_than(version.with_min(Some(0)))
}
}
Operator::LessThanEqual => Self::lower_than(version.with_local(LocalVersion::Max)),
Operator::GreaterThan => {
if let Some(dev) = version.dev() {
Self::higher_than(version.with_dev(Some(dev + 1)))
} else if let Some(post) = version.post() {
Self::higher_than(version.with_post(Some(post + 1)))
} else {
Self::strictly_higher_than(version.with_max(Some(0)))
}
}
Operator::GreaterThanEqual => Self::higher_than(version),
Operator::EqualStar => {
let low = version.with_dev(Some(0));
let mut high = low.clone();
if let Some(post) = high.post() {
high = high.with_post(Some(post + 1));
} else if let Some(pre) = high.pre() {
high = high.with_pre(Some(Prerelease {
kind: pre.kind,
number: pre.number + 1,
}));
} else {
let mut release = high.release().to_vec();
*release.last_mut().unwrap() += 1;
high = high.with_release(release);
}
Self::from_range_bounds(low..high)
}
Operator::NotEqualStar => {
let low = version.with_dev(Some(0));
let mut high = low.clone();
if let Some(post) = high.post() {
high = high.with_post(Some(post + 1));
} else if let Some(pre) = high.pre() {
high = high.with_pre(Some(Prerelease {
kind: pre.kind,
number: pre.number + 1,
}));
} else {
let mut release = high.release().to_vec();
*release.last_mut().unwrap() += 1;
high = high.with_release(release);
}
Self::from_range_bounds(low..high).complement()
}
}
}
}
pub fn release_specifiers_to_ranges(specifiers: VersionSpecifiers) -> Ranges<Version> {
let mut range = Ranges::full();
for specifier in specifiers {
range = range.intersection(&release_specifier_to_range(specifier, false));
}
range
}
pub fn release_specifier_to_range(specifier: VersionSpecifier, trim: bool) -> Ranges<Version> {
let VersionSpecifier { operator, version } = specifier;
let version_trimmed = if trim {
version.only_release_trimmed()
} else {
version.only_release()
};
match operator {
Operator::Equal => Ranges::singleton(version_trimmed),
Operator::ExactEqual => Ranges::singleton(version_trimmed),
Operator::NotEqual => Ranges::singleton(version_trimmed).complement(),
Operator::LessThan => Ranges::strictly_lower_than(version_trimmed),
Operator::LessThanEqual => Ranges::lower_than(version_trimmed),
Operator::GreaterThan => Ranges::strictly_higher_than(version_trimmed),
Operator::GreaterThanEqual => Ranges::higher_than(version_trimmed),
Operator::TildeEqual => {
let release = version.release();
let [rest @ .., last, _] = &*release else {
unreachable!("~= must have at least two segments");
};
let upper = Version::new(rest.iter().chain([&(last + 1)]));
Ranges::from_range_bounds(version_trimmed..upper)
}
Operator::EqualStar => {
let low_full = version.only_release();
let high = {
let mut high = low_full.clone();
let mut release = high.release().to_vec();
*release.last_mut().unwrap() += 1;
high = high.with_release(release);
high
};
Ranges::from_range_bounds(version..high)
}
Operator::NotEqualStar => {
let low_full = version.only_release();
let high = {
let mut high = low_full.clone();
let mut release = high.release().to_vec();
*release.last_mut().unwrap() += 1;
high = high.with_release(release);
high
};
Ranges::from_range_bounds(version..high).complement()
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct LowerBound(pub Bound<Version>);
impl LowerBound {
pub fn new(bound: Bound<Version>) -> Self {
Self(match bound {
Bound::Included(version) => Bound::Included(version.only_release_trimmed()),
Bound::Excluded(version) => Bound::Excluded(version.only_release_trimmed()),
Bound::Unbounded => Bound::Unbounded,
})
}
#[must_use]
pub fn major_minor(&self) -> Self {
match &self.0 {
Bound::Included(version) => Self(Bound::Included(Version::new(
version.release().iter().take(2),
))),
Bound::Excluded(version) => Self(Bound::Included(Version::new(
version.release().iter().take(2),
))),
Bound::Unbounded => Self(Bound::Unbounded),
}
}
pub fn contains(&self, version: &Version) -> bool {
match self.0 {
Bound::Included(ref bound) => bound <= version,
Bound::Excluded(ref bound) => bound < version,
Bound::Unbounded => true,
}
}
pub fn specifier(&self) -> Option<VersionSpecifier> {
match &self.0 {
Bound::Included(version) => Some(VersionSpecifier::greater_than_equal_version(
version.clone(),
)),
Bound::Excluded(version) => {
Some(VersionSpecifier::greater_than_version(version.clone()))
}
Bound::Unbounded => None,
}
}
}
impl PartialOrd for LowerBound {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for LowerBound {
fn cmp(&self, other: &Self) -> Ordering {
let left = self.0.as_ref();
let right = other.0.as_ref();
match (left, right) {
(Bound::Unbounded, Bound::Unbounded) => Ordering::Equal,
(Bound::Included(_left), Bound::Unbounded) => Ordering::Greater,
(Bound::Excluded(_left), Bound::Unbounded) => Ordering::Greater,
(Bound::Unbounded, Bound::Included(_right)) => Ordering::Less,
(Bound::Included(left), Bound::Included(right)) => left.cmp(right),
(Bound::Excluded(left), Bound::Included(right)) => match left.cmp(right) {
Ordering::Less => Ordering::Less,
Ordering::Equal => Ordering::Greater,
Ordering::Greater => Ordering::Greater,
},
(Bound::Unbounded, Bound::Excluded(_right)) => Ordering::Less,
(Bound::Included(left), Bound::Excluded(right)) => match left.cmp(right) {
Ordering::Less => Ordering::Less,
Ordering::Equal => Ordering::Less,
Ordering::Greater => Ordering::Greater,
},
(Bound::Excluded(left), Bound::Excluded(right)) => left.cmp(right),
}
}
}
impl Default for LowerBound {
fn default() -> Self {
Self(Bound::Unbounded)
}
}
impl Deref for LowerBound {
type Target = Bound<Version>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<LowerBound> for Bound<Version> {
fn from(bound: LowerBound) -> Self {
bound.0
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct UpperBound(pub Bound<Version>);
impl UpperBound {
pub fn new(bound: Bound<Version>) -> Self {
Self(match bound {
Bound::Included(version) => Bound::Included(version.only_release_trimmed()),
Bound::Excluded(version) => Bound::Excluded(version.only_release_trimmed()),
Bound::Unbounded => Bound::Unbounded,
})
}
#[must_use]
pub fn major_minor(&self) -> Self {
match &self.0 {
Bound::Included(version) => Self(Bound::Included(Version::new(
version.release().iter().take(2),
))),
Bound::Excluded(version) => {
if version.release().get(2).is_some_and(|patch| *patch > 0) {
Self(Bound::Included(Version::new(
version.release().iter().take(2),
)))
} else {
Self(Bound::Excluded(Version::new(
version.release().iter().take(2),
)))
}
}
Bound::Unbounded => Self(Bound::Unbounded),
}
}
pub fn contains(&self, version: &Version) -> bool {
match self.0 {
Bound::Included(ref bound) => bound >= version,
Bound::Excluded(ref bound) => bound > version,
Bound::Unbounded => true,
}
}
pub fn specifier(&self) -> Option<VersionSpecifier> {
match &self.0 {
Bound::Included(version) => {
Some(VersionSpecifier::less_than_equal_version(version.clone()))
}
Bound::Excluded(version) => Some(VersionSpecifier::less_than_version(version.clone())),
Bound::Unbounded => None,
}
}
}
impl PartialOrd for UpperBound {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for UpperBound {
fn cmp(&self, other: &Self) -> Ordering {
let left = self.0.as_ref();
let right = other.0.as_ref();
match (left, right) {
(Bound::Unbounded, Bound::Unbounded) => Ordering::Equal,
(Bound::Included(_left), Bound::Unbounded) => Ordering::Less,
(Bound::Excluded(_left), Bound::Unbounded) => Ordering::Less,
(Bound::Unbounded, Bound::Included(_right)) => Ordering::Greater,
(Bound::Included(left), Bound::Included(right)) => left.cmp(right),
(Bound::Excluded(left), Bound::Included(right)) => match left.cmp(right) {
Ordering::Less => Ordering::Less,
Ordering::Equal => Ordering::Less,
Ordering::Greater => Ordering::Greater,
},
(Bound::Unbounded, Bound::Excluded(_right)) => Ordering::Greater,
(Bound::Included(left), Bound::Excluded(right)) => match left.cmp(right) {
Ordering::Less => Ordering::Less,
Ordering::Equal => Ordering::Greater,
Ordering::Greater => Ordering::Greater,
},
(Bound::Excluded(left), Bound::Excluded(right)) => left.cmp(right),
}
}
}
impl Default for UpperBound {
fn default() -> Self {
Self(Bound::Unbounded)
}
}
impl Deref for UpperBound {
type Target = Bound<Version>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<UpperBound> for Bound<Version> {
fn from(bound: UpperBound) -> Self {
bound.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn less_than_post_release() {
let specifier: VersionSpecifier = "<0.12.0.post2".parse().unwrap();
let range = Ranges::<Version>::from(specifier);
let v = "0.11.0".parse::<Version>().unwrap();
assert!(range.contains(&v), "should include 0.11.0");
let v = "0.12.0a1".parse::<Version>().unwrap();
assert!(!range.contains(&v), "should exclude 0.12.0a1");
let v = "0.12.0b1".parse::<Version>().unwrap();
assert!(!range.contains(&v), "should exclude 0.12.0b1");
let v = "0.12.0rc1".parse::<Version>().unwrap();
assert!(!range.contains(&v), "should exclude 0.12.0rc1");
let v = "0.12.0.dev0".parse::<Version>().unwrap();
assert!(!range.contains(&v), "should exclude 0.12.0.dev0");
let v = "0.12.0a1.post1".parse::<Version>().unwrap();
assert!(!range.contains(&v), "should exclude 0.12.0a1.post1");
let v = "0.12.0b1.post1".parse::<Version>().unwrap();
assert!(!range.contains(&v), "should exclude 0.12.0b1.post1");
let v = "0.12.0".parse::<Version>().unwrap();
assert!(range.contains(&v), "should include 0.12.0");
let v = "0.12.0.post1".parse::<Version>().unwrap();
assert!(range.contains(&v), "should include 0.12.0.post1");
let v = "0.12.0.post2".parse::<Version>().unwrap();
assert!(!range.contains(&v), "should exclude 0.12.0.post2");
let v = "0.13.0".parse::<Version>().unwrap();
assert!(!range.contains(&v), "should exclude 0.13.0");
}
#[test]
fn less_than_final_release() {
let specifier: VersionSpecifier = "<0.12.0".parse().unwrap();
let range = Ranges::<Version>::from(specifier);
let v = "0.11.0".parse::<Version>().unwrap();
assert!(range.contains(&v), "should include 0.11.0");
let v = "0.12.0a1".parse::<Version>().unwrap();
assert!(!range.contains(&v), "should exclude 0.12.0a1");
let v = "0.12.0.dev0".parse::<Version>().unwrap();
assert!(!range.contains(&v), "should exclude 0.12.0.dev0");
let v = "0.12.0".parse::<Version>().unwrap();
assert!(!range.contains(&v), "should exclude 0.12.0");
let v = "0.12.0.post1".parse::<Version>().unwrap();
assert!(!range.contains(&v), "should exclude 0.12.0.post1");
}
#[test]
fn less_than_pre_release() {
let specifier: VersionSpecifier = "<0.12.0b1".parse().unwrap();
let range = Ranges::<Version>::from(specifier);
let v = "0.12.0a1".parse::<Version>().unwrap();
assert!(range.contains(&v), "should include 0.12.0a1");
let v = "0.12.0.dev0".parse::<Version>().unwrap();
assert!(range.contains(&v), "should include 0.12.0.dev0");
let v = "0.12.0b1".parse::<Version>().unwrap();
assert!(!range.contains(&v), "should exclude 0.12.0b1");
let v = "0.12.0".parse::<Version>().unwrap();
assert!(!range.contains(&v), "should exclude 0.12.0");
}
#[test]
fn less_than_post_zero() {
let specifier: VersionSpecifier = "<0.12.0.post0".parse().unwrap();
let range = Ranges::<Version>::from(specifier);
let v = "0.11.0".parse::<Version>().unwrap();
assert!(range.contains(&v), "should include 0.11.0");
let v = "0.12.0a1".parse::<Version>().unwrap();
assert!(!range.contains(&v), "should exclude 0.12.0a1");
let v = "0.12.0".parse::<Version>().unwrap();
assert!(range.contains(&v), "should include 0.12.0");
let v = "0.12.0.post0".parse::<Version>().unwrap();
assert!(!range.contains(&v), "should exclude 0.12.0.post0");
let v = "0.12.0.post1".parse::<Version>().unwrap();
assert!(!range.contains(&v), "should exclude 0.12.0.post1");
}
#[test]
fn u64_max_version_segments_rejected_at_parse_time() {
assert!(
"~=18446744073709551615.0"
.parse::<VersionSpecifier>()
.is_err()
);
assert!(
"==18446744073709551615.*"
.parse::<VersionSpecifier>()
.is_err()
);
assert!(
"~=18446744073709551614.0"
.parse::<VersionSpecifier>()
.is_ok()
);
assert!(
"==18446744073709551614.*"
.parse::<VersionSpecifier>()
.is_ok()
);
}
}