Skip to content
123 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor1//! Ordering versions well enough to pick an upgrade: semver, Cargo, Go's
2//! `v1.2.3`, Python's `1.2.post1` and the like.
3
4use std::cmp::Ordering;
5
6#[derive(Debug, PartialEq, Eq)]
7enum Part {
8 Number(u64),
9 Text(String),
10}
11
12/// The release and the pre-release of a version, each split into numbers
13/// and words. `+build` metadata is ignored, as semver says.
14fn parts(version: &str) -> (Vec<Part>, Vec<Part>) {
15 let version = version.trim().trim_start_matches(['v', 'V', '=']);
16 let version = version.split('+').next().unwrap_or(version);
17 let (release, pre) = match version.split_once('-') {
18 Some((release, pre)) => (release, Some(pre)),
19 None => (version, None),
20 };
21 let split = |text: &str| -> Vec<Part> {
22 let mut out = Vec::new();
23 let mut current = String::new();
24 let mut digits = false;
25 let flush = |current: &mut String, digits: bool, out: &mut Vec<Part>| {
26 if current.is_empty() {
27 return;
28 }
29 out.push(if digits {
30 Part::Number(current.parse().unwrap_or(u64::MAX))
31 } else {
32 Part::Text(current.to_lowercase())
33 });
34 current.clear();
35 };
36 for c in text.chars() {
37 if c.is_ascii_alphanumeric() {
38 let is_digit = c.is_ascii_digit();
39 if !current.is_empty() && is_digit != digits {
40 flush(&mut current, digits, &mut out);
41 }
42 digits = is_digit;
43 current.push(c);
44 } else {
45 flush(&mut current, digits, &mut out);
46 }
47 }
48 flush(&mut current, digits, &mut out);
49 out
50 };
51 let mut release = split(release);
52 // A Python pre-release is written into the release: `2.0.0rc1`.
53 let mut pre_parts = pre.map(split).unwrap_or_default();
54 if let Some(at) = release.iter().position(|part| matches!(part, Part::Text(text) if is_pre_word(text))) {
55 let mut tail = release.split_off(at);
56 tail.append(&mut pre_parts);
57 pre_parts = tail;
58 }
59 (release, pre_parts)
60}
61
62fn is_pre_word(text: &str) -> bool {
63 matches!(text, "a" | "b" | "c" | "rc" | "alpha" | "beta" | "pre" | "dev" | "preview")
64}
65
66fn compare_parts(a: &[Part], b: &[Part]) -> Ordering {
67 let length = a.len().max(b.len());
68 for index in 0..length {
69 let ordering = match (a.get(index), b.get(index)) {
70 (Some(Part::Number(x)), Some(Part::Number(y))) => x.cmp(y),
71 (Some(Part::Text(x)), Some(Part::Text(y))) => x.cmp(y),
72 (Some(Part::Number(_)), Some(Part::Text(_))) => Ordering::Greater,
73 (Some(Part::Text(_)), Some(Part::Number(_))) => Ordering::Less,
74 // 1.2 == 1.2.0
75 (Some(Part::Number(x)), None) => x.cmp(&0),
76 (None, Some(Part::Number(y))) => 0.cmp(y),
77 // 1.2.post1 > 1.2
78 (Some(Part::Text(_)), None) => Ordering::Greater,
79 (None, Some(Part::Text(_))) => Ordering::Less,
80 (None, None) => Ordering::Equal,
81 };
82 if ordering != Ordering::Equal {
83 return ordering;
84 }
85 }
86 Ordering::Equal
87}
88
89/// Orders two versions. A pre-release comes before its release.
90pub fn compare(a: &str, b: &str) -> Ordering {
91 let (a_release, a_pre) = parts(a);
92 let (b_release, b_pre) = parts(b);
93 compare_parts(&a_release, &b_release).then_with(|| match (a_pre.is_empty(), b_pre.is_empty()) {
94 (true, true) => Ordering::Equal,
95 (true, false) => Ordering::Greater,
96 (false, true) => Ordering::Less,
97 (false, false) => compare_parts(&a_pre, &b_pre),
98 })
99}
100
101#[cfg(test)]
102mod tests {
103 use super::compare;
104 use std::cmp::Ordering::*;
105
106 #[test]
107 fn versions_are_ordered() {
108 for (a, b, expected) in [
109 ("1.2.3", "1.2.10", Less),
110 ("v1.9.0", "v1.10.0", Less),
111 ("2.0.0", "2.0.0-rc.1", Greater),
112 ("1.0.0-alpha", "1.0.0-beta", Less),
113 ("1.2", "1.2.0", Equal),
114 ("4.17.21", "4.17.21", Equal),
115 ("2.0.0rc1", "2.0.0", Less),
116 ("1.2.post1", "1.2", Greater),
117 ("0.10.0", "0.9.9", Greater),
118 ("1.0.0+build.5", "1.0.0", Equal),
119 ] {
120 assert_eq!(compare(a, b), expected, "{a} vs {b}");
121 }
122 }
123}

This file's history is long; its oldest lines are credited to the oldest commit read.