use std::{
convert::TryFrom,
fmt::{Display, Formatter, Result as FmtResult},
str::FromStr,
};
use nom::{combinator::all_consuming, Parser};
#[cfg(feature = "serialization")]
use serde::{Deserialize, Serialize};
use time::{Date, OffsetDateTime, Weekday};
use crate::{
error::{Error, Result},
parser::parse_version,
};
pub type Year = i32;
pub type Week = u8;
pub type Patch = u32;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
pub struct Version {
pub year: Year,
pub week: Week,
pub patch: Patch,
}
impl Version {
pub fn new(year: Year, week: Week, patch: Patch) -> Result<Self> {
if !(1..=53).contains(&week) {
return Err(Error::InvalidWeek {
week,
});
}
let full_year = Self::full_year(year);
match Date::from_iso_week_date(full_year, week, Weekday::Monday) {
Ok(_) => Ok(Self {
year,
week,
patch,
}),
Err(_) => Err(Error::InvalidYearWeek {
year,
week,
}),
}
}
pub fn parse<S>(text: S) -> Result<Self>
where
S: AsRef<str>,
{
let text = text.as_ref();
text.parse().map_err(|_| Error::invalid_version(text))
}
pub fn today() -> Self {
let now = OffsetDateTime::now_utc();
let year = now.year();
let week = now.iso_week();
let short_year = Self::short_year(year);
Self {
year: short_year,
week,
patch: 0,
}
}
pub fn increment_patch(&mut self) -> &mut Self {
self.patch += 1;
self
}
pub fn next_week(&mut self) -> Result<&mut Self> {
self.week += 1;
if self.week > 53 {
self.year += 1;
self.week = 1;
}
let full_year = Self::full_year(self.year);
match Date::from_iso_week_date(full_year, self.week, Weekday::Monday) {
Ok(_) => {
self.patch = 0;
Ok(self)
},
Err(_) => {
self.week += 1;
if self.week > 53 {
self.year += 1;
self.week = 1;
}
self.patch = 0;
Ok(self)
},
}
}
pub fn short_year(year: Year) -> Year {
year - 2000
}
pub fn full_year(year: Year) -> Year {
year + 2000
}
pub fn same_week(&self, other: &Self) -> bool {
self.year == other.year && self.week == other.week
}
}
impl FromStr for Version {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
if s.is_empty() {
return Err(Error::invalid_version("empty string"));
}
Version::try_from(s)
}
}
impl TryFrom<&str> for Version {
type Error = Error;
fn try_from(s: &str) -> Result<Self> {
all_consuming(parse_version)
.parse(s)
.map(|(_, version)| version)
.map_err(|e| match e {
nom::Err::Error(err) | nom::Err::Failure(err) => Error::from_nom(s, err),
nom::Err::Incomplete(_) => Error::format(s),
})
}
}
impl Display for Version {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(f, "{}.{}.{}", self.year, self.week, self.patch)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_version_parse() {
let v: Version = "25.12.3".parse().unwrap();
assert_eq!(v, Version {
year: 25,
week: 12,
patch: 3
});
}
#[test]
fn test_version_from_str_invalid() {
assert!("25.54.1".parse::<Version>().is_err());
assert!("invalid.version".parse::<Version>().is_err());
}
#[test]
fn test_version_display() {
let version = Version {
year: 25,
week: 12,
patch: 3,
};
assert_eq!(version.to_string(), "25.12.3");
}
#[test]
fn test_version_ordering() {
let v1 = Version {
year: 25,
week: 10,
patch: 1,
};
let v2 = Version {
year: 25,
week: 10,
patch: 2,
};
let v3 = Version {
year: 25,
week: 11,
patch: 1,
};
let v4 = Version {
year: 26,
week: 1,
patch: 1,
};
assert!(v1 < v2);
assert!(v2 < v3);
assert!(v3 < v4);
}
#[test]
fn test_today() {
let today = Version::today();
assert!(today.week >= 1 && today.week <= 53);
assert_eq!(today.patch, 0);
}
#[test]
fn test_increment_patch() {
let mut v = Version {
year: 25,
week: 10,
patch: 1,
};
v.increment_patch();
assert_eq!(v.patch, 2);
}
#[test]
fn test_next_week() {
let mut v = Version {
year: 25,
week: 10,
patch: 5,
};
v.next_week().unwrap();
assert_eq!(v.week, 11);
assert_eq!(v.patch, 0);
}
#[test]
fn test_next_week_year_rollover() {
let mut v = Version {
year: 25,
week: 52,
patch: 1,
};
v.next_week().unwrap();
assert!((v.year == 25 && v.week == 53) || (v.year == 26 && v.week == 1));
assert_eq!(v.patch, 0);
}
#[test]
fn test_full_year() {
assert_eq!(Version::full_year(25), 2025);
assert_eq!(Version::full_year(-5), 1995);
}
#[test]
fn test_short_year() {
assert_eq!(Version::short_year(2025), 25);
assert_eq!(Version::short_year(1995), -5);
}
#[test]
fn test_same_week() {
let v1 = Version {
year: 25,
week: 10,
patch: 1,
};
let v2 = Version {
year: 25,
week: 10,
patch: 5,
};
let v3 = Version {
year: 25,
week: 11,
patch: 1,
};
assert!(v1.same_week(&v2));
assert!(!v1.same_week(&v3));
}
#[test]
fn test_year_2000() {
let version = Version::parse("0.1.0").unwrap();
assert_eq!(version.year, 0);
assert_eq!(Version::full_year(version.year), 2000);
let created = Version::new(0, 1, 0).unwrap();
assert_eq!(created.to_string(), "0.1.0");
let v1 = Version::parse("0.52.0").unwrap();
let v2 = Version::parse("1.1.0").unwrap();
assert!(v1 < v2);
}
}