deb_version/
lib.rs

1use std::cmp::Ordering;
2
3fn epoch(version: &str) -> (&str, &str) {
4    if let Some(colon) = version.find(':') {
5        (&version[..colon], &version[colon + 1..])
6    } else {
7        ("", version)
8    }
9}
10
11fn debian_revision(version: &str) -> (&str, &str) {
12    if let Some(hyphen) = version.rfind('-') {
13        (&version[..hyphen], &version[hyphen + 1..])
14    } else {
15        (version, "")
16    }
17}
18
19fn split_point<F>(from: &str, pat: F) -> usize
20where
21    F: Fn(char) -> bool,
22{
23    for (pos, chr) in from.chars().enumerate() {
24        if !pat(chr) {
25            return pos;
26        }
27    }
28    from.len()
29}
30
31fn take_while<F>(from: &str, pat: F) -> (&str, &str)
32where
33    F: Fn(char) -> bool,
34{
35    let point = split_point(from, pat);
36    (&from[..point], &from[point..])
37}
38
39fn take_component(version: &str) -> ((&str, &str), &str) {
40    let (alpha_part, version) = take_while(version, |chr: char| !chr.is_digit(10));
41    let (num_part, version) = take_while(version, |chr: char| chr.is_digit(10));
42
43    ((alpha_part, num_part), version)
44}
45
46fn compare_digits(left: &str, right: &str) -> Ordering {
47    if left == right {
48        return Ordering::Equal;
49    }
50
51    let left = left.trim_start_matches('0');
52    let right = right.trim_start_matches('0');
53
54    match left.len().cmp(&right.len()) {
55        Ordering::Equal => left.cmp(&right),
56        other => other,
57    }
58}
59
60fn is_ascii_letter(chr: char) -> bool {
61    (chr >= 'a' && chr <= 'z') || (chr >= 'A' && chr <= 'Z')
62}
63
64fn compare_non_digit(left: char, right: char) -> Ordering {
65    if left == right {
66        return Ordering::Equal;
67    }
68
69    if '~' == left {
70        return Ordering::Less;
71    }
72
73    if '~' == right {
74        return Ordering::Greater;
75    }
76
77    let left_letter = is_ascii_letter(left);
78    let right_letter = is_ascii_letter(right);
79
80    if left_letter == right_letter {
81        left.cmp(&right)
82    } else if left_letter {
83        Ordering::Less
84    } else {
85        Ordering::Greater
86    }
87}
88
89fn compare_non_digits(left: &str, right: &str) -> Ordering {
90    if left == right {
91        return Ordering::Equal;
92    }
93
94    let mut left = left.chars();
95    let mut right = right.chars();
96
97    loop {
98        if let Some(l) = left.next() {
99            if let Some(r) = right.next() {
100                match compare_non_digit(l, r) {
101                    Ordering::Equal => continue,
102                    other => return other,
103                }
104            } else {
105                return if '~' == l {
106                    Ordering::Less
107                } else {
108                    Ordering::Greater
109                };
110            }
111        } else {
112            return if let Some(r) = right.next() {
113                if '~' == r {
114                    Ordering::Greater
115                } else {
116                    Ordering::Less
117                }
118            } else {
119                // implies strings are the same, which we test for
120                unreachable!();
121            };
122        }
123    }
124}
125
126fn compare_simple_version(mut left: &str, mut right: &str) -> Ordering {
127    if left == right {
128        return Ordering::Equal;
129    }
130
131    loop {
132        let ((left_alpha, left_digit), left_remainder) = take_component(left);
133        let ((right_alpha, right_digit), right_remainder) = take_component(right);
134
135        match compare_non_digits(left_alpha, right_alpha) {
136            Ordering::Equal => {}
137            other => return other,
138        }
139
140        match compare_digits(left_digit, right_digit) {
141            Ordering::Equal => {}
142            other => return other,
143        }
144
145        if left_remainder.is_empty() && right_remainder.is_empty() {
146            return Ordering::Equal;
147        }
148
149        left = left_remainder;
150        right = right_remainder;
151    }
152}
153
154/// Compare two version numbers.
155///
156/// # Example
157/// ```rust
158/// extern crate deb_version;
159/// assert_eq!(
160///     std::cmp::Ordering::Less,
161///     deb_version::compare_versions("1.0", "2.0")
162/// );
163/// ```
164pub fn compare_versions(left: &str, right: &str) -> Ordering {
165    if left == right {
166        return Ordering::Equal;
167    }
168
169    let (left_epoch, left) = epoch(left);
170    let (right_epoch, right) = epoch(right);
171
172    match compare_digits(left_epoch, right_epoch) {
173        Ordering::Equal => {}
174        other => return other,
175    }
176
177    let (left, left_debian) = debian_revision(left);
178    let (right, right_debian) = debian_revision(right);
179
180    match compare_simple_version(left, right) {
181        Ordering::Equal => {}
182        other => return other,
183    }
184
185    compare_simple_version(left_debian, right_debian)
186}
187
188#[cfg(test)]
189mod tests {
190    use std::cmp::Ordering;
191
192    use crate::compare_versions;
193
194    #[test]
195    fn test_epoch() {
196        use crate::epoch;
197
198        assert_eq!(("", "17"), epoch("17"));
199        assert_eq!(("1", "17"), epoch("1:17"));
200
201        // not valid, but kind of implied by the spec?
202        assert_eq!(("1", "17:19"), epoch("1:17:19"));
203    }
204
205    #[test]
206    fn test_extract_revision() {
207        use crate::debian_revision;
208
209        assert_eq!(("17", ""), debian_revision("17"));
210        assert_eq!(("17", "2"), debian_revision("17-2"));
211        assert_eq!(("17-2", "3"), debian_revision("17-2-3"));
212    }
213
214    #[test]
215    fn test_take_component() {
216        use crate::take_component;
217
218        assert_eq!((("abc", "123"), "def456"), take_component("abc123def456"));
219        assert_eq!((("a", "123"), "def456"), take_component("a123def456"));
220        assert_eq!((("abc", "1"), "def456"), take_component("abc1def456"));
221        assert_eq!((("a", "1"), "def456"), take_component("a1def456"));
222
223        assert_eq!((("", "12"), "bc34"), take_component("12bc34"));
224        assert_eq!((("", "12"), "b34"), take_component("12b34"));
225        assert_eq!((("", "1"), "bc34"), take_component("1bc34"));
226        assert_eq!((("", "1"), "b34"), take_component("1b34"));
227
228        assert_eq!((("", "17"), ""), take_component("17"));
229        assert_eq!((("", "1"), ""), take_component("1"));
230        assert_eq!((("ab", ""), ""), take_component("ab"));
231        assert_eq!((("a", ""), ""), take_component("a"));
232
233        assert_eq!((("", ""), ""), take_component(""));
234    }
235
236    #[test]
237    fn test_compare_digits() {
238        use crate::compare_digits;
239        use std::cmp::Ordering::*;
240
241        assert_eq!(Equal, compare_digits("1", "1"));
242        assert_eq!(Equal, compare_digits("1", "01"));
243        assert_eq!(Equal, compare_digits("100", "0100"));
244        assert_eq!(Equal, compare_digits("0100", "100"));
245        assert_eq!(Equal, compare_digits("00000100", "00000000000100"));
246        assert_eq!(Equal, compare_digits("000100", "0100"));
247
248        assert_eq!(Equal, compare_digits("", "00"));
249        assert_eq!(Less, compare_digits("", "001"));
250        assert_eq!(Greater, compare_digits("001", ""));
251
252        assert_eq!(Less, compare_digits("1", "2"));
253        assert_eq!(Greater, compare_digits("2", "1"));
254
255        assert_eq!(Less, compare_digits("01", "02"));
256        assert_eq!(Greater, compare_digits("02", "01"));
257
258        assert_eq!(Less, compare_digits("10", "20"));
259        assert_eq!(Greater, compare_digits("20", "10"));
260
261        assert_eq!(Less, compare_digits("11", "12"));
262        assert_eq!(Greater, compare_digits("12", "11"));
263
264        assert_eq!(Less, compare_digits("1", "10"));
265        assert_eq!(Greater, compare_digits("10", "1"));
266    }
267
268    #[test]
269    fn test_compare_non_digits() {
270        use crate::compare_non_digits;
271        use std::cmp::Ordering::*;
272
273        assert_eq!(Equal, compare_non_digits("a", "a"));
274        assert_eq!(Equal, compare_non_digits("Z", "Z"));
275        assert_eq!(Equal, compare_non_digits("~", "~"));
276        assert_eq!(Equal, compare_non_digits("-", "-"));
277
278        assert_eq!(Less, compare_non_digits("a", "b"));
279    }
280
281    #[test]
282    fn simple() {
283        assert_eq!(Ordering::Less, compare_versions("3.0", "3.1"));
284        assert_eq!(Ordering::Greater, compare_versions("3.1", "3.0"));
285        assert_eq!(Ordering::Equal, compare_versions("3.0", "3.0"));
286    }
287}