Skip to main content

r_description/
version.rs

1//! R Version strings
2use std::cmp::Ordering;
3
4#[derive(Debug, PartialEq, Eq, std::hash::Hash, Clone)]
5/// Represents an R version string like "1.2.3" or "2.5-1".
6///
7/// R version strings consist of non-negative integers separated by `.` or `-`.
8/// Both separators are equivalent: `2.5-1` and `2.5.1` represent the same version.
9/// There is no concept of pre-release versions in R's versioning scheme.
10pub struct Version {
11    /// Version components like [1, 2, 3]
12    pub components: Vec<u32>,
13}
14
15impl std::fmt::Display for Version {
16    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17        f.write_str(
18            &self
19                .components
20                .iter()
21                .map(|c| c.to_string())
22                .collect::<Vec<_>>()
23                .join("."),
24        )
25    }
26}
27
28impl Version {
29    /// Create a new version
30    pub fn new(major: u32, minor: u32, patch: Option<u32>) -> Self {
31        Self {
32            components: if let Some(patch) = patch {
33                vec![major, minor, patch]
34            } else {
35                vec![major, minor]
36            },
37        }
38    }
39}
40
41impl std::str::FromStr for Version {
42    type Err = String;
43
44    fn from_str(s: &str) -> Result<Self, Self::Err> {
45        // Both '.' and '-' are valid separators in R version strings and are equivalent.
46        // e.g. "2.5-1" == "2.5.1"
47        let components = s
48            .split(|c| c == '.' || c == '-')
49            .map(|part| {
50                part.parse()
51                    .map_err(|_| format!("Invalid version component: {s}"))
52            })
53            .collect::<Result<Vec<_>, _>>()?;
54
55        if components.len() < 2 {
56            return Err(format!("Invalid version string: {s}"));
57        }
58
59        Ok(Self { components })
60    }
61}
62
63impl Ord for Version {
64    fn cmp(&self, other: &Self) -> Ordering {
65        for (a, b) in self.components.iter().zip(other.components.iter()) {
66            match a.cmp(b) {
67                Ordering::Equal => continue,
68                ordering => return ordering,
69            }
70        }
71        self.components.len().cmp(&other.components.len())
72    }
73}
74
75impl PartialOrd for Version {
76    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
77        Some(self.cmp(other))
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::Version;
84    use std::str::FromStr;
85
86    #[test]
87    fn test_version_from_str() {
88        let version = Version::from_str("1.2.3").unwrap();
89        assert_eq!(version, Version::new(1, 2, Some(3)));
90
91        // '-' and '.' are equivalent separators in R
92        let version = Version::from_str("2.5-1").unwrap();
93        assert_eq!(version.components, vec![2, 5, 1]);
94
95        // Development versions use a 4th numeric component
96        let version = Version::from_str("1.2.3.9000").unwrap();
97        assert_eq!(version.components, vec![1, 2, 3, 9000]);
98    }
99
100    #[test]
101    fn test_version_cmp() {
102        use std::cmp::Ordering;
103
104        let v1 = Version::from_str("1.2.3").unwrap();
105        let v2 = Version::from_str("1.2.3").unwrap();
106        assert_eq!(v1.cmp(&v2), Ordering::Equal);
107
108        let v1 = Version::from_str("1.2.3").unwrap();
109        let v2 = Version::from_str("1.2.4").unwrap();
110        assert_eq!(v1.cmp(&v2), Ordering::Less);
111
112        // '-' and '.' are equivalent: "2.5-1" == "2.5.1"
113        let v1 = Version::from_str("2.5-1").unwrap();
114        let v2 = Version::from_str("2.5.1").unwrap();
115        assert_eq!(v1.cmp(&v2), Ordering::Equal);
116
117        // Versions can have more than three components
118        let v1 = Version::from_str("1.2.3.9000").unwrap();
119        let v2 = Version::from_str("1.2.3").unwrap();
120        assert_eq!(v1.cmp(&v2), Ordering::Greater);
121
122        let v1 = Version::from_str("1.2.3.9000").unwrap();
123        let v2 = Version::from_str("1.2.4").unwrap();
124        assert_eq!(v1.cmp(&v2), Ordering::Less);
125    }
126
127    #[test]
128    fn test_version_display() {
129        // Display normalizes to '.' separator
130        let version = Version::from_str("1.2.3").unwrap();
131        assert_eq!(version.to_string(), "1.2.3");
132
133        let version = Version::from_str("2.5-1").unwrap();
134        assert_eq!(version.to_string(), "2.5.1");
135    }
136
137    #[test]
138    fn test_version_invalid() {
139        assert!(Version::from_str("a").is_err());
140        assert!(Version::from_str("1.a.3").is_err());
141        // Single component is not a valid R package version
142        assert!(Version::from_str("1").is_err());
143    }
144}