1use std::cmp::Ordering;
3
4#[derive(Debug, PartialEq, Eq, std::hash::Hash, Clone)]
5pub struct Version {
7 pub components: Vec<u32>,
9 pub pre_release: Option<String>, }
12
13impl std::fmt::Display for Version {
14 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
15 f.write_str(
17 &self
18 .components
19 .iter()
20 .map(|c| c.to_string())
21 .collect::<Vec<_>>()
22 .join("."),
23 )?;
24 if let Some(pre_release) = &self.pre_release {
25 f.write_str("-")?;
26 f.write_str(pre_release)?;
27 }
28 Ok(())
29 }
30}
31
32impl Version {
33 pub fn new(major: u32, minor: u32, patch: Option<u32>, pre_release: Option<&str>) -> Self {
35 Self {
36 components: if let Some(patch) = patch {
37 vec![major, minor, patch]
38 } else {
39 vec![major, minor]
40 },
41 pre_release: pre_release.map(|s| s.to_string()),
42 }
43 }
44}
45
46impl std::str::FromStr for Version {
47 type Err = String;
48
49 fn from_str(s: &str) -> Result<Self, Self::Err> {
50 let mut parts = s.splitn(2, '-');
52 let version = parts
53 .next()
54 .ok_or(format!("Invalid version string: {s}"))?;
55 let pre_release = parts.next().map(|s| s.to_string());
56
57 let components = version
58 .split('.')
59 .map(|part| {
60 part.parse()
61 .map_err(|_| format!("Invalid version component: {s}"))
62 })
63 .collect::<Result<Vec<_>, _>>()?;
64
65 Ok(Self {
66 components,
67 pre_release,
68 })
69 }
70}
71
72impl Ord for Version {
73 fn cmp(&self, other: &Self) -> Ordering {
74 for (a, b) in self.components.iter().zip(other.components.iter()) {
76 match a.cmp(b) {
77 Ordering::Equal => continue,
78 ordering => return ordering,
79 }
80 }
81 if self.components.len() < other.components.len() {
82 Ordering::Less
83 } else if self.components.len() > other.components.len() {
84 Ordering::Greater
85 } else {
86 self.compare_pre_release(other)
87 }
88 }
89}
90
91impl PartialOrd for Version {
92 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
93 Some(self.cmp(other))
94 }
95}
96
97impl Version {
98 fn compare_pre_release(&self, other: &Self) -> Ordering {
99 match (&self.pre_release, &other.pre_release) {
100 (None, None) => Ordering::Equal,
101 (None, Some(_)) => Ordering::Greater,
102 (Some(_), None) => Ordering::Less,
103 (Some(a), Some(b)) => a.cmp(b),
104 }
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::Version;
111 use std::str::FromStr;
112
113 #[test]
114 fn test_version_from_str() {
115 use std::str::FromStr;
116
117 let version = Version::from_str("1.2.3").unwrap();
118 assert_eq!(version, Version::new(1, 2, Some(3), None));
119
120 let version = Version::from_str("1.2.3-alpha").unwrap();
121 assert_eq!(version, Version::new(1, 2, Some(3), Some("alpha")));
122
123 let version = Version::from_str("1.2.3-beta").unwrap();
124 assert_eq!(version, Version::new(1, 2, Some(3), Some("beta")));
125 }
126
127 #[test]
128 fn test_version_cmp() {
129 use std::cmp::Ordering;
130
131 let v1 = Version::from_str("1.2.3").unwrap();
132 let v2 = Version::from_str("1.2.3").unwrap();
133 assert_eq!(v1.cmp(&v2), Ordering::Equal);
134
135 let v1 = Version::from_str("1.2.3").unwrap();
136 let v2 = Version::from_str("1.2.4").unwrap();
137 assert_eq!(v1.cmp(&v2), Ordering::Less);
138
139 let v1 = Version::from_str("1.2.3").unwrap();
140 let v2 = Version::from_str("1.2.3-alpha").unwrap();
141 assert_eq!(v1.cmp(&v2), Ordering::Greater);
142
143 let v1 = Version::from_str("1.2.3-alpha").unwrap();
144 let v2 = Version::from_str("1.2.3-beta").unwrap();
145 assert_eq!(v1.cmp(&v2), Ordering::Less);
146 }
147
148 #[test]
149 fn test_version_invalid() {
150 assert!(Version::from_str("a").is_err());
151 assert!(Version::from_str("a1-b").is_err());
152 }
153}