version_compare/
version.rs

1//! Version module, which provides the `Version` struct as parsed version representation.
2//!
3//! Version numbers in the form of a string are parsed to a `Version` first, before any comparison
4//! is made. This struct provides many methods and features for easy comparison, probing and other
5//! things.
6
7use std::borrow::Borrow;
8use std::cmp::Ordering;
9use std::fmt;
10use std::iter::Peekable;
11use std::slice::Iter;
12
13use crate::{Cmp, Manifest, Part};
14
15/// Version struct, wrapping a string, providing useful comparison functions.
16///
17/// A version in string format can be parsed using methods like `Version::from("1.2.3");`,
18/// returning a `Result` with the parse result.
19///
20/// The original version string can be accessed using `version.as_str()`. A `Version` that isn't
21/// derrived from a version string returns a generated string.
22///
23/// The struct provides many methods for easy comparison and probing.
24///
25/// # Examples
26///
27/// ```
28/// use version_compare::{Version};
29///
30/// let ver = Version::from("1.2.3").unwrap();
31/// ```
32#[derive(Clone, Eq)]
33pub struct Version<'a> {
34    version: &'a str,
35    parts: Vec<Part<'a>>,
36    manifest: Option<&'a Manifest>,
37}
38
39impl<'a> Version<'a> {
40    /// Create a `Version` instance from a version string.
41    ///
42    /// The version string should be passed to the `version` parameter.
43    ///
44    /// # Examples
45    ///
46    /// ```
47    /// use version_compare::{Cmp, Version};
48    ///
49    /// let a = Version::from("1.2.3").unwrap();
50    /// let b = Version::from("1.3.0").unwrap();
51    ///
52    /// assert_eq!(a.compare(b), Cmp::Lt);
53    /// ```
54    pub fn from(version: &'a str) -> Option<Self> {
55        Some(Version {
56            version,
57            parts: split_version_str(version, None)?,
58            manifest: None,
59        })
60    }
61
62    /// Create a `Version` instance from already existing parts
63    ///
64    ///
65    /// # Examples
66    ///
67    /// ```
68    /// use version_compare::{Cmp, Version, Part};
69    ///
70    /// let ver = Version::from_parts("1.0", vec![Part::Number(1), Part::Number(0)]);
71    /// ```
72    pub fn from_parts(version: &'a str, parts: Vec<Part<'a>>) -> Self {
73        Version {
74            version,
75            parts,
76            manifest: None,
77        }
78    }
79
80    /// Create a `Version` instance from a version string with the given `manifest`.
81    ///
82    /// The version string should be passed to the `version` parameter.
83    ///
84    /// # Examples
85    ///
86    /// ```
87    /// use version_compare::{Cmp, Version, Manifest};
88    ///
89    /// let manifest = Manifest::default();
90    /// let ver = Version::from_manifest("1.2.3", &manifest).unwrap();
91    ///
92    /// assert_eq!(ver.compare(Version::from("1.2.3").unwrap()), Cmp::Eq);
93    /// ```
94    pub fn from_manifest(version: &'a str, manifest: &'a Manifest) -> Option<Self> {
95        Some(Version {
96            version,
97            parts: split_version_str(version, Some(manifest))?,
98            manifest: Some(manifest),
99        })
100    }
101
102    /// Get the version manifest, if available.
103    ///
104    /// # Examples
105    ///
106    /// ```
107    /// use version_compare::Version;
108    ///
109    /// let version = Version::from("1.2.3").unwrap();
110    ///
111    /// if version.has_manifest() {
112    ///     println!(
113    ///         "Maximum version part depth is {} for this version",
114    ///         version.manifest().unwrap().max_depth.unwrap_or(0),
115    ///     );
116    /// } else {
117    ///     println!("Version has no manifest");
118    /// }
119    /// ```
120    pub fn manifest(&self) -> Option<&Manifest> {
121        self.manifest
122    }
123
124    /// Check whether this version has a manifest.
125    ///
126    /// # Examples
127    ///
128    /// ```
129    /// use version_compare::Version;
130    ///
131    /// let version = Version::from("1.2.3").unwrap();
132    ///
133    /// if version.has_manifest() {
134    ///     println!("This version does have a manifest");
135    /// } else {
136    ///     println!("This version does not have a manifest");
137    /// }
138    /// ```
139    pub fn has_manifest(&self) -> bool {
140        self.manifest().is_some()
141    }
142
143    /// Set the version manifest.
144    ///
145    /// # Examples
146    ///
147    /// ```
148    /// use version_compare::{Version, Manifest};
149    ///
150    /// let manifest = Manifest::default();
151    /// let mut version = Version::from("1.2.3").unwrap();
152    ///
153    /// version.set_manifest(Some(&manifest));
154    /// ```
155    pub fn set_manifest(&mut self, manifest: Option<&'a Manifest>) {
156        self.manifest = manifest;
157
158        // TODO: Re-parse the version string, because the manifest might have changed.
159    }
160
161    /// Get the original version string.
162    ///
163    /// # Examples
164    ///
165    /// ```
166    /// use version_compare::Version;
167    ///
168    /// let ver = Version::from("1.2.3").unwrap();
169    ///
170    /// assert_eq!(ver.as_str(), "1.2.3");
171    /// ```
172    pub fn as_str(&self) -> &str {
173        self.version
174    }
175
176    /// Get a specific version part by it's `index`.
177    /// An error is returned if the given index is out of bound.
178    ///
179    /// # Examples
180    ///
181    /// ```
182    /// use version_compare::{Version, Part};
183    ///
184    /// let ver = Version::from("1.2.3").unwrap();
185    ///
186    /// assert_eq!(ver.part(0), Ok(Part::Number(1)));
187    /// assert_eq!(ver.part(1), Ok(Part::Number(2)));
188    /// assert_eq!(ver.part(2), Ok(Part::Number(3)));
189    /// ```
190    #[allow(clippy::result_unit_err)]
191    pub fn part(&self, index: usize) -> Result<Part<'a>, ()> {
192        // Make sure the index is in-bound
193        if index >= self.parts.len() {
194            return Err(());
195        }
196
197        Ok(self.parts[index])
198    }
199
200    /// Get a vector of all version parts.
201    ///
202    /// # Examples
203    ///
204    /// ```
205    /// use version_compare::{Version, Part};
206    ///
207    /// let ver = Version::from("1.2.3").unwrap();
208    ///
209    /// assert_eq!(ver.parts(), [
210    ///     Part::Number(1),
211    ///     Part::Number(2),
212    ///     Part::Number(3)
213    /// ]);
214    /// ```
215    pub fn parts(&self) -> &[Part<'a>] {
216        self.parts.as_slice()
217    }
218
219    /// Compare this version to the given `other` version using the default `Manifest`.
220    ///
221    /// This method returns one of the following comparison operators:
222    ///
223    /// * `Lt`
224    /// * `Eq`
225    /// * `Gt`
226    ///
227    /// Other comparison operators can be used when comparing, but aren't returned by this method.
228    ///
229    /// # Examples:
230    ///
231    /// ```
232    /// use version_compare::{Cmp, Version};
233    ///
234    /// let a = Version::from("1.2").unwrap();
235    /// let b = Version::from("1.3.2").unwrap();
236    ///
237    /// assert_eq!(a.compare(&b), Cmp::Lt);
238    /// assert_eq!(b.compare(&a), Cmp::Gt);
239    /// assert_eq!(a.compare(&a), Cmp::Eq);
240    /// ```
241    pub fn compare<V>(&self, other: V) -> Cmp
242    where
243        V: Borrow<Version<'a>>,
244    {
245        compare_iter(
246            self.parts.iter().peekable(),
247            other.borrow().parts.iter().peekable(),
248            self.manifest,
249        )
250    }
251
252    /// Compare this version to the given `other` version,
253    /// and check whether the given comparison operator is valid using the default `Manifest`.
254    ///
255    /// All comparison operators can be used.
256    ///
257    /// # Examples:
258    ///
259    /// ```
260    /// use version_compare::{Cmp, Version};
261    ///
262    /// let a = Version::from("1.2").unwrap();
263    /// let b = Version::from("1.3.2").unwrap();
264    ///
265    /// assert!(a.compare_to(&b, Cmp::Lt));
266    /// assert!(a.compare_to(&b, Cmp::Le));
267    /// assert!(a.compare_to(&a, Cmp::Eq));
268    /// assert!(a.compare_to(&a, Cmp::Le));
269    /// ```
270    pub fn compare_to<V>(&self, other: V, operator: Cmp) -> bool
271    where
272        V: Borrow<Version<'a>>,
273    {
274        match self.compare(other) {
275            Cmp::Eq => matches!(operator, Cmp::Eq | Cmp::Le | Cmp::Ge),
276            Cmp::Lt => matches!(operator, Cmp::Ne | Cmp::Lt | Cmp::Le),
277            Cmp::Gt => matches!(operator, Cmp::Ne | Cmp::Gt | Cmp::Ge),
278            _ => unreachable!(),
279        }
280    }
281}
282
283impl<'a> fmt::Display for Version<'a> {
284    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
285        write!(f, "{}", self.version)
286    }
287}
288
289// Show just the version component parts as debug output
290impl<'a> fmt::Debug for Version<'a> {
291    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
292        if f.alternate() {
293            write!(f, "{:#?}", self.parts)
294        } else {
295            write!(f, "{:?}", self.parts)
296        }
297    }
298}
299
300/// Implement the partial ordering trait for the version struct, to easily allow version comparison.
301impl<'a> PartialOrd for Version<'a> {
302    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
303        Some(self.compare(other).ord().unwrap())
304    }
305}
306
307/// Implement the partial equality trait for the version struct, to easily allow version comparison.
308impl<'a> PartialEq for Version<'a> {
309    fn eq(&self, other: &Self) -> bool {
310        self.compare_to(other, Cmp::Eq)
311    }
312}
313
314/// Split the given version string, in it's version parts.
315fn split_version_str<'a>(
316    version: &'a str,
317    manifest: Option<&'a Manifest>,
318) -> Option<Vec<Part<'a>>> {
319    // Split the version string, and create a vector to put the parts in
320    let split = version.split(|c| !char::is_alphanumeric(c));
321    let mut parts = Vec::new();
322
323    // Get the manifest to follow
324    let mut used_manifest = &Manifest::default();
325    if let Some(m) = manifest {
326        used_manifest = m;
327    }
328
329    // Loop over the parts, and parse them
330    for part in split {
331        // We may not go over the maximum depth
332        if used_manifest.max_depth.is_some() && parts.len() >= used_manifest.max_depth.unwrap_or(0)
333        {
334            break;
335        }
336
337        // Skip empty parts
338        if part.is_empty() {
339            continue;
340        }
341
342        // Try to parse the value as an number
343        match part.parse::<i32>() {
344            Ok(number) => {
345                // For GNU ordering we parse numbers with leading zero as string
346                if number > 0
347                    && part.starts_with('0')
348                    && manifest.map(|m| m.gnu_ordering).unwrap_or(false)
349                {
350                    parts.push(Part::Text(part));
351                    continue;
352                }
353
354                // Push the number part to the vector
355                parts.push(Part::Number(number));
356            }
357            Err(_) => {
358                // Ignore text parts if specified
359                if used_manifest.ignore_text {
360                    continue;
361                }
362
363                // Numbers suffixed by text should be split into a number and text as well,
364                // if the number overflows, handle it as text
365                let split_at = part
366                    .char_indices()
367                    .take(part.len() - 1)
368                    .take_while(|(_, c)| c.is_ascii_digit())
369                    .map(|(i, c)| (i, c, part.chars().nth(i + 1).unwrap()))
370                    .filter(|(_, _, b)| b.is_alphabetic())
371                    .map(|(i, _, _)| i)
372                    .next();
373                if let Some(at) = split_at {
374                    if let Ok(n) = part[..=at].parse() {
375                        parts.push(Part::Number(n));
376                        parts.push(Part::Text(&part[at + 1..]));
377                    } else {
378                        parts.push(Part::Text(part));
379                    }
380                    continue;
381                }
382
383                // Push the text part to the vector
384                parts.push(Part::Text(part))
385            }
386        }
387    }
388
389    // The version must contain a number part if any part was parsed
390    if !parts.is_empty() && !parts.iter().any(|p| matches!(p, Part::Number(_))) {
391        return None;
392    }
393
394    // Return the list of parts
395    Some(parts)
396}
397
398/// Compare two version numbers based on the iterators of their version parts.
399///
400/// This method returns one of the following comparison operators:
401///
402/// * `Lt`
403/// * `Eq`
404/// * `Gt`
405///
406/// Other comparison operators can be used when comparing, but aren't returned by this method.
407fn compare_iter<'a>(
408    mut iter: Peekable<Iter<Part<'a>>>,
409    mut other_iter: Peekable<Iter<Part<'a>>>,
410    manifest: Option<&Manifest>,
411) -> Cmp {
412    // Iterate over the iterator, without consuming it
413    for part in &mut iter {
414        match (part, other_iter.next()) {
415            // If we only have a zero on the lhs, continue
416            (Part::Number(lhs), None) if lhs == &0 => {
417                continue;
418            }
419
420            // If we only have text on the lhs, it is less
421            (Part::Text(_), None) => return Cmp::Lt,
422
423            // If we have anything else on the lhs, it is greater
424            (_, None) => return Cmp::Gt,
425
426            // Compare numbers
427            (Part::Number(lhs), Some(Part::Number(rhs))) => match Cmp::from(lhs.cmp(rhs)) {
428                Cmp::Eq => {}
429                cmp => return cmp,
430            },
431
432            // Compare text
433            (Part::Text(lhs), Some(Part::Text(rhs))) => {
434                // Normalize case and compare text: "RC1" will be less than "RC2"
435                match Cmp::from(lhs.to_lowercase().cmp(&rhs.to_lowercase())) {
436                    Cmp::Eq => {}
437                    cmp => return cmp,
438                }
439            }
440
441            // For GNU ordering we have a special number/text comparison
442            (lhs @ Part::Number(_), Some(rhs @ Part::Text(_)))
443            | (lhs @ Part::Text(_), Some(rhs @ Part::Number(_)))
444                if manifest.map(|m| m.gnu_ordering).unwrap_or(false) =>
445            {
446                match compare_gnu_number_text(lhs, rhs) {
447                    Some(Cmp::Eq) | None => {}
448                    Some(cmp) => return cmp,
449                }
450            }
451
452            // TODO: decide what to do for other type combinations
453            _ => {}
454        }
455    }
456
457    // Check whether we should iterate over the other iterator, if it has any items left
458    match other_iter.peek() {
459        // Compare based on the other iterator
460        Some(_) => compare_iter(other_iter, iter, manifest).flip(),
461
462        // Nothing more to iterate over, the versions should be equal
463        None => Cmp::Eq,
464    }
465}
466
467/// Special logic for comparing a number and text with GNU ordering.
468///
469/// Numbers should be ordered like this:
470///
471/// - 3
472/// - 04
473/// - 4
474// TODO: this is not efficient, find a better method
475fn compare_gnu_number_text(lhs: &Part, rhs: &Part) -> Option<Cmp> {
476    // Both values must be parsable as numbers
477    let lhs_num = match lhs {
478        Part::Number(n) => *n,
479        Part::Text(n) => n.parse().ok()?,
480    };
481    let rhs_num = match rhs {
482        Part::Number(n) => *n,
483        Part::Text(n) => n.parse().ok()?,
484    };
485
486    // Return ordering if numeric values are different
487    match lhs_num.cmp(&rhs_num).into() {
488        Cmp::Eq => {}
489        cmp => return Some(cmp),
490    }
491
492    // Either value must have a leading zero
493    if !matches!(lhs, Part::Text(t) if t.starts_with('0'))
494        && !matches!(rhs, Part::Text(t) if t.starts_with('0'))
495    {
496        return None;
497    }
498
499    let lhs = match lhs {
500        Part::Number(n) => format!("{}", n),
501        Part::Text(n) => n.to_string(),
502    };
503    let rhs = match rhs {
504        Part::Number(n) => format!("{}", n),
505        Part::Text(n) => n.to_string(),
506    };
507
508    Some(lhs.cmp(&rhs).into())
509}
510
511#[cfg_attr(tarpaulin, skip)]
512#[cfg(test)]
513mod tests {
514    use std::cmp;
515
516    use crate::test::{COMBIS, VERSIONS, VERSIONS_ERROR};
517    use crate::{Cmp, Manifest, Part};
518
519    use super::Version;
520
521    #[test]
522    // TODO: This doesn't really test whether this method fully works
523    fn from() {
524        // Test whether parsing works for each test version
525        for version in VERSIONS {
526            assert!(Version::from(version.0).is_some());
527        }
528
529        // Test whether parsing works for each test invalid version
530        for version in VERSIONS_ERROR {
531            assert!(Version::from(version.0).is_none());
532        }
533    }
534
535    #[test]
536    // TODO: This doesn't really test whether this method fully works
537    fn from_manifest() {
538        // Create a manifest
539        let manifest = Manifest::default();
540
541        // Test whether parsing works for each test version
542        for version in VERSIONS {
543            assert_eq!(
544                Version::from_manifest(version.0, &manifest)
545                    .unwrap()
546                    .manifest,
547                Some(&manifest)
548            );
549        }
550
551        // Test whether parsing works for each test invalid version
552        for version in VERSIONS_ERROR {
553            assert!(Version::from_manifest(version.0, &manifest).is_none());
554        }
555    }
556
557    #[test]
558    fn manifest() {
559        let manifest = Manifest::default();
560        let mut version = Version::from("1.2.3").unwrap();
561
562        version.manifest = Some(&manifest);
563        assert_eq!(version.manifest(), Some(&manifest));
564
565        version.manifest = None;
566        assert_eq!(version.manifest(), None);
567    }
568
569    #[test]
570    fn has_manifest() {
571        let manifest = Manifest::default();
572        let mut version = Version::from("1.2.3").unwrap();
573
574        version.manifest = Some(&manifest);
575        assert!(version.has_manifest());
576
577        version.manifest = None;
578        assert!(!version.has_manifest());
579    }
580
581    #[test]
582    fn set_manifest() {
583        let manifest = Manifest::default();
584        let mut version = Version::from("1.2.3").unwrap();
585
586        version.set_manifest(Some(&manifest));
587        assert_eq!(version.manifest, Some(&manifest));
588
589        version.set_manifest(None);
590        assert_eq!(version.manifest, None);
591    }
592
593    #[test]
594    fn as_str() {
595        // Test for each test version
596        for version in VERSIONS {
597            // The input version string must be the same as the returned string
598            assert_eq!(Version::from(version.0).unwrap().as_str(), version.0);
599        }
600    }
601
602    #[test]
603    fn part() {
604        // Test for each test version
605        for version in VERSIONS {
606            // Create a version object
607            let ver = Version::from(version.0).unwrap();
608
609            // Loop through each part
610            for i in 0..version.1 {
611                assert_eq!(ver.part(i), Ok(ver.parts[i]));
612            }
613
614            // A value outside the range must return an error
615            assert!(ver.part(version.1).is_err());
616        }
617    }
618
619    #[test]
620    fn parts() {
621        // Test for each test version
622        for version in VERSIONS {
623            // The number of parts must match
624            assert_eq!(Version::from(version.0).unwrap().parts().len(), version.1);
625        }
626    }
627
628    #[test]
629    fn parts_max_depth() {
630        // Create a manifest
631        let mut manifest = Manifest::default();
632
633        // Loop through a range of numbers
634        for depth in 0..5 {
635            // Set the maximum depth
636            manifest.max_depth = if depth > 0 { Some(depth) } else { None };
637
638            // Test for each test version with the manifest
639            for version in VERSIONS {
640                // Create a version object, and count it's parts
641                let ver = Version::from_manifest(version.0, &manifest);
642
643                // Some versions might be none, because not all of the start with a number when the
644                // maximum depth is 1. A version string with only text isn't allowed,
645                // resulting in none.
646                if ver.is_none() {
647                    continue;
648                }
649
650                // Get the part count
651                let count = ver.unwrap().parts().len();
652
653                // The number of parts must match
654                if depth == 0 {
655                    assert_eq!(count, version.1);
656                } else {
657                    assert_eq!(count, cmp::min(version.1, depth));
658                }
659            }
660        }
661    }
662
663    #[test]
664    fn parts_ignore_text() {
665        // Create a manifest
666        let mut manifest = Manifest::default();
667
668        // Try this for true and false
669        for ignore in &[true, false] {
670            // Set to ignore text
671            manifest.ignore_text = *ignore;
672
673            // Keep track whether any version passed with text
674            let mut had_text = false;
675
676            // Test each test version
677            for version in VERSIONS {
678                // Create a version instance, and get it's parts
679                let ver = Version::from_manifest(version.0, &manifest).unwrap();
680
681                // Loop through all version parts
682                for part in ver.parts() {
683                    if let Part::Text(_) = part {
684                        // Set the flag
685                        had_text = true;
686
687                        // Break the loop if we already reached text when not ignored
688                        if !ignore {
689                            break;
690                        }
691                    }
692                }
693            }
694
695            // Assert had text
696            assert_eq!(had_text, !ignore);
697        }
698    }
699
700    #[test]
701    fn compare() {
702        // Compare each version in the version set
703        for entry in COMBIS {
704            // Get both versions
705            let (a, b) = entry.versions();
706
707            // Compare them
708            assert_eq!(
709                a.compare(b),
710                entry.2.clone(),
711                "Testing that {} is {} {}",
712                entry.0,
713                entry.2.sign(),
714                entry.1,
715            );
716        }
717    }
718
719    #[test]
720    fn compare_to() {
721        // Compare each version in the version set
722        for entry in COMBIS.iter().filter(|c| c.3.is_none()) {
723            // Get both versions
724            let (a, b) = entry.versions();
725
726            // Test normally and inverse
727            assert!(a.compare_to(&b, entry.2));
728            assert!(!a.compare_to(b, entry.2.invert()));
729        }
730
731        // Assert an exceptional case, compare to not equal
732        assert!(Version::from("1.2")
733            .unwrap()
734            .compare_to(Version::from("1.2.3").unwrap(), Cmp::Ne,));
735    }
736
737    #[test]
738    fn display() {
739        assert_eq!(format!("{}", Version::from("1.2.3").unwrap()), "1.2.3");
740    }
741
742    #[test]
743    fn debug() {
744        assert_eq!(
745            format!("{:?}", Version::from("1.2.3").unwrap()),
746            "[Number(1), Number(2), Number(3)]",
747        );
748        assert_eq!(
749            format!("{:#?}", Version::from("1.2.3").unwrap()),
750            "[\n    Number(\n        1,\n    ),\n    Number(\n        2,\n    ),\n    Number(\n        3,\n    ),\n]",
751        );
752    }
753
754    #[test]
755    fn partial_cmp() {
756        // Compare each version in the version set
757        for entry in COMBIS {
758            // Get both versions
759            let (a, b) = entry.versions();
760
761            // Compare and assert
762            match entry.2 {
763                Cmp::Eq => assert!(a == b),
764                Cmp::Lt => assert!(a < b),
765                Cmp::Gt => assert!(a > b),
766                _ => {}
767            }
768        }
769    }
770
771    #[test]
772    fn partial_eq() {
773        // Compare each version in the version set
774        for entry in COMBIS {
775            // Skip entries that are less or equal, or greater or equal
776            match entry.2 {
777                Cmp::Le | Cmp::Ge => continue,
778                _ => {}
779            }
780
781            // Get both versions
782            let (a, b) = entry.versions();
783
784            // Determine what the result should be
785            let result = matches!(entry.2, Cmp::Eq);
786
787            // Test
788            assert_eq!(a == b, result);
789        }
790
791        // Assert an exceptional case, compare to not equal
792        assert!(Version::from("1.2").unwrap() != Version::from("1.2.3").unwrap());
793    }
794}