use crate::diagnostics::DiagnosticCode;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Weekday {
Sun = 0,
Mon = 1,
Tue = 2,
Wed = 3,
Thu = 4,
Fri = 5,
Sat = 6,
}
impl Weekday {
pub fn from_index(i: u32) -> Weekday {
use Weekday::*;
match i % 7 {
0 => Sun,
1 => Mon,
2 => Tue,
3 => Wed,
4 => Thu,
5 => Fri,
_ => Sat,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OnDay {
Day(u8),
Last(Weekday),
OnAfter(Weekday, u8),
OnBefore(Weekday, u8),
}
pub fn is_leap(year: i32) -> bool {
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}
pub fn days_in_month(year: i32, month: u8) -> u8 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 if is_leap(year) => 29,
2 => 28,
_ => 0, }
}
pub fn days_from_civil(year: i32, month: u8, day: u8) -> i64 {
let y = year as i64 - if month <= 2 { 1 } else { 0 };
let era = (if y >= 0 { y } else { y - 399 }) / 400;
let yoe = y - era * 400; let m = month as i64;
let d = day as i64;
let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d - 1; let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; era * 146097 + doe - 719468
}
pub fn civil_from_days(days: i64) -> (i32, u8, u8) {
let z = days + 719468;
let era = (if z >= 0 { z } else { z - 146096 }) / 146097;
let doe = z - era * 146097; let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; let y = yoe + 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 };
(year as i32, m as u8, d as u8)
}
pub fn year_of_unix(seconds: i64) -> i32 {
civil_from_days(seconds.div_euclid(86400)).0
}
pub fn weekday_of(year: i32, month: u8, day: u8) -> Weekday {
let days = days_from_civil(year, month, day);
let idx = (days + 4).rem_euclid(7); Weekday::from_index(idx as u32)
}
pub fn resolve_on_day(
on: OnDay,
year: i32,
month: u8,
) -> std::result::Result<ResolvedDay, (DiagnosticCode, String)> {
let dim = days_in_month(year, month);
let result = match on {
OnDay::Day(d) => ResolvedDay {
day: d as i32,
month,
year,
},
OnDay::Last(wd) => {
let last = dim;
let last_wd = weekday_of(year, month, last) as i32;
let want = wd as i32;
let back = (last_wd - want).rem_euclid(7);
ResolvedDay {
day: last as i32 - back,
month,
year,
}
}
OnDay::OnAfter(wd, d) => {
let start_wd = weekday_at(year, month, d as i32) as i32;
let want = wd as i32;
let fwd = (want - start_wd).rem_euclid(7);
normalise(year, month, d as i32 + fwd)
}
OnDay::OnBefore(wd, d) => {
let start_wd = weekday_at(year, month, d as i32) as i32;
let want = wd as i32;
let back = (start_wd - want).rem_euclid(7);
normalise(year, month, d as i32 - back)
}
};
Ok(result)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResolvedDay {
pub year: i32,
pub month: u8,
pub day: i32,
}
fn weekday_at(year: i32, month: u8, day: i32) -> Weekday {
let days = days_from_civil(year, month, 1) + (day as i64 - 1);
Weekday::from_index(((days + 4).rem_euclid(7)) as u32)
}
fn normalise(mut year: i32, mut month: u8, mut day: i32) -> ResolvedDay {
loop {
if day < 1 {
month = if month == 1 {
year -= 1;
12
} else {
month - 1
};
day += days_in_month(year, month) as i32;
} else {
let dim = days_in_month(year, month) as i32;
if day > dim {
day -= dim;
month = if month == 12 {
year += 1;
1
} else {
month + 1
};
} else {
return ResolvedDay { year, month, day };
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn leap_years() {
assert!(is_leap(2000));
assert!(!is_leap(1900));
assert!(is_leap(2020));
assert!(!is_leap(2021));
}
#[test]
fn epoch_is_thursday() {
assert_eq!(weekday_of(1970, 1, 1), Weekday::Thu);
assert_eq!(weekday_of(2000, 1, 1), Weekday::Sat);
assert_eq!(weekday_of(2020, 3, 8), Weekday::Sun); }
#[test]
fn days_from_civil_anchors() {
assert_eq!(days_from_civil(1970, 1, 1), 0);
assert_eq!(days_from_civil(1970, 1, 2), 1);
assert_eq!(days_from_civil(1969, 12, 31), -1);
assert_eq!(days_from_civil(2020, 3, 8), 18329);
}
#[test]
fn last_sunday() {
let r = resolve_on_day(OnDay::Last(Weekday::Sun), 2020, 3).unwrap();
assert_eq!((r.month, r.day), (3, 29));
}
#[test]
fn sun_ge_8_and_le_25() {
let a = resolve_on_day(OnDay::OnAfter(Weekday::Sun, 8), 2020, 3).unwrap();
assert_eq!((a.month, a.day), (3, 8));
let b = resolve_on_day(OnDay::OnBefore(Weekday::Sun, 25), 2020, 10).unwrap();
assert_eq!((b.month, b.day), (10, 25));
}
#[test]
fn spill_into_next_month() {
let r = resolve_on_day(OnDay::OnAfter(Weekday::Sun, 29), 2021, 4).unwrap();
assert_eq!((r.month, r.day), (5, 2));
}
}