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, operator: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}
795