use crate::types::{DiagCode, Span};
use super::LoweringContext;
pub(super) fn check_module_identity_dates(
ctx: &mut LoweringContext,
last_updated: &str,
last_updated_span: Span,
revision_dates: &[(String, Span)],
) {
let now = now_utc();
let last_updated_time = check_date(ctx, last_updated, last_updated_span, now);
let revs: Vec<(Option<DateComponents>, Span)> = revision_dates
.iter()
.map(|(date, span)| (check_date(ctx, date, *span, now), *span))
.collect();
for i in 1..revs.len() {
if let (Some(prev), Some(curr)) = (&revs[i - 1].0, &revs[i].0)
&& curr >= prev
{
ctx.emit_diagnostic(
DiagCode::RevisionNotDescending,
revs[i].1,
format!(
"revision {} is not in reverse chronological order",
revision_dates[i].0
),
);
}
}
if let Some(last_updated_time) = &last_updated_time {
for (i, rev) in revs.iter().enumerate() {
if let Some(t) = &rev.0
&& t > last_updated_time
{
ctx.emit_diagnostic(
DiagCode::RevisionAfterUpdate,
rev.1,
format!(
"revision {} is after LAST-UPDATED {}",
revision_dates[i].0, last_updated
),
);
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct DateComponents {
year: u32,
month: u32,
day: u32,
hour: u32,
min: u32,
}
fn check_date(
ctx: &mut LoweringContext,
date: &str,
span: Span,
now: DateComponents,
) -> Option<DateComponents> {
if date.is_empty() {
return None;
}
let bytes = date.as_bytes();
if bytes.len() != 11 && bytes.len() != 13 {
ctx.emit_diagnostic(
DiagCode::DateLength,
span,
format!(
"date {:?} has illegal length {} (expected 11 or 13)",
date,
bytes.len()
),
);
return None;
}
for (i, &b) in bytes[..bytes.len() - 1].iter().enumerate() {
if !b.is_ascii_digit() {
ctx.emit_diagnostic(
DiagCode::DateCharacter,
span,
format!(
"date {:?} contains illegal character at position {}",
date,
i + 1
),
);
return None;
}
}
if bytes[bytes.len() - 1] != b'Z' {
ctx.emit_diagnostic(
DiagCode::DateCharacter,
span,
format!("date {:?} must end with 'Z'", date),
);
return None;
}
let digit = |i: usize| -> u32 { (bytes[i] - b'0') as u32 };
let (year, offset) = if bytes.len() == 11 {
let y = digit(0) * 10 + digit(1) + 1900;
ctx.emit_diagnostic(
DiagCode::DateYear2Digits,
span,
format!("date {:?} uses 2-digit year representing {}", date, y),
);
(y, 2)
} else {
(
digit(0) * 1000 + digit(1) * 100 + digit(2) * 10 + digit(3),
4,
)
};
let month = digit(offset) * 10 + digit(offset + 1);
let day = digit(offset + 2) * 10 + digit(offset + 3);
let hour = digit(offset + 4) * 10 + digit(offset + 5);
let min = digit(offset + 6) * 10 + digit(offset + 7);
if !(1..=12).contains(&month) {
ctx.emit_diagnostic(
DiagCode::DateMonth,
span,
format!("date {:?} has illegal month {:02}", date, month),
);
return None;
}
if !(1..=31).contains(&day) {
ctx.emit_diagnostic(
DiagCode::DateDay,
span,
format!("date {:?} has illegal day {:02}", date, day),
);
return None;
}
if hour > 23 {
ctx.emit_diagnostic(
DiagCode::DateHour,
span,
format!("date {:?} has illegal hour {:02}", date, hour),
);
return None;
}
if min > 59 {
ctx.emit_diagnostic(
DiagCode::DateMinutes,
span,
format!("date {:?} has illegal minutes {:02}", date, min),
);
return None;
}
if !is_valid_date(year, month, day) {
ctx.emit_diagnostic(
DiagCode::DateValue,
span,
format!("date {:?} is not a valid calendar date", date),
);
return None;
}
let dc = DateComponents {
year,
month,
day,
hour,
min,
};
let smi_epoch = DateComponents {
year: 1990,
month: 1,
day: 1,
hour: 0,
min: 0,
};
if dc < smi_epoch {
ctx.emit_diagnostic(
DiagCode::DateInPast,
span,
format!("date {:?} predates the SMI standard", date),
);
}
if dc > now {
ctx.emit_diagnostic(
DiagCode::DateInFuture,
span,
format!("date {:?} is in the future", date),
);
}
Some(dc)
}
fn is_leap_year(year: u32) -> bool {
(year.is_multiple_of(4) && !year.is_multiple_of(100)) || year.is_multiple_of(400)
}
fn days_in_month(year: u32, month: u32) -> u32 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => {
if is_leap_year(year) {
29
} else {
28
}
}
_ => 0,
}
}
fn is_valid_date(year: u32, month: u32, day: u32) -> bool {
day <= days_in_month(year, month)
}
fn now_utc() -> DateComponents {
use std::time::{SystemTime, UNIX_EPOCH};
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let days = secs / 86400;
let time_of_day = secs % 86400;
let hour = (time_of_day / 3600) as u32;
let min = ((time_of_day % 3600) / 60) as u32;
let z = days as i64 + 719468;
let era = if z >= 0 { z } else { z - 146096 } / 146097;
let doe = (z - era * 146097) as u32;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let year = if m <= 2 { y + 1 } else { y };
DateComponents {
year: year as u32,
month: m,
day: d,
hour,
min,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_dates() {
assert!(is_valid_date(2024, 2, 29)); assert!(!is_valid_date(2023, 2, 29)); assert!(is_valid_date(2023, 12, 31));
assert!(!is_valid_date(2023, 4, 31));
}
#[test]
fn date_comparison() {
let d1 = DateComponents {
year: 2020,
month: 1,
day: 1,
hour: 0,
min: 0,
};
let d2 = DateComponents {
year: 2021,
month: 1,
day: 1,
hour: 0,
min: 0,
};
assert!(d1 < d2);
assert!(d2 > d1);
assert!(d1 <= d2);
}
}