1use std::cmp::Ordering;
2use std::fmt;
3use thiserror::Error;
4
5#[derive(Debug, Clone, Eq, PartialEq)]
10pub enum Segment {
11 Numeric(u64),
12 String(String),
13}
14
15impl PartialOrd for Segment {
16 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
17 Some(self.cmp(other))
18 }
19}
20
21impl Ord for Segment {
22 fn cmp(&self, other: &Self) -> Ordering {
23 match (self, other) {
24 (Segment::Numeric(a), Segment::Numeric(b)) => a.cmp(b),
25 (Segment::String(a), Segment::String(b)) => a.cmp(b),
26 (Segment::Numeric(_), Segment::String(_)) => Ordering::Greater,
28 (Segment::String(_), Segment::Numeric(_)) => Ordering::Less,
29 }
30 }
31}
32
33impl fmt::Display for Segment {
34 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35 match self {
36 Segment::Numeric(n) => write!(f, "{}", n),
37 Segment::String(s) => write!(f, "{}", s),
38 }
39 }
40}
41
42#[derive(Debug, Clone, Eq)]
63pub struct Version {
64 segments: Vec<Segment>,
65 original: String,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Error)]
69pub enum VersionError {
70 #[error("empty version string")]
71 Empty,
72 #[error("invalid character in version: '{0}'")]
73 InvalidCharacter(char),
74 #[error("invalid version format: '{0}'")]
75 InvalidFormat(String),
76}
77
78impl Version {
79 pub fn parse(input: &str) -> Result<Self, VersionError> {
88 let input = input.trim();
89
90 if input.is_empty() {
91 return Ok(Version {
92 segments: vec![Segment::Numeric(0)],
93 original: "0".to_string(),
94 });
95 }
96
97 for c in input.chars() {
99 if !c.is_ascii_alphanumeric() && c != '.' && c != '-' {
100 return Err(VersionError::InvalidCharacter(c));
101 }
102 }
103
104 let normalized = input.replace('-', ".pre.");
106
107 let segments = parse_segments(&normalized)?;
108
109 if segments.is_empty() {
110 return Err(VersionError::InvalidFormat(input.to_string()));
111 }
112
113 Ok(Version {
114 segments,
115 original: input.to_string(),
116 })
117 }
118
119 pub fn is_prerelease(&self) -> bool {
123 self.segments
124 .iter()
125 .any(|s| matches!(s, Segment::String(_)))
126 }
127
128 pub fn bump(&self) -> Version {
135 let mut new_segments = self.segments.clone();
141
142 while new_segments
144 .last()
145 .is_some_and(|s| matches!(s, Segment::String(_)))
146 {
147 new_segments.pop();
148 }
149
150 if new_segments.len() > 1 {
152 new_segments.pop();
153 }
154
155 if let Some(Segment::Numeric(n)) = new_segments.last_mut() {
157 *n += 1;
158 }
159
160 if new_segments.is_empty() {
161 new_segments.push(Segment::Numeric(1));
162 }
163
164 let original = new_segments
165 .iter()
166 .map(|s| s.to_string())
167 .collect::<Vec<_>>()
168 .join(".");
169
170 Version {
171 segments: new_segments,
172 original,
173 }
174 }
175
176 pub fn increment_last(&self) -> Version {
185 let mut new_segments = self.segments.clone();
186
187 for seg in new_segments.iter_mut().rev() {
189 if let Segment::Numeric(n) = seg {
190 *n += 1;
191 break;
192 }
193 }
194
195 let original = new_segments
196 .iter()
197 .map(|s| s.to_string())
198 .collect::<Vec<_>>()
199 .join(".");
200
201 Version {
202 segments: new_segments,
203 original,
204 }
205 }
206
207 pub fn append_zero(&self) -> Version {
211 let mut new_segments = self.segments.clone();
212 new_segments.push(Segment::Numeric(0));
213
214 let original = new_segments
215 .iter()
216 .map(|s| s.to_string())
217 .collect::<Vec<_>>()
218 .join(".");
219
220 Version {
221 segments: new_segments,
222 original,
223 }
224 }
225
226 pub fn segments(&self) -> &[Segment] {
228 &self.segments
229 }
230}
231
232fn parse_segments(input: &str) -> Result<Vec<Segment>, VersionError> {
234 let mut segments = Vec::new();
235
236 for part in input.split('.') {
237 if part.is_empty() {
238 continue;
239 }
240
241 let mut chars = part.chars().peekable();
244 while chars.peek().is_some() {
245 let first = *chars.peek().unwrap();
246 if first.is_ascii_digit() {
247 let mut num_str = String::new();
248 while let Some(&c) = chars.peek() {
249 if c.is_ascii_digit() {
250 num_str.push(c);
251 chars.next();
252 } else {
253 break;
254 }
255 }
256 let n: u64 = num_str.parse().map_err(|_| {
257 VersionError::InvalidFormat(format!("numeric overflow: {}", num_str))
258 })?;
259 segments.push(Segment::Numeric(n));
260 } else if first.is_ascii_alphabetic() {
261 let mut s = String::new();
262 while let Some(&c) = chars.peek() {
263 if c.is_ascii_alphabetic() {
264 s.push(c);
265 chars.next();
266 } else {
267 break;
268 }
269 }
270 segments.push(Segment::String(s));
271 } else {
272 return Err(VersionError::InvalidCharacter(first));
273 }
274 }
275 }
276
277 Ok(segments)
278}
279
280impl PartialEq for Version {
281 fn eq(&self, other: &Self) -> bool {
282 self.cmp(other) == Ordering::Equal
283 }
284}
285
286impl PartialOrd for Version {
287 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
288 Some(self.cmp(other))
289 }
290}
291
292impl Ord for Version {
293 fn cmp(&self, other: &Self) -> Ordering {
294 let a = &self.segments;
295 let b = &other.segments;
296 let max_len = a.len().max(b.len());
297
298 for i in 0..max_len {
299 let seg_a = a.get(i);
300 let seg_b = b.get(i);
301
302 let ord = match (seg_a, seg_b) {
303 (Some(sa), Some(sb)) => sa.cmp(sb),
304 (Some(sa), None) => sa.cmp(&Segment::Numeric(0)),
306 (None, Some(sb)) => Segment::Numeric(0).cmp(sb),
307 (None, None) => Ordering::Equal,
308 };
309
310 if ord != Ordering::Equal {
311 return ord;
312 }
313 }
314
315 Ordering::Equal
316 }
317}
318
319impl fmt::Display for Version {
320 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
321 write!(f, "{}", self.original)
322 }
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328
329 #[test]
332 fn parse_simple_version() {
333 let v = Version::parse("1.2.3").unwrap();
334 assert_eq!(
335 v.segments,
336 vec![
337 Segment::Numeric(1),
338 Segment::Numeric(2),
339 Segment::Numeric(3)
340 ]
341 );
342 }
343
344 #[test]
345 fn parse_single_segment() {
346 let v = Version::parse("5").unwrap();
347 assert_eq!(v.segments, vec![Segment::Numeric(5)]);
348 }
349
350 #[test]
351 fn parse_with_leading_zeros() {
352 let v = Version::parse("01.02.03").unwrap();
353 assert_eq!(
354 v.segments,
355 vec![
356 Segment::Numeric(1),
357 Segment::Numeric(2),
358 Segment::Numeric(3)
359 ]
360 );
361 }
362
363 #[test]
364 fn parse_prerelease_with_dot() {
365 let v = Version::parse("1.0.0.alpha").unwrap();
366 assert_eq!(
367 v.segments,
368 vec![
369 Segment::Numeric(1),
370 Segment::Numeric(0),
371 Segment::Numeric(0),
372 Segment::String("alpha".to_string()),
373 ]
374 );
375 assert!(v.is_prerelease());
376 }
377
378 #[test]
379 fn parse_prerelease_inline() {
380 let v = Version::parse("1.0.0rc1").unwrap();
381 assert_eq!(
382 v.segments,
383 vec![
384 Segment::Numeric(1),
385 Segment::Numeric(0),
386 Segment::Numeric(0),
387 Segment::String("rc".to_string()),
388 Segment::Numeric(1),
389 ]
390 );
391 }
392
393 #[test]
394 fn parse_prerelease_with_hyphen() {
395 let v = Version::parse("1.0.0-rc1").unwrap();
396 assert_eq!(
397 v.segments,
398 vec![
399 Segment::Numeric(1),
400 Segment::Numeric(0),
401 Segment::Numeric(0),
402 Segment::String("pre".to_string()),
403 Segment::String("rc".to_string()),
404 Segment::Numeric(1),
405 ]
406 );
407 }
408
409 #[test]
410 fn parse_empty_string() {
411 let v = Version::parse("").unwrap();
412 assert_eq!(v.segments, vec![Segment::Numeric(0)]);
413 }
414
415 #[test]
416 fn parse_invalid_character() {
417 assert!(Version::parse("1.0+build").is_err());
418 assert!(Version::parse("1.0_pre1").is_err());
419 }
420
421 #[test]
424 fn compare_simple_versions() {
425 let v1 = Version::parse("1.0.0").unwrap();
426 let v2 = Version::parse("1.0.1").unwrap();
427 assert!(v1 < v2);
428 }
429
430 #[test]
431 fn compare_major_versions() {
432 let v1 = Version::parse("1.0.0").unwrap();
433 let v2 = Version::parse("2.0.0").unwrap();
434 assert!(v1 < v2);
435 }
436
437 #[test]
438 fn trailing_zeros_are_equal() {
439 let v1 = Version::parse("1.0").unwrap();
440 let v2 = Version::parse("1.0.0").unwrap();
441 let v3 = Version::parse("1.0.0.0").unwrap();
442 assert_eq!(v1, v2);
443 assert_eq!(v2, v3);
444 assert_eq!(v1, v3);
445 }
446
447 #[test]
448 fn single_segment_equals_with_trailing_zeros() {
449 let v1 = Version::parse("1").unwrap();
450 let v2 = Version::parse("1.0").unwrap();
451 assert_eq!(v1, v2);
452 }
453
454 #[test]
455 fn prerelease_less_than_release() {
456 let pre = Version::parse("1.0.0.alpha").unwrap();
457 let rel = Version::parse("1.0.0").unwrap();
458 assert!(pre < rel);
459 }
460
461 #[test]
462 fn prerelease_inline_less_than_release() {
463 let pre = Version::parse("1.0.0a").unwrap();
464 let rel = Version::parse("1.0.0").unwrap();
465 assert!(pre < rel);
466 }
467
468 #[test]
469 fn prerelease_ordering() {
470 let alpha = Version::parse("1.0.0.alpha").unwrap();
471 let beta = Version::parse("1.0.0.beta").unwrap();
472 let rc = Version::parse("1.0.0.rc").unwrap();
473 let release = Version::parse("1.0.0").unwrap();
474
475 assert!(alpha < beta);
476 assert!(beta < rc);
477 assert!(rc < release);
478 }
479
480 #[test]
481 fn prerelease_a_b_ordering() {
482 let a = Version::parse("1.0.0a").unwrap();
483 let b = Version::parse("1.0.0b").unwrap();
484 assert!(a < b);
485 }
486
487 #[test]
488 fn string_segment_less_than_integer() {
489 let with_str = Version::parse("1.0.0.alpha").unwrap();
491 let without = Version::parse("1.0.0").unwrap();
492 assert!(with_str < without);
493
494 let with_str2 = Version::parse("1.0.0a").unwrap();
495 assert!(with_str2 < without);
496 }
497
498 #[test]
499 fn compare_different_lengths() {
500 let short = Version::parse("1.0").unwrap();
501 let long = Version::parse("1.0.1").unwrap();
502 assert!(short < long);
503 }
504
505 #[test]
506 fn compare_year_based_versions() {
507 let v1 = Version::parse("2020.1.1").unwrap();
508 let v2 = Version::parse("2021.1.1").unwrap();
509 assert!(v1 < v2);
510 }
511
512 #[test]
515 fn not_prerelease() {
516 let v = Version::parse("1.2.3").unwrap();
517 assert!(!v.is_prerelease());
518 }
519
520 #[test]
521 fn is_prerelease_with_alpha() {
522 let v = Version::parse("1.0.0.alpha").unwrap();
523 assert!(v.is_prerelease());
524 }
525
526 #[test]
527 fn is_prerelease_inline() {
528 let v = Version::parse("1.0.0rc1").unwrap();
529 assert!(v.is_prerelease());
530 }
531
532 #[test]
533 fn four_segment_numeric_not_prerelease() {
534 let v = Version::parse("1.0.0.1").unwrap();
535 assert!(!v.is_prerelease());
536 }
537
538 #[test]
541 fn bump_three_segments() {
542 let v = Version::parse("1.2.3").unwrap();
543 let bumped = v.bump();
544 assert_eq!(bumped, Version::parse("1.3").unwrap());
545 }
546
547 #[test]
548 fn bump_two_segments() {
549 let v = Version::parse("1.0").unwrap();
550 let bumped = v.bump();
551 assert_eq!(bumped, Version::parse("2").unwrap());
552 }
553
554 #[test]
555 fn bump_single_segment() {
556 let v = Version::parse("1").unwrap();
557 let bumped = v.bump();
558 assert_eq!(bumped, Version::parse("2").unwrap());
559 }
560
561 #[test]
562 fn bump_four_segments() {
563 let v = Version::parse("1.2.3.4").unwrap();
564 let bumped = v.bump();
565 assert_eq!(bumped, Version::parse("1.2.4").unwrap());
566 }
567
568 #[test]
569 fn bump_with_trailing_zeros() {
570 let v = Version::parse("1.0.0").unwrap();
573 let bumped = v.bump();
574 assert_eq!(bumped, Version::parse("1.1").unwrap());
575 }
576
577 #[test]
580 fn display_preserves_original() {
581 let v = Version::parse("1.2.3").unwrap();
582 assert_eq!(v.to_string(), "1.2.3");
583 }
584
585 #[test]
586 fn display_prerelease() {
587 let v = Version::parse("1.0.0.alpha").unwrap();
588 assert_eq!(v.to_string(), "1.0.0.alpha");
589 }
590}