use std::cmp::Ordering;
use std::fmt;
use time::OffsetDateTime;
use crate::error::{Error, Result};
const MAX_VERSION: u128 = 10u128.pow(20) - 1;
const MIN_DIGITS: u32 = 14;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Version(u128);
impl Version {
pub fn new(value: u128) -> Result<Self> {
if value > MAX_VERSION {
return Err(Error::encode(
"version",
format!("{value} exceeds Decimal128(20,0) range"),
));
}
Ok(Self(value))
}
pub const fn get(self) -> u128 {
self.0
}
pub fn mscons(value: u128) -> Result<Self> {
let version = Self::new(value)?;
if !version.is_well_formed() {
return Err(Error::encode(
"version",
format!(
"{value} has {} digits; MSCONS assigns at least {MIN_DIGITS}. \
Use Version::new to store a short version that has already been \
received — this constructor is for validating one at ingest",
value.checked_ilog10().map_or(1, |d| d + 1),
),
));
}
Ok(version)
}
pub const fn is_well_formed(self) -> bool {
self.0 >= 10u128.pow(MIN_DIGITS - 1)
}
pub const fn to_i128(self) -> i128 {
self.0 as i128
}
pub fn from_i128(value: i128) -> Result<Self> {
u128::try_from(value)
.map_err(|_| Error::decode("version", format!("negative value {value}")))
.and_then(Self::new)
}
}
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct VersionScope(String);
impl VersionScope {
pub fn new(operator: impl AsRef<str>, year: i32, month: u8) -> Result<Self> {
let operator = operator.as_ref();
if operator.is_empty() {
return Err(Error::config("version scope operator must not be empty"));
}
if operator.contains(':') {
return Err(Error::config(format!(
"version scope operator {operator:?} must not contain ':' (the canonical separator)"
)));
}
if !(1..=12).contains(&month) {
return Err(Error::config(format!("month {month} out of range 1..=12")));
}
Ok(Self(format!("{operator}:{year:04}-{month:02}")))
}
pub fn for_interval(operator: impl AsRef<str>, interval_start: OffsetDateTime) -> Result<Self> {
let month = metering::calendar::local_month(interval_start);
Self::new(operator, month.year(), u8::from(month.month()))
}
pub fn period(&self) -> &str {
let at = self.0.rfind(':').expect("canonical form contains ':'");
&self.0[at + 1..]
}
pub fn operator(&self) -> &str {
let at = self.0.rfind(':').expect("canonical form contains ':'");
&self.0[..at]
}
pub fn covers(&self, interval_start: OffsetDateTime) -> bool {
let month = metering::calendar::local_month(interval_start);
self.period() == format!("{:04}-{:02}", month.year(), u8::from(month.month()))
}
pub fn parse(s: impl Into<String>) -> Result<Self> {
let s = s.into();
let Some((operator, period)) = s.rsplit_once(':') else {
return Err(Error::decode(
"version_scope",
format!("missing ':' in {s:?}"),
));
};
if operator.is_empty() || period.len() != 7 {
return Err(Error::decode(
"version_scope",
format!("malformed scope {s:?}"),
));
}
Ok(Self(s))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for VersionScope {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ScopedVersion {
scope: VersionScope,
version: Version,
}
impl ScopedVersion {
pub const fn new(scope: VersionScope, version: Version) -> Self {
Self { scope, version }
}
pub const fn scope(&self) -> &VersionScope {
&self.scope
}
pub const fn version(&self) -> Version {
self.version
}
pub fn try_cmp(&self, other: &Self) -> Result<Ordering> {
if self.scope != other.scope {
return Err(Error::VersionScopeMismatch {
left: self.scope.0.clone(),
right: other.scope.0.clone(),
});
}
Ok(self.version.0.cmp(&other.version.0))
}
pub fn supersedes(&self, other: &Self) -> Result<bool> {
Ok(self.try_cmp(other)? == Ordering::Greater)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn scope(op: &str, y: i32, m: u8) -> VersionScope {
VersionScope::new(op, y, m).unwrap()
}
#[test]
fn version_round_trips_through_storage_encoding() {
let v = Version::new(20_260_727_000_001).unwrap();
assert_eq!(Version::from_i128(v.to_i128()).unwrap(), v);
}
#[test]
fn version_rejects_values_beyond_decimal128_20_0() {
assert!(Version::new(MAX_VERSION).is_ok());
assert!(Version::new(MAX_VERSION + 1).is_err());
}
#[test]
fn version_rejects_negative_on_decode() {
assert!(Version::from_i128(-1).is_err());
}
#[test]
fn the_strict_constructor_refuses_a_short_version() {
let err = Version::mscons(42).unwrap_err().to_string();
assert!(err.contains("14"), "{err}");
assert!(
err.contains("Version::new"),
"the message must name the permissive path for data already received: {err}"
);
assert!(Version::mscons(20_260_727_000_001).is_ok());
assert!(Version::mscons(u128::MAX).is_err());
}
#[test]
fn the_permissive_constructor_still_accepts_what_arrived() {
assert!(Version::new(42).is_ok());
assert!(!Version::new(42).unwrap().is_well_formed());
}
#[test]
fn well_formedness_tracks_the_14_digit_rule() {
assert!(Version::new(10_000_000_000_000).unwrap().is_well_formed());
assert!(!Version::new(9_999_999_999_999).unwrap().is_well_formed());
}
#[test]
fn short_versions_are_stored_and_still_order() {
let s = scope("9900000000001", 2026, 7);
let a = ScopedVersion::new(s.clone(), Version::new(1).unwrap());
let b = ScopedVersion::new(s, Version::new(2).unwrap());
assert!(b.supersedes(&a).unwrap());
}
#[test]
fn versions_order_within_a_scope() {
let s = scope("9900000000001", 2026, 7);
let older = ScopedVersion::new(s.clone(), Version::new(20_260_701_000_001).unwrap());
let newer = ScopedVersion::new(s, Version::new(20_260_715_000_002).unwrap());
assert_eq!(older.try_cmp(&newer).unwrap(), Ordering::Less);
assert!(newer.supersedes(&older).unwrap());
assert!(!older.supersedes(&newer).unwrap());
}
#[test]
fn versions_do_not_compare_across_operators() {
let a = ScopedVersion::new(scope("9900000000001", 2026, 7), Version::new(5).unwrap());
let b = ScopedVersion::new(scope("9900000000002", 2026, 7), Version::new(9).unwrap());
assert!(matches!(
a.try_cmp(&b),
Err(Error::VersionScopeMismatch { .. })
));
}
#[test]
fn versions_do_not_compare_across_months() {
let a = ScopedVersion::new(scope("9900000000001", 2026, 7), Version::new(5).unwrap());
let b = ScopedVersion::new(scope("9900000000001", 2026, 8), Version::new(9).unwrap());
assert!(a.try_cmp(&b).is_err());
}
#[test]
fn a_scope_derived_from_an_interval_uses_the_local_month() {
use time::macros::datetime;
let july = VersionScope::for_interval("99", datetime!(2026-07-31 20:00 UTC)).unwrap();
let august = VersionScope::for_interval("99", datetime!(2026-07-31 23:00 UTC)).unwrap();
assert_eq!(july.period(), "2026-07");
assert_eq!(august.period(), "2026-08");
}
#[test]
fn every_version_of_one_interval_derives_the_same_scope() {
use time::macros::datetime;
let interval = datetime!(2026-07-20 06:00 UTC);
let original = VersionScope::for_interval("99", interval).unwrap();
let correction = VersionScope::for_interval("99", interval).unwrap();
assert_eq!(original, correction);
}
#[test]
fn covers_accepts_only_intervals_in_the_scope_month() {
use time::macros::datetime;
let july = VersionScope::for_interval("99", datetime!(2026-07-20 00:00 UTC)).unwrap();
assert!(july.covers(datetime!(2026-07-01 00:00 UTC)));
assert!(july.covers(datetime!(2026-07-31 20:00 UTC)));
assert!(!july.covers(datetime!(2026-08-01 00:00 UTC)));
assert!(!july.covers(datetime!(2026-07-31 23:00 UTC)));
}
#[test]
fn operator_and_period_split_the_canonical_form() {
let s = scope("9900000000001", 2026, 3);
assert_eq!(s.operator(), "9900000000001");
assert_eq!(s.period(), "2026-03");
}
#[test]
fn scope_round_trips_through_canonical_form() {
let s = scope("9900000000001", 2026, 3);
assert_eq!(s.as_str(), "9900000000001:2026-03");
assert_eq!(VersionScope::parse(s.as_str()).unwrap(), s);
}
#[test]
fn scope_rejects_separator_in_operator() {
assert!(VersionScope::new("bad:operator", 2026, 7).is_err());
}
#[test]
fn scope_rejects_out_of_range_month() {
assert!(VersionScope::new("9900000000001", 2026, 0).is_err());
assert!(VersionScope::new("9900000000001", 2026, 13).is_err());
}
}