1use std::cmp::Ordering;
6
7#[derive(Debug, Clone, Copy, PartialEq)]
8pub struct Version {
9 pub major: u32,
10 pub minor: u32,
11 pub patch: u32,
12}
13
14#[derive(Debug, Clone, PartialEq)]
15pub enum VersionError {
16 InvalidVersion,
17 VersionTooOld(Version),
18}
19
20impl Version {
21 pub fn parse(version: &str) -> Result<Version, VersionError> {
22 let parts: Vec<&str> = version.split(&['.', '-']).collect();
23 if parts.is_empty() {
24 return Err(VersionError::InvalidVersion);
25 }
26
27 Ok(Version {
28 major: parts[0].parse().or(Err(VersionError::InvalidVersion))?,
29 minor: if parts.len() > 1 {
30 parts[1].parse().or(Err(VersionError::InvalidVersion))?
31 } else {
32 0
33 },
34 patch: if parts.len() > 2 {
35 parts[2].parse().or(Err(VersionError::InvalidVersion))?
36 } else {
37 0
38 },
39 })
40 }
41}
42
43impl TryInto<Version> for &str {
44 type Error = VersionError;
45
46 fn try_into(self) -> Result<Version, Self::Error> {
47 Version::parse(self)
48 }
49}
50
51impl TryInto<Version> for String {
52 type Error = VersionError;
53
54 fn try_into(self) -> Result<Version, Self::Error> {
55 Version::parse(&self)
56 }
57}
58
59impl From<Version> for String {
60 fn from(value: Version) -> Self {
61 format!("{}.{}.{}", value.major, value.minor, value.patch)
62 }
63}
64
65impl std::fmt::Display for Version {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
68 }
69}
70
71impl PartialOrd for Version {
72 fn ge(&self, other: &Self) -> bool {
73 self.major >= other.major && self.minor >= other.minor && self.patch >= other.patch
74 }
75
76 fn gt(&self, other: &Self) -> bool {
77 (self.major > other.major)
78 || (self.major == other.major && self.minor > other.minor)
79 || (self.major == other.major && self.minor == other.minor && self.patch > other.patch)
80 }
81
82 fn le(&self, other: &Self) -> bool {
83 self.major <= other.major && self.minor <= other.minor && self.patch <= other.patch
84 }
85
86 fn lt(&self, other: &Self) -> bool {
87 (self.major < other.major)
88 || (self.major == other.major && self.minor < other.minor)
89 || (self.major == other.major && self.minor == other.minor && self.patch < other.patch)
90 }
91
92 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
93 if self == other {
94 Some(Ordering::Equal)
95 } else if self < other {
96 Some(Ordering::Less)
97 } else {
98 Some(Ordering::Greater)
99 }
100 }
101}
102
103#[cfg(test)]
104mod testing {
105 use super::*;
106
107 #[test]
108 fn test_version_parse_valid() {
109 assert_eq!(
110 Version::parse("1.2.3").unwrap(),
111 Version {
112 major: 1,
113 minor: 2,
114 patch: 3
115 }
116 );
117 assert_eq!(
118 Version::parse("1.2").unwrap(),
119 Version {
120 major: 1,
121 minor: 2,
122 patch: 0
123 }
124 );
125 assert_eq!(
126 Version::parse("1").unwrap(),
127 Version {
128 major: 1,
129 minor: 0,
130 patch: 0
131 }
132 );
133 assert_eq!(
134 Version::parse("4.0.20250530-gab5866c").unwrap(),
135 Version {
136 major: 4,
137 minor: 0,
138 patch: 20250530
139 }
140 )
141 }
142
143 #[test]
144 fn test_version_parse_invalid() {
145 assert!(Version::parse("").is_err());
146 assert!(Version::parse("1.2.3:4").is_err());
147 assert!(Version::parse("a.b.c").is_err());
148 }
149
150 #[test]
151 fn test_version_into_string() {
152 let version = Version {
153 major: 1,
154 minor: 2,
155 patch: 3,
156 };
157 let version_str: String = version.into();
158 assert_eq!(version_str, "1.2.3");
159 }
160
161 #[test]
162 fn test_version_partial_ord() {
163 let v1 = Version {
164 major: 1,
165 minor: 0,
166 patch: 0,
167 };
168 let v2 = Version {
169 major: 1,
170 minor: 1,
171 patch: 0,
172 };
173 let v3 = Version {
174 major: 1,
175 minor: 1,
176 patch: 1,
177 };
178
179 assert!(v1 < v2);
180 assert!(v2 < v3);
181 assert!(v1 < v3);
182 assert!(v3 > v2);
183 assert!(v2 > v1);
184 assert!(v3 > v1);
185 }
186
187 #[test]
188 fn test_version_partial_eq() {
189 let v1 = Version {
190 major: 1,
191 minor: 0,
192 patch: 0,
193 };
194 let v2 = Version {
195 major: 1,
196 minor: 0,
197 patch: 0,
198 };
199 let v3 = Version {
200 major: 1,
201 minor: 1,
202 patch: 0,
203 };
204
205 assert_eq!(v1, v2);
206 assert_ne!(v1, v3);
207 }
208
209 #[test]
210 fn test_version_try_into() {
211 let version_str = "1.2.3";
212 let version: Version = version_str.try_into().unwrap();
213 assert_eq!(
214 version,
215 Version {
216 major: 1,
217 minor: 2,
218 patch: 3
219 }
220 );
221
222 let version_string = String::from("1.2.3");
223 let version: Version = version_string.try_into().unwrap();
224 assert_eq!(
225 version,
226 Version {
227 major: 1,
228 minor: 2,
229 patch: 3
230 }
231 );
232 }
233
234 #[test]
235 fn test_display() {
236 let version = Version {
237 major: 1,
238 minor: 2,
239 patch: 3,
240 };
241 assert_eq!(format!("{}", version), "1.2.3");
242 }
243}