1#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
8#[cfg_attr(feature = "serde", derive(serde::Serialize))]
9pub struct Version {
10 pub major: u64,
12 pub minor: u64,
14 pub patch: u64,
16}
17
18impl std::fmt::Display for Version {
19 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
21 }
22}
23
24pub fn parse_dotted_version(raw: &str) -> Option<Version> {
29 for token in raw.split_whitespace() {
30 let mut parts = token.split('.');
31 let Some(major) = parts.next().and_then(leading_number) else {
32 continue;
33 };
34 let Some(minor) = parts.next().and_then(leading_number) else {
35 continue; };
37 let patch = parts.next().and_then(leading_number).unwrap_or(0);
38 return Some(Version {
39 major,
40 minor,
41 patch,
42 });
43 }
44 None
45}
46
47fn leading_number(s: &str) -> Option<u64> {
49 let end = s.bytes().take_while(u8::is_ascii_digit).count();
50 if end == 0 {
51 return None;
52 }
53 s[..end].parse().ok()
54}
55
56#[cfg(test)]
57mod tests {
58 use super::*;
59
60 #[test]
61 fn parses_real_world_shapes() {
62 let v = parse_dotted_version("git version 2.54.0.windows.1").unwrap();
65 assert_eq!((v.major, v.minor, v.patch), (2, 54, 0));
66 let v = parse_dotted_version("git version 2.41.0-rc1").unwrap();
67 assert_eq!((v.major, v.minor, v.patch), (2, 41, 0));
68 let v = parse_dotted_version("git version 2.54").unwrap();
69 assert_eq!(v.patch, 0, "missing patch defaults to 0");
70 let v = parse_dotted_version("jj 0.42.0").unwrap();
72 assert_eq!((v.major, v.minor, v.patch), (0, 42, 0));
73 assert!(parse_dotted_version("no digits here").is_none());
74 assert!(parse_dotted_version("git version unknowable").is_none());
75 }
76
77 #[test]
78 fn orders_numerically() {
79 let lo = parse_dotted_version("jj 0.38.0").unwrap();
80 let hi = parse_dotted_version("jj 0.40.0").unwrap();
81 assert!(hi > lo);
82 assert!(
83 Version {
84 major: 2,
85 minor: 9,
86 patch: 0
87 } < Version {
88 major: 2,
89 minor: 10,
90 patch: 0
91 }
92 );
93 }
94
95 #[test]
96 fn displays_dotted() {
97 let v = parse_dotted_version("git version 2.54.1").unwrap();
98 assert_eq!(v.to_string(), "2.54.1");
99 }
100}
101
102#[cfg(test)]
107mod proptests {
108 use super::*;
109 use proptest::prelude::*;
110
111 proptest! {
112 #[test]
113 fn never_panics_on_arbitrary_text(s in any::<String>()) {
114 let _ = parse_dotted_version(&s);
115 }
116
117 #[test]
119 fn never_panics_on_versionish_text(s in r"[a-z]{0,6} ?[0-9.\-+a-z]{0,20}") {
120 let _ = parse_dotted_version(&s);
121 }
122 }
123}