///|
/// Calendar utilities: leap year detection, days-in-month,
/// and date arithmetic for cron calculations.
///|
/// Check if a year is a leap year in the Gregorian calendar.
pub fn is_leap_year(year : Int) -> Bool {
if year % 400 == 0 {
true
} else if year % 100 == 0 {
false
} else {
year % 4 == 0
}
}
///|
/// Return the number of days in a given month (1-indexed).
pub fn days_in_month(year : Int, month : Int) -> Int {
match month {
1 => 31
2 => if is_leap_year(year) { 29 } else { 28 }
3 => 31
4 => 30
5 => 31
6 => 30
7 => 31
8 => 31
9 => 30
10 => 31
11 => 30
12 => 31
_ => 0
}
}
///|
/// Add one minute to a DateTime, handling all overflow cases
/// including cross-day, cross-month, and cross-year.
pub fn add_minute(dt : DateTime) -> DateTime {
let minute = dt.minute + 1
if minute < 60 {
return { ..dt, minute, second: 0 }
}
// Minute overflow: advance hour
let hour = dt.hour + 1
if hour < 24 {
return { ..dt, hour, minute: 0, second: 0 }
}
// Hour overflow: advance day
let day = dt.day + 1
let dim = days_in_month(dt.year, dt.month)
if day <= dim {
return { ..dt, day, hour: 0, minute: 0, second: 0 }
}
// Day overflow: advance month
let month = dt.month + 1
if month <= 12 {
return { ..dt, month, day: 1, hour: 0, minute: 0, second: 0 }
}
// Month overflow: advance year
{ year: dt.year + 1, month: 1, day: 1, hour: 0, minute: 0, second: 0 }
}
///|
/// Add one second to a DateTime, handling all overflow cases.
pub fn add_second(dt : DateTime) -> DateTime {
if dt.second + 1 < 60 {
return { ..dt, second: dt.second + 1 }
}
if dt.minute + 1 < 60 {
return { ..dt, minute: dt.minute + 1, second: 0 }
}
if dt.hour + 1 < 24 {
return { ..dt, hour: dt.hour + 1, minute: 0, second: 0 }
}
let dim = days_in_month(dt.year, dt.month)
if dt.day + 1 <= dim {
return { ..dt, day: dt.day + 1, hour: 0, minute: 0, second: 0 }
}
if dt.month + 1 <= 12 {
return { ..dt, month: dt.month + 1, day: 1, hour: 0, minute: 0, second: 0 }
}
{ year: dt.year + 1, month: 1, day: 1, hour: 0, minute: 0, second: 0 }
}
///|
/// Get the day of week for a given date.
/// Returns 0=Sunday, 1=Monday, ..., 6=Saturday
/// Uses Zeller's congruence.
pub fn day_of_week(year : Int, month : Int, day : Int) -> Int {
let m = if month < 3 { month + 12 } else { month }
let y = if month < 3 { year - 1 } else { year }
let k = y % 100
let j = y / 100
// Zeller's congruence for Gregorian calendar
let h = day + 13 * (m + 1) / 5 + k + k / 4 + j / 4 + 5 * j
let result = h % 7
// Convert Zeller result (0=Saturday) to our format (0=Sunday)
match result {
0 => 6 // Saturday
1 => 0 // Sunday
2 => 1 // Monday
3 => 2 // Tuesday
4 => 3 // Wednesday
5 => 4 // Thursday
6 => 5 // Friday
_ => 0 // should not happen
}
}
///|
/// Validate that a date actually exists in the calendar.
pub fn is_valid_date(year : Int, month : Int, day : Int) -> Bool {
if month < 1 || month > 12 {
return false
}
if day < 1 || day > days_in_month(year, month) {
return false
}
true
}
///|
/// Validate a full DateTime.
pub fn is_valid_datetime(dt : DateTime) -> Bool {
if dt.hour < 0 || dt.hour > 23 {
return false
}
if dt.minute < 0 || dt.minute > 59 {
return false
}
if dt.second < 0 || dt.second > 59 {
return false
}
is_valid_date(dt.year, dt.month, dt.day)
}