semver_php/
intervals.rs

1//! Interval algebra for constraints.
2//!
3//! Provides utilities for:
4//! - Converting constraints to intervals
5//! - Checking if one constraint is a subset of another
6//! - Checking if two constraints have any intersection
7//! - Compacting constraints
8
9use crate::{
10	constraint::{Constraint, MultiConstraint, Operator, SingleConstraint},
11	interval::{BranchConstraint, Interval, IntervalResult},
12	version::version_compare,
13};
14use std::{cmp::Ordering, collections::HashMap};
15
16/// Intervals helper for constraint algebra.
17pub struct Intervals {
18	cache: HashMap<String, IntervalResult>,
19}
20
21impl Default for Intervals {
22	fn default() -> Self {
23		Self::new()
24	}
25}
26
27impl Intervals {
28	/// Create a new Intervals helper.
29	#[must_use]
30	pub fn new() -> Self {
31		Self {
32			cache: HashMap::new(),
33		}
34	}
35
36	/// Clear the memoization cache.
37	pub fn clear(&mut self) {
38		self.cache.clear();
39	}
40
41	/// Check if `candidate` is a subset of `constraint`.
42	pub fn is_subset_of(
43		&mut self,
44		candidate: &dyn Constraint,
45		constraint: &dyn Constraint,
46	) -> bool {
47		// MatchAll contains everything
48		if constraint.is_match_all() {
49			return true;
50		}
51
52		// MatchNone contains nothing, and nothing is a subset of MatchNone except MatchNone itself
53		if candidate.is_match_none() || constraint.is_match_none() {
54			return false;
55		}
56
57		// Create intersection: candidate AND constraint
58		let intersection = self.get_intersection(candidate, constraint);
59		let candidate_intervals = self.get(candidate);
60
61		// Check numeric intervals match
62		if intersection.numeric.len() != candidate_intervals.numeric.len() {
63			return false;
64		}
65
66		for (i, interval) in intersection.numeric.iter().enumerate() {
67			if i >= candidate_intervals.numeric.len() {
68				return false;
69			}
70
71			let cand_interval = &candidate_intervals.numeric[i];
72
73			// Compare start bounds
74			if interval.start().to_string() != cand_interval.start().to_string() {
75				return false;
76			}
77
78			// Compare end bounds
79			if interval.end().to_string() != cand_interval.end().to_string() {
80				return false;
81			}
82		}
83
84		// Check branch constraints match
85		if intersection.branches.exclude != candidate_intervals.branches.exclude {
86			return false;
87		}
88
89		if intersection.branches.names.len() != candidate_intervals.branches.names.len() {
90			return false;
91		}
92
93		for (i, name) in intersection.branches.names.iter().enumerate() {
94			if i >= candidate_intervals.branches.names.len()
95				|| name != &candidate_intervals.branches.names[i]
96			{
97				return false;
98			}
99		}
100
101		true
102	}
103
104	/// Check if two constraints have any intersection.
105	pub fn have_intersections(&mut self, a: &dyn Constraint, b: &dyn Constraint) -> bool {
106		if a.is_match_all() || b.is_match_all() {
107			return true;
108		}
109
110		if a.is_match_none() || b.is_match_none() {
111			return false;
112		}
113
114		let intersection = self.get_intersection_early_exit(a, b);
115		intersection.matches_something()
116	}
117
118	/// Get intervals for a constraint (with caching).
119	pub fn get(&mut self, constraint: &dyn Constraint) -> IntervalResult {
120		let key = constraint.to_string();
121
122		if let Some(cached) = self.cache.get(&key) {
123			return cached.clone();
124		}
125
126		let result = self.generate_intervals(constraint, false);
127		self.cache.insert(key, result.clone());
128		result
129	}
130
131	/// Get intersection of two constraints.
132	fn get_intersection(&mut self, a: &dyn Constraint, b: &dyn Constraint) -> IntervalResult {
133		// Create a conjunctive constraint and generate intervals
134		let a_intervals = self.get(a);
135		let b_intervals = self.get(b);
136		self.intersect_intervals(&a_intervals, &b_intervals)
137	}
138
139	/// Get intersection with early exit on first valid interval.
140	fn get_intersection_early_exit(
141		&mut self,
142		a: &dyn Constraint,
143		b: &dyn Constraint,
144	) -> IntervalResult {
145		let a_intervals = self.get(a);
146		let b_intervals = self.get(b);
147		self.intersect_intervals_early_exit(&a_intervals, &b_intervals)
148	}
149
150	/// Generate intervals from a constraint.
151	fn generate_intervals(
152		&mut self,
153		constraint: &dyn Constraint,
154		stop_on_first: bool,
155	) -> IntervalResult {
156		if constraint.is_match_all() {
157			return IntervalResult::any();
158		}
159
160		if constraint.is_match_none() {
161			return IntervalResult::none();
162		}
163
164		if let Some(single) = constraint.as_single() {
165			return self.generate_single_intervals(single);
166		}
167
168		if let Some(multi) = constraint.as_multi() {
169			return self.generate_multi_intervals(multi, stop_on_first);
170		}
171
172		IntervalResult::none()
173	}
174
175	/// Generate intervals from a single constraint.
176	#[allow(clippy::unused_self)]
177	fn generate_single_intervals(&self, constraint: &SingleConstraint) -> IntervalResult {
178		let op = constraint.operator();
179		let version = constraint.version();
180
181		// Handle dev branches
182		if version.starts_with("dev-") {
183			let mut intervals = Vec::new();
184			let mut branches = BranchConstraint::no_dev();
185
186			match op {
187				Operator::Ne => {
188					// != dev-foo means any numeric version may match
189					intervals.push(Interval::any());
190					branches = BranchConstraint::new(vec![version.to_string()], true);
191				},
192				Operator::Eq => {
193					branches.names.push(version.to_string());
194				},
195				_ => {
196					// Other operators on dev branches don't make sense for numeric ranges
197				},
198			}
199
200			return IntervalResult::new(intervals, branches);
201		}
202
203		// Numeric version handling
204		match op {
205			Operator::Gt | Operator::Ge => IntervalResult::new(
206				vec![Interval::new(
207					SingleConstraint::new(op, version),
208					Interval::until_positive_infinity(),
209				)],
210				BranchConstraint::no_dev(),
211			),
212			Operator::Lt | Operator::Le => IntervalResult::new(
213				vec![Interval::new(
214					Interval::from_zero(),
215					SingleConstraint::new(op, version),
216				)],
217				BranchConstraint::no_dev(),
218			),
219			Operator::Ne => {
220				// != x becomes: [0, <x) and (>x, +inf)
221				IntervalResult::new(
222					vec![
223						Interval::new(
224							Interval::from_zero(),
225							SingleConstraint::new(Operator::Lt, version),
226						),
227						Interval::new(
228							SingleConstraint::new(Operator::Gt, version),
229							Interval::until_positive_infinity(),
230						),
231					],
232					BranchConstraint::any_dev(),
233				)
234			},
235			Operator::Eq => {
236				// == x becomes: [>=x, <=x]
237				IntervalResult::new(
238					vec![Interval::new(
239						SingleConstraint::new(Operator::Ge, version),
240						SingleConstraint::new(Operator::Le, version),
241					)],
242					BranchConstraint::no_dev(),
243				)
244			},
245		}
246	}
247
248	/// Generate intervals from a multi constraint.
249	fn generate_multi_intervals(
250		&mut self,
251		multi: &MultiConstraint,
252		stop_on_first: bool,
253	) -> IntervalResult {
254		let constraints = multi.constraints();
255
256		let mut numeric_groups: Vec<Vec<Interval>> = Vec::new();
257		let mut constraint_branches: Vec<BranchConstraint> = Vec::new();
258
259		for c in constraints {
260			let res = self.get(c.as_ref());
261			numeric_groups.push(res.numeric);
262			constraint_branches.push(res.branches);
263		}
264
265		// Compute branch result
266		let branches = if multi.is_disjunctive() {
267			self.merge_branches_disjunctive(&constraint_branches)
268		} else {
269			self.merge_branches_conjunctive(&constraint_branches)
270		};
271
272		// If only one group, return it
273		if numeric_groups.len() == 1 {
274			return IntervalResult::new(numeric_groups.into_iter().next().unwrap(), branches);
275		}
276
277		// Merge numeric intervals
278		let numeric = if multi.is_disjunctive() {
279			self.merge_intervals_disjunctive(&numeric_groups)
280		} else {
281			self.merge_intervals_conjunctive(&numeric_groups, stop_on_first)
282		};
283
284		IntervalResult::new(numeric, branches)
285	}
286
287	/// Merge branches for disjunctive constraints.
288	#[allow(clippy::unused_self)]
289	fn merge_branches_disjunctive(&self, branches: &[BranchConstraint]) -> BranchConstraint {
290		let mut result = BranchConstraint::no_dev();
291
292		for b in branches {
293			if b.exclude {
294				if result.exclude {
295					// Disjunctive: exclude only what's excluded in all
296					result.names.retain(|n| b.names.contains(n));
297				} else {
298					// Switch to exclude mode
299					result.exclude = true;
300					result.names = b
301						.names
302						.iter()
303						.filter(|n| !result.names.contains(*n))
304						.cloned()
305						.collect();
306				}
307			} else if result.exclude {
308				// Keep excluding, but remove any explicitly included
309				result.names.retain(|n| !b.names.contains(n));
310			} else {
311				// Just add all branch names
312				result.names.extend(b.names.iter().cloned());
313			}
314		}
315
316		result.names.sort();
317		result.names.dedup();
318		result
319	}
320
321	/// Merge branches for conjunctive constraints.
322	#[allow(clippy::unused_self)]
323	fn merge_branches_conjunctive(&self, branches: &[BranchConstraint]) -> BranchConstraint {
324		let mut result = BranchConstraint::any_dev();
325
326		for b in branches {
327			if b.exclude {
328				if result.exclude {
329					// Conjunctive: add all exclusions
330					result.names.extend(b.names.iter().cloned());
331				} else {
332					// Only keep included names that aren't excluded
333					result.names.retain(|n| !b.names.contains(n));
334				}
335			} else if result.exclude {
336				// Switch to include mode with filtered names
337				let new_names: Vec<String> = b
338					.names
339					.iter()
340					.filter(|n| !result.names.contains(*n))
341					.cloned()
342					.collect();
343				result.exclude = false;
344				result.names = new_names;
345			} else {
346				// Conjunctive: only keep names in both
347				result.names.retain(|n| b.names.contains(n));
348			}
349		}
350
351		result.names.sort();
352		result.names.dedup();
353		result
354	}
355
356	/// Merge intervals for disjunctive constraints (union).
357	#[allow(clippy::unused_self)]
358	fn merge_intervals_disjunctive(&self, groups: &[Vec<Interval>]) -> Vec<Interval> {
359		// Collect all interval borders
360		let mut borders: Vec<Border> = Vec::new();
361
362		for group in groups {
363			for interval in group {
364				borders.push(Border {
365					version: interval.start().version().to_string(),
366					operator: interval.start().operator(),
367					is_start: true,
368				});
369				borders.push(Border {
370					version: interval.end().version().to_string(),
371					operator: interval.end().operator(),
372					is_start: false,
373				});
374			}
375		}
376
377		// Sort borders
378		borders.sort_by(|a, b| {
379			let cmp = version_compare(&a.version, &b.version);
380			if cmp == Ordering::Equal {
381				a.sort_order().cmp(&b.sort_order())
382			} else {
383				cmp
384			}
385		});
386
387		// Sweep line to find union intervals
388		let mut active_intervals = 0;
389		let mut intervals = Vec::new();
390		let mut start: Option<SingleConstraint> = None;
391
392		for border in borders {
393			if border.is_start {
394				if active_intervals == 0 {
395					start = Some(SingleConstraint::new(border.operator, &border.version));
396				}
397				active_intervals += 1;
398			} else {
399				active_intervals -= 1;
400				if active_intervals == 0 {
401					if let Some(s) = start.take() {
402						// Filter invalid intervals
403						if !is_invalid_interval(&s, &border) {
404							intervals.push(Interval::new(
405								s,
406								SingleConstraint::new(border.operator, &border.version),
407							));
408						}
409					}
410				}
411			}
412		}
413
414		intervals
415	}
416
417	/// Merge intervals for conjunctive constraints (intersection).
418	#[allow(clippy::unused_self)]
419	fn merge_intervals_conjunctive(
420		&self,
421		groups: &[Vec<Interval>],
422		stop_on_first: bool,
423	) -> Vec<Interval> {
424		// Collect all interval borders
425		let mut borders: Vec<Border> = Vec::new();
426
427		for group in groups {
428			for interval in group {
429				borders.push(Border {
430					version: interval.start().version().to_string(),
431					operator: interval.start().operator(),
432					is_start: true,
433				});
434				borders.push(Border {
435					version: interval.end().version().to_string(),
436					operator: interval.end().operator(),
437					is_start: false,
438				});
439			}
440		}
441
442		// Sort borders
443		borders.sort_by(|a, b| {
444			let cmp = version_compare(&a.version, &b.version);
445			if cmp == Ordering::Equal {
446				a.sort_order().cmp(&b.sort_order())
447			} else {
448				cmp
449			}
450		});
451
452		// Sweep line to find intersection intervals
453		let mut active_intervals = 0;
454		let activation_threshold = groups.len();
455		let mut intervals = Vec::new();
456		let mut start: Option<SingleConstraint> = None;
457
458		for border in borders {
459			if border.is_start {
460				active_intervals += 1;
461			} else {
462				active_intervals -= 1;
463			}
464
465			if start.is_none() && active_intervals >= activation_threshold {
466				start = Some(SingleConstraint::new(border.operator, &border.version));
467			} else if start.is_some() && active_intervals < activation_threshold {
468				if let Some(s) = start.take() {
469					// Filter invalid intervals
470					if !is_invalid_interval(&s, &border) {
471						intervals.push(Interval::new(
472							s,
473							SingleConstraint::new(border.operator, &border.version),
474						));
475
476						if stop_on_first {
477							break;
478						}
479					}
480				}
481			}
482		}
483
484		intervals
485	}
486
487	/// Intersect two interval results.
488	fn intersect_intervals(&self, a: &IntervalResult, b: &IntervalResult) -> IntervalResult {
489		let groups = vec![a.numeric.clone(), b.numeric.clone()];
490		let numeric = self.merge_intervals_conjunctive(&groups, false);
491		let branches = self.merge_branches_conjunctive(&[a.branches.clone(), b.branches.clone()]);
492		IntervalResult::new(numeric, branches)
493	}
494
495	/// Intersect with early exit.
496	fn intersect_intervals_early_exit(
497		&self,
498		a: &IntervalResult,
499		b: &IntervalResult,
500	) -> IntervalResult {
501		let groups = vec![a.numeric.clone(), b.numeric.clone()];
502		let numeric = self.merge_intervals_conjunctive(&groups, true);
503		let branches = self.merge_branches_conjunctive(&[a.branches.clone(), b.branches.clone()]);
504		IntervalResult::new(numeric, branches)
505	}
506}
507
508/// Border for interval sweep line algorithm.
509#[derive(Debug)]
510struct Border {
511	version: String,
512	operator: Operator,
513	is_start: bool,
514}
515
516impl Border {
517	const fn sort_order(&self) -> i32 {
518		match self.operator {
519			Operator::Ge => -3,
520			Operator::Lt => -2,
521			Operator::Gt => 2,
522			Operator::Le => 3,
523			Operator::Eq | Operator::Ne => 0,
524		}
525	}
526}
527
528/// Check if interval is invalid (e.g., > x to <= x with same x).
529fn is_invalid_interval(start: &SingleConstraint, border: &Border) -> bool {
530	if version_compare(start.version(), &border.version) != Ordering::Equal {
531		return false;
532	}
533
534	// Invalid if: > x to <= x, or >= x to < x
535	(start.operator() == Operator::Gt && border.operator == Operator::Le)
536		|| (start.operator() == Operator::Ge && border.operator == Operator::Lt)
537}
538
539#[cfg(test)]
540mod tests {
541	use super::*;
542
543	#[test]
544	fn test_single_constraint_intervals() {
545		let mut intervals = Intervals::new();
546
547		let c = SingleConstraint::new(Operator::Ge, "1.0.0.0-dev");
548		let result = intervals.get(&c);
549		assert_eq!(result.numeric.len(), 1);
550		assert_eq!(result.numeric[0].start().operator(), Operator::Ge);
551	}
552
553	#[test]
554	fn test_have_intersections() {
555		let mut intervals = Intervals::new();
556
557		let c1 = SingleConstraint::new(Operator::Ge, "1.0.0.0");
558		let c2 = SingleConstraint::new(Operator::Lt, "2.0.0.0");
559		assert!(intervals.have_intersections(&c1, &c2));
560
561		let c3 = SingleConstraint::new(Operator::Gt, "3.0.0.0");
562		let c4 = SingleConstraint::new(Operator::Lt, "2.0.0.0");
563		assert!(!intervals.have_intersections(&c3, &c4));
564	}
565}