h_version/
version.rs

1use std::cmp::Ordering;
2use std::fmt::{Debug, Display};
3
4///
5///
6/// # Example
7/// parts of version can be ignored
8/// ```
9/// use std::cmp::Ordering::Greater;
10/// use h_version::Version;
11/// let version1 = Version::parse("1:123.543.57-alpha+001");
12/// let version2 = Version::parse("1:123.543.56-beta+002");
13/// assert_eq!(version1.cmp(&version2),Greater);
14/// ```
15pub struct Version {
16    pub epoch: Option<u64>, // epochs (e.g., "1:2.3.4")
17    pub components: Vec<String>, // Main version components (e.g., 1.2.3)
18    pub pre_release: Option<String>, // Pre-release tag (e.g., "alpha", "beta", "Snapshot", "rc")
19    pub build_metadata: Option<String>, // Build metadata (e.g., "+001")
20}
21impl Version {
22    /// makes a version from a str.
23    /// # Example
24    /// ```
25    /// use h_version::Version;
26    /// let version = Version::parse("1:23423.553.845-rc+255");
27    /// let version = version.to_string();
28    /// assert_eq!(version,"1:23423.553.845-rc+255".to_string());
29    pub fn parse(version_str: &str) -> Self {
30        // Handle epochs
31        let mut parts = version_str.splitn(2, ':');
32        let epoch = parts.next().and_then(|s| s.parse::<u64>().ok());
33        let rest = parts.next().unwrap_or(version_str);
34
35        // Split into main version and build metadata
36        let mut parts = rest.splitn(2, '+');
37        let version_part = parts.next().unwrap_or(rest);
38        let build_metadata = parts.next().map(|s| s.to_string());
39
40        // Split into main version and pre-release
41        let mut parts = version_part.splitn(2, '-');
42        let main_version = parts.next().unwrap_or(version_part);
43        let pre_release = parts.next().map(|s| s.to_string());
44
45        // Split main version into components
46        let components: Vec<String> = main_version
47            .split(['.']) // Split on `.`
48            .map(|s| s.to_string())
49            .collect();
50
51        Version {
52            epoch,
53            components,
54            pre_release,
55            build_metadata,
56        }
57    }
58}
59impl PartialEq for Version {
60    fn eq(&self, other: &Self) -> bool {
61        self.epoch == other.epoch
62            && self.components == other.components
63            && self.pre_release == other.pre_release
64            && self.build_metadata == other.build_metadata
65    }
66    fn ne(&self, other: &Self) -> bool {
67        self.epoch != other.epoch
68            || self.components != other.components
69            || self.pre_release != other.pre_release
70            || self.build_metadata != other.build_metadata
71    }
72}
73impl Eq for Version {}
74
75impl PartialOrd for Version {
76    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
77        Some(self.cmp(other))
78    }
79}
80impl Ord for Version {
81    fn cmp(&self, other: &Self) -> Ordering {
82        // Compare epochs
83        if let Some(epoch_cmp) = self.epoch.partial_cmp(&other.epoch) {
84            if epoch_cmp != Ordering::Equal {
85                return epoch_cmp;
86            }
87        }
88
89        // Compare main components
90        for (a, b) in self.components.iter().zip(&other.components) {
91            let a_num = a.parse::<u64>().ok();
92            let b_num = b.parse::<u64>().ok();
93
94            match (a_num, b_num) {
95                // Numeric comparison
96                (Some(a_num), Some(b_num)) => {
97                    let cmp = a_num.cmp(&b_num);
98                    if cmp != Ordering::Equal {
99                        return cmp;
100                    }
101                }
102                // If one is numeric and the other is not, the numeric one is smaller
103                (Some(_), None) => return Ordering::Less,
104                (None, Some(_)) => return Ordering::Greater,
105                // comparison for non-numeric components
106                (None, None) => {
107                    let cmp = a.cmp(b);
108                    if cmp != Ordering::Equal {
109                        return cmp;
110                    }
111                }
112            }
113        }
114        // If main components are equal, compare pre-releases
115        if self.pre_release.is_some() || other.pre_release.is_some() {
116            return match (&self.pre_release, &other.pre_release) {
117                (None, None) => Ordering::Equal,
118                (None, Some(_)) => Ordering::Greater, // No pre-release is greater
119                (Some(_), None) => Ordering::Less, // Pre-release is less
120                (Some(a), Some(b)) => a.to_lowercase().cmp(&b.to_lowercase()), // Compare pre-releases (because alpha, beta and rc are in order there is no need to compare them one by one. just compare the strings of them.)
121            }
122        }
123        // compare build metadata
124        self.build_metadata.partial_cmp(&other.build_metadata).unwrap()
125    }
126}
127impl Debug for Version {
128    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129        let epoch = self.epoch.unwrap_or_default();
130        let components = &self.components;
131        let pre_release = self.pre_release.clone().unwrap_or_default();
132        let build_metadata = self.epoch.unwrap_or_default();
133        write!(f, "epoch:{epoch} components:{components:?} pre_release:{pre_release} build_metadata:{build_metadata}")
134    }
135}
136impl Display for Version {
137    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138        let mut string = String::new();
139        // epoch
140        let epoch = self.epoch;
141        if let Some(epoch) = epoch {
142            string += epoch.to_string().as_str();
143            string += ":";
144        }
145        // components
146        let components = &self.components;
147        for component in components {
148            string += component.as_str();
149            string += ".";
150        }
151        string.remove(string.len() - 1);
152        // pre_release
153        let pre_release = self.pre_release.clone();
154        if let Some(pre_release) = pre_release {
155            string += "-";
156            string += pre_release.as_str();
157        }
158        let build_metadata = self.build_metadata.clone();
159        if let Some(build_metadata) = build_metadata {
160            string += "+";
161            string += build_metadata.as_str();
162        }
163        write!(f, "{}",string)
164    }
165}
166impl Default for Version {
167    fn default() -> Self {
168        Version::parse("0.0.1")
169    }
170}