semver_php/constraint/
bound.rs

1use crate::version::version_compare;
2use std::{
3	cmp::Ordering,
4	fmt::{self, Display},
5};
6
7/// Represents a version boundary (inclusive or exclusive).
8/// Used to determine the range of versions a constraint covers.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct Bound {
11	version: String,
12	inclusive: bool,
13}
14
15impl Bound {
16	/// Create a new bound.
17	pub fn new(version: impl Into<String>, inclusive: bool) -> Self {
18		Self {
19			version: version.into(),
20			inclusive,
21		}
22	}
23
24	/// The zero bound: 0.0.0.0-dev (inclusive).
25	/// Represents the absolute minimum version.
26	#[must_use]
27	pub fn zero() -> Self {
28		Self::new("0.0.0.0-dev", true)
29	}
30
31	/// Positive infinity bound (exclusive).
32	/// PHP uses `PHP_INT_MAX` which is 9223372036854775807 on 64-bit.
33	#[must_use]
34	pub fn positive_infinity() -> Self {
35		Self::new("9223372036854775807.0.0.0", false)
36	}
37
38	/// Get the version string.
39	#[must_use]
40	pub fn version(&self) -> &str {
41		&self.version
42	}
43
44	/// Check if this bound is inclusive.
45	#[must_use]
46	pub const fn is_inclusive(&self) -> bool {
47		self.inclusive
48	}
49
50	/// Check if this is the zero bound.
51	#[must_use]
52	pub fn is_zero(&self) -> bool {
53		self.version == "0.0.0.0-dev" && self.inclusive
54	}
55
56	/// Check if this is the positive infinity bound.
57	#[must_use]
58	pub fn is_positive_infinity(&self) -> bool {
59		self.version == "9223372036854775807.0.0.0" && !self.inclusive
60	}
61
62	/// Compare this bound to another.
63	/// Returns ordering based on version comparison and inclusivity.
64	#[must_use]
65	pub fn compare_to(&self, other: &Self) -> Ordering {
66		let cmp = version_compare(&self.version, &other.version);
67
68		if cmp != Ordering::Equal {
69			return cmp;
70		}
71
72		// If versions are equal, inclusivity matters
73		// For lower bounds: inclusive < exclusive (inclusive starts earlier)
74		// For upper bounds: exclusive < inclusive (exclusive ends earlier)
75		// This comparison assumes lower bound context
76		match (self.inclusive, other.inclusive) {
77			(true, false) => Ordering::Less,
78			(false, true) => Ordering::Greater,
79			_ => Ordering::Equal,
80		}
81	}
82
83	/// Compare for lower bound context.
84	/// In lower bounds, inclusive is "smaller" (starts earlier).
85	#[must_use]
86	pub fn compare_as_lower(&self, other: &Self) -> Ordering {
87		let cmp = version_compare(&self.version, &other.version);
88
89		if cmp != Ordering::Equal {
90			return cmp;
91		}
92
93		// For lower bounds: inclusive starts earlier than exclusive
94		match (self.inclusive, other.inclusive) {
95			(true, false) => Ordering::Less,
96			(false, true) => Ordering::Greater,
97			_ => Ordering::Equal,
98		}
99	}
100
101	/// Compare for upper bound context.
102	/// In upper bounds, exclusive is "smaller" (ends earlier).
103	#[must_use]
104	pub fn compare_as_upper(&self, other: &Self) -> Ordering {
105		let cmp = version_compare(&self.version, &other.version);
106
107		if cmp != Ordering::Equal {
108			return cmp;
109		}
110
111		// For upper bounds: exclusive ends earlier than inclusive
112		match (self.inclusive, other.inclusive) {
113			(false, true) => Ordering::Less,
114			(true, false) => Ordering::Greater,
115			_ => Ordering::Equal,
116		}
117	}
118}
119
120impl Display for Bound {
121	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122		let bracket = if self.inclusive {
123			"inclusive"
124		} else {
125			"exclusive"
126		};
127
128		write!(f, "{} ({})", self.version, bracket)
129	}
130}
131
132#[cfg(test)]
133mod tests {
134	use super::*;
135
136	#[test]
137	fn test_bound_creation() {
138		let bound = Bound::new("1.0.0", true);
139		assert_eq!(bound.version(), "1.0.0");
140		assert!(bound.is_inclusive());
141	}
142
143	#[test]
144	fn test_zero_bound() {
145		let zero = Bound::zero();
146		assert!(zero.is_zero());
147		assert!(!zero.is_positive_infinity());
148	}
149
150	#[test]
151	fn test_positive_infinity() {
152		let inf = Bound::positive_infinity();
153		assert!(inf.is_positive_infinity());
154		assert!(!inf.is_zero());
155	}
156
157	#[test]
158	fn test_bound_comparison() {
159		let a = Bound::new("1.0.0", true);
160		let b = Bound::new("2.0.0", true);
161		assert_eq!(a.compare_to(&b), Ordering::Less);
162
163		let c = Bound::new("1.0.0", false);
164		assert_eq!(a.compare_to(&c), Ordering::Less); // inclusive < exclusive for same version
165	}
166}