use std::fmt;
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct PackedVersion(u32);
#[derive(Debug, Error, PartialEq)]
pub enum VersionError {
#[error("Major version {0} exceeds maximum of 4294")]
MajorTooLarge(u32),
#[error("Minor version {0} exceeds maximum of 999")]
MinorTooLarge(u32),
#[error("Patch version {0} exceeds maximum of 999")]
PatchTooLarge(u32),
#[error("Invalid version string format: {0}")]
InvalidFormat(String),
#[error("Failed to parse version component: {0}")]
ParseError(String),
}
impl PackedVersion {
pub const MAX_MAJOR: u32 = 4293;
pub const MAX_MINOR: u32 = 999;
pub const MAX_PATCH: u32 = 999;
pub fn new(major: u32, minor: u32, patch: u32) -> Result<Self, VersionError> {
if major > Self::MAX_MAJOR {
return Err(VersionError::MajorTooLarge(major));
}
if minor > Self::MAX_MINOR {
return Err(VersionError::MinorTooLarge(minor));
}
if patch > Self::MAX_PATCH {
return Err(VersionError::PatchTooLarge(patch));
}
let major_part = major
.checked_mul(1_000_000)
.ok_or(VersionError::MajorTooLarge(major))?;
let minor_part = minor
.checked_mul(1_000)
.ok_or(VersionError::MinorTooLarge(minor))?;
let value = major_part
.checked_add(minor_part)
.and_then(|v| v.checked_add(patch))
.ok_or(VersionError::MajorTooLarge(major))?;
Ok(Self(value))
}
pub const fn from_raw(value: u32) -> Self {
Self(value)
}
pub fn parse(s: &str) -> Result<Self, VersionError> {
let parts: Vec<&str> = s.split('.').collect();
if parts.len() != 3 {
return Err(VersionError::InvalidFormat(format!(
"Expected format 'major.minor.patch', got '{}'",
s
)));
}
let major = parts[0]
.parse::<u32>()
.map_err(|e| VersionError::ParseError(format!("major: {}", e)))?;
let minor = parts[1]
.parse::<u32>()
.map_err(|e| VersionError::ParseError(format!("minor: {}", e)))?;
let patch = parts[2]
.parse::<u32>()
.map_err(|e| VersionError::ParseError(format!("patch: {}", e)))?;
Self::new(major, minor, patch)
}
pub const fn as_u32(&self) -> u32 {
self.0
}
pub const fn major(&self) -> u32 {
self.0 / 1_000_000
}
pub const fn minor(&self) -> u32 {
(self.0 / 1_000) % 1_000
}
pub const fn patch(&self) -> u32 {
self.0 % 1_000
}
pub fn is_compatible_with(&self, other: &Self) -> bool {
self.major() == other.major()
}
pub fn is_breaking_change_from(&self, other: &Self) -> bool {
self.major() > other.major()
}
}
pub fn pack_version(major: u32, minor: u32, patch: u32) -> Result<PackedVersion, VersionError> {
PackedVersion::new(major, minor, patch)
}
pub const fn pack_version_unchecked(major: u32, minor: u32, patch: u32) -> PackedVersion {
PackedVersion(major * 1_000_000 + minor * 1_000 + patch)
}
impl fmt::Display for PackedVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}.{}", self.major(), self.minor(), self.patch())
}
}
impl From<PackedVersion> for u32 {
fn from(v: PackedVersion) -> u32 {
v.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pack_version() {
let v = pack_version(2, 3, 1).unwrap();
assert_eq!(v.as_u32(), 2_003_001);
assert_eq!(v.major(), 2);
assert_eq!(v.minor(), 3);
assert_eq!(v.patch(), 1);
}
#[test]
fn test_version_comparison() {
assert!(pack_version(2, 3, 1).unwrap() < pack_version(2, 4, 0).unwrap());
assert!(pack_version(2, 3, 99).unwrap() < pack_version(2, 4, 0).unwrap());
assert!(pack_version(2, 10, 0).unwrap() > pack_version(2, 9, 999).unwrap());
assert!(pack_version(3, 0, 0).unwrap() > pack_version(2, 999, 999).unwrap());
}
#[test]
fn test_parse_version() {
let v = PackedVersion::parse("2.3.1").unwrap();
assert_eq!(v.as_u32(), 2_003_001);
let v = PackedVersion::parse("10.999.999").unwrap();
assert_eq!(v.major(), 10);
assert_eq!(v.minor(), 999);
assert_eq!(v.patch(), 999);
}
#[test]
fn test_parse_errors() {
assert!(PackedVersion::parse("1.2").is_err());
assert!(PackedVersion::parse("1.2.3.4").is_err());
assert!(PackedVersion::parse("a.b.c").is_err());
assert!(PackedVersion::parse("1.1000.1").is_err()); }
#[test]
fn test_limits() {
assert!(pack_version(4293, 999, 999).is_ok());
assert!(pack_version(0, 0, 0).is_ok());
assert_eq!(
pack_version(4294, 0, 0),
Err(VersionError::MajorTooLarge(4294))
);
assert_eq!(
pack_version(0, 1000, 0),
Err(VersionError::MinorTooLarge(1000))
);
assert_eq!(
pack_version(0, 0, 1000),
Err(VersionError::PatchTooLarge(1000))
);
}
#[test]
fn test_display() {
let v = pack_version(2, 3, 1).unwrap();
assert_eq!(v.to_string(), "2.3.1");
}
#[test]
fn test_compatibility() {
let v1 = pack_version(2, 3, 1).unwrap();
let v2 = pack_version(2, 5, 0).unwrap();
let v3 = pack_version(3, 0, 0).unwrap();
assert!(v1.is_compatible_with(&v2));
assert!(!v1.is_compatible_with(&v3));
assert!(!v1.is_breaking_change_from(&v2));
assert!(v3.is_breaking_change_from(&v1));
}
#[test]
fn test_const_packing() {
const VERSION: PackedVersion = pack_version_unchecked(1, 0, 0);
assert_eq!(VERSION.as_u32(), 1_000_000);
}
}