use super::parse::{build_date_prefix, build_date_suffix, find_separator, parse_format};
use super::{CalverDate, CalverError};
pub fn next_version(
format: &str,
date: CalverDate,
previous_version: Option<&str>,
) -> Result<String, CalverError> {
let tokens = parse_format(format)?;
let date_prefix = build_date_prefix(&tokens, date);
let date_suffix = build_date_suffix(&tokens, date);
let patch = match previous_version {
Some(prev)
if date_segments_match(prev, &tokens, date) =>
{
extract_patch(prev, &tokens) + 1
}
_ => 0,
};
Ok(format!("{date_prefix}{patch}{date_suffix}"))
}
pub fn validate_format(format: &str) -> Result<(), CalverError> {
parse_format(format)?;
Ok(())
}
fn date_segments_match(previous: &str, tokens: &[super::parse::Token], date: CalverDate) -> bool {
let expected_prefix = build_date_prefix(tokens, date);
let expected_suffix = build_date_suffix(tokens, date);
let prefix_matches = previous.starts_with(&expected_prefix);
let suffix_matches = if expected_suffix.is_empty() {
true
} else {
previous.ends_with(&expected_suffix)
};
prefix_matches && suffix_matches
}
fn extract_patch(previous: &str, tokens: &[super::parse::Token]) -> u64 {
use super::parse::Token;
let mut segments_before_patch = 0;
let mut segments_after_patch = 0;
let mut past_patch = false;
for token in tokens {
match token {
Token::Separator(_) => {}
Token::Patch => {
past_patch = true;
}
_ => {
if past_patch {
segments_after_patch += 1;
} else {
segments_before_patch += 1;
}
}
}
}
let sep = find_separator(tokens);
let parts: Vec<&str> = previous.split(&sep).collect();
if parts.len() > segments_before_patch {
let patch_idx = segments_before_patch;
let _ = segments_after_patch; parts[patch_idx].parse().unwrap_or(0)
} else {
0
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::calver::CalverDate;
fn date_2026_03() -> CalverDate {
CalverDate {
year: 2026,
month: 3,
day: 16,
iso_week: 12,
day_of_week: 1, }
}
fn date_2026_04() -> CalverDate {
CalverDate {
year: 2026,
month: 4,
day: 1,
iso_week: 14,
day_of_week: 3, }
}
#[test]
fn first_release_default_format() {
let v = next_version("YYYY.MM.PATCH", date_2026_03(), None).unwrap();
assert_eq!(v, "2026.3.0");
}
#[test]
fn first_release_zero_padded() {
let v = next_version("YYYY.0M.PATCH", date_2026_03(), None).unwrap();
assert_eq!(v, "2026.03.0");
}
#[test]
fn first_release_daily() {
let v = next_version("YYYY.MM.DD.PATCH", date_2026_03(), None).unwrap();
assert_eq!(v, "2026.3.16.0");
}
#[test]
fn first_release_short_year() {
let v = next_version("YY.MM.PATCH", date_2026_03(), None).unwrap();
assert_eq!(v, "26.3.0");
}
#[test]
fn first_release_weekly() {
let v = next_version("YY.WW.PATCH", date_2026_03(), None).unwrap();
assert_eq!(v, "26.12.0");
}
#[test]
fn patch_increments_same_month() {
let v = next_version("YYYY.MM.PATCH", date_2026_03(), Some("2026.3.0")).unwrap();
assert_eq!(v, "2026.3.1");
}
#[test]
fn patch_increments_twice() {
let v = next_version("YYYY.MM.PATCH", date_2026_03(), Some("2026.3.4")).unwrap();
assert_eq!(v, "2026.3.5");
}
#[test]
fn patch_increments_zero_padded() {
let v = next_version("YYYY.0M.PATCH", date_2026_03(), Some("2026.03.2")).unwrap();
assert_eq!(v, "2026.03.3");
}
#[test]
fn patch_increments_daily() {
let v = next_version("YYYY.MM.DD.PATCH", date_2026_03(), Some("2026.3.16.0")).unwrap();
assert_eq!(v, "2026.3.16.1");
}
#[test]
fn patch_resets_new_month() {
let v = next_version("YYYY.MM.PATCH", date_2026_04(), Some("2026.3.5")).unwrap();
assert_eq!(v, "2026.4.0");
}
#[test]
fn patch_resets_new_year() {
let date = CalverDate {
year: 2027,
month: 1,
day: 1,
iso_week: 53,
day_of_week: 5,
};
let v = next_version("YYYY.MM.PATCH", date, Some("2026.12.3")).unwrap();
assert_eq!(v, "2027.1.0");
}
#[test]
fn patch_resets_new_day() {
let date = CalverDate {
year: 2026,
month: 3,
day: 17,
iso_week: 12,
day_of_week: 2,
};
let v = next_version("YYYY.MM.DD.PATCH", date, Some("2026.3.16.3")).unwrap();
assert_eq!(v, "2026.3.17.0");
}
#[test]
fn patch_resets_new_week() {
let date = CalverDate {
year: 2026,
month: 3,
day: 23,
iso_week: 13,
day_of_week: 1,
};
let v = next_version("YY.WW.PATCH", date, Some("26.12.2")).unwrap();
assert_eq!(v, "26.13.0");
}
#[test]
fn validate_valid_format() {
assert!(validate_format("YYYY.MM.PATCH").is_ok());
assert!(validate_format("YYYY.0M.PATCH").is_ok());
assert!(validate_format("YY.WW.PATCH").is_ok());
assert!(validate_format("YYYY.MM.DD.PATCH").is_ok());
}
#[test]
fn validate_invalid_format() {
assert!(validate_format("YYYY.MM").is_err());
assert!(validate_format("").is_err());
}
#[test]
fn previous_version_is_completely_different() {
let v = next_version("YYYY.MM.PATCH", date_2026_03(), Some("1.2.3")).unwrap();
assert_eq!(v, "2026.3.0");
}
#[test]
fn previous_version_unparseable_patch() {
let v = next_version("YYYY.MM.PATCH", date_2026_03(), Some("2026.3.abc")).unwrap();
assert_eq!(v, "2026.3.1");
}
#[test]
fn dash_separator() {
let v = next_version("YYYY-MM-PATCH", date_2026_03(), None).unwrap();
assert_eq!(v, "2026-3-0");
}
#[test]
fn dash_separator_increment() {
let v = next_version("YYYY-MM-PATCH", date_2026_03(), Some("2026-3-2")).unwrap();
assert_eq!(v, "2026-3-3");
}
#[test]
fn error_display() {
use crate::calver::CalverError;
assert_eq!(
CalverError::NoPatchToken.to_string(),
"calver format must contain the PATCH token"
);
assert_eq!(
CalverError::EmptyFormat.to_string(),
"calver format string is empty"
);
assert!(
CalverError::UnknownToken("X".into())
.to_string()
.contains("unknown calver format token")
);
}
}