1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
use chrono::{Datelike, Utc};
use os_release::OsRelease;
use std::{
fmt::{self, Display, Formatter},
io,
str::FromStr,
};
#[derive(Debug, Error)]
pub enum VersionError {
#[error("failed to fetch /etc/os-release: {}", _0)]
OsRelease(io::Error),
#[error("version parsing error: {}", _0)]
Parse(VersionParseError),
}
#[derive(Debug, Error)]
pub enum VersionParseError {
#[error("release version component was not a number: found {}", _0)]
VersionNaN(String),
#[error("invalid minor release version: expected 4 or 10, found {}", _0)]
InvalidMinorVersion(u8),
#[error("major version does not exist")]
NoMajor,
#[error("minor version does not exist")]
NoMinor,
#[error("release version is empty")]
NoVersion,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct Version {
pub major: u8,
pub minor: u8,
pub patch: u8,
}
impl Version {
pub fn detect() -> Result<Self, VersionError> {
let release = OsRelease::new().map_err(VersionError::OsRelease)?;
release.version.parse::<Version>().map_err(VersionError::Parse)
}
pub fn is_lts(self) -> bool { self.major % 2 == 0 && self.minor == 4 }
pub fn months_since(self) -> i32 {
let today = Utc::today();
let major = 2000 - today.year() as u32;
let minor = today.month() as u32;
months_since(self, major, minor)
}
pub fn next_release(self) -> Self {
let (major, minor) = if self.minor == 10 { (self.major + 1, 4) } else { (self.major, 10) };
Version { major, minor, patch: 0 }
}
}
impl Display for Version {
fn fmt(&self, fmt: &mut Formatter) -> fmt::Result {
write!(fmt, "{}.{:02}", self.major, self.minor)?;
if self.patch != 0 {
write!(fmt, "{}", self.patch)?;
}
Ok(())
}
}
impl FromStr for Version {
type Err = VersionParseError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let version = input.split_whitespace().next().ok_or(VersionParseError::NoVersion)?;
if version.is_empty() {
return Err(VersionParseError::NoVersion);
}
let mut iter = version.split('.');
let major = iter.next().ok_or(VersionParseError::NoMajor)?;
let major =
major.parse::<u8>().map_err(|_| VersionParseError::VersionNaN(major.to_owned()))?;
let minor = iter.next().ok_or(VersionParseError::NoMinor)?;
let minor =
minor.parse::<u8>().map_err(|_| VersionParseError::VersionNaN(minor.to_owned()))?;
let patch = iter.next().and_then(|p| p.parse::<u8>().ok()).unwrap_or(0);
Ok(Version { major, minor, patch })
}
}
fn months_since(version: Version, major: u32, minor: u32) -> i32 {
((major as i32 - version.major as i32) * 12) + minor as i32 - version.minor as i32
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn months_since_release() {
assert_eq!(18, months_since(Version { major: 18, minor: 4, patch: 0 }, 19, 10));
assert_eq!(3, months_since(Version { major: 19, minor: 10, patch: 0 }, 20, 1));
assert_eq!(-3, months_since(Version { major: 18, minor: 4, patch: 0 }, 18, 1))
}
#[test]
pub fn lts_check() {
assert!(Version { major: 18, minor: 4, patch: 0 }.is_lts());
assert!(!Version { major: 18, minor: 10, patch: 0 }.is_lts());
assert!(!Version { major: 19, minor: 4, patch: 0 }.is_lts());
assert!(!Version { major: 19, minor: 10, patch: 0 }.is_lts());
assert!(Version { major: 20, minor: 4, patch: 0 }.is_lts());
}
#[test]
pub fn lts_parse() {
assert_eq!(
Version { major: 18, minor: 4, patch: 1 },
"18.04.1 LTS".parse::<Version>().unwrap()
)
}
#[test]
pub fn lts_next() {
assert_eq!(
Version { major: 18, minor: 10, patch: 0 },
Version { major: 18, minor: 4, patch: 1 }.next_release()
)
}
#[test]
pub fn non_lts_parse() {
assert_eq!(Version { major: 18, minor: 10, patch: 0 }, "18.10".parse::<Version>().unwrap())
}
#[test]
pub fn non_lts_next() {
assert_eq!(
Version { major: 19, minor: 4, patch: 0 },
Version { major: 18, minor: 10, patch: 0 }.next_release()
)
}
}