1use std::cmp::Ordering;
7use std::fmt;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct Version {
11 pub major: u32,
12 pub minor: u32,
13 pub patch: u32,
14}
15
16impl Version {
17 pub const fn new(major: u32, minor: u32, patch: u32) -> Self {
18 Self {
19 major,
20 minor,
21 patch,
22 }
23 }
24
25 pub fn parse(value: &str) -> Result<Self, String> {
26 let mut parts = value.trim().split('.');
27
28 let mut next = |what: &str| -> Result<u32, String> {
29 parts
30 .next()
31 .ok_or_else(|| format!("version `{value}` has no {what}"))?
32 .parse()
33 .map_err(|_| format!("version `{value}` has a non-numeric {what}"))
34 };
35
36 let version = Self {
37 major: next("major")?,
38 minor: next("minor")?,
39 patch: next("patch")?,
40 };
41
42 if parts.next().is_some() {
43 return Err(format!("version `{value}` has more than three parts"));
44 }
45
46 Ok(version)
47 }
48}
49
50impl Ord for Version {
51 fn cmp(&self, other: &Self) -> Ordering {
52 (self.major, self.minor, self.patch).cmp(&(other.major, other.minor, other.patch))
53 }
54}
55
56impl PartialOrd for Version {
57 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
58 Some(self.cmp(other))
59 }
60}
61
62impl fmt::Display for Version {
63 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71
72 #[test]
73 fn versions_order_by_component() {
74 assert!(Version::parse("0.2.0").unwrap() > Version::parse("0.1.9").unwrap());
75 assert!(Version::parse("1.0.0").unwrap() > Version::parse("0.99.99").unwrap());
76 assert!(Version::parse("0.1.10").unwrap() > Version::parse("0.1.9").unwrap());
77 }
78
79 #[test]
80 fn a_version_that_cannot_be_compared_is_rejected_rather_than_guessed() {
81 assert!(Version::parse("0.1").is_err());
82 assert!(Version::parse("0.1.0-beta").is_err());
83 assert!(Version::parse("0.1.0.1").is_err());
84 }
85}