///|
pub suberror ScheduleError {
InvalidDate(String)
InvalidRule(String)
}
///|
pub struct DateTime {
year : Int
month : Int
day : Int
hour : Int
minute : Int
} derive(Eq, Compare, Debug)
///|
pub fn is_leap_year(year : Int) -> Bool {
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}
///|
pub fn days_in_month(year : Int, month : Int) -> Int {
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
}
}
///|
pub fn DateTime::new(
year : Int,
month : Int,
day : Int,
hour : Int,
minute : Int,
) -> DateTime raise ScheduleError {
if month < 1 || month > 12 {
raise InvalidDate("month must be 1..12")
}
if day < 1 || day > days_in_month(year, month) {
raise InvalidDate("day is outside this month")
}
if hour < 0 || hour > 23 || minute < 0 || minute > 59 {
raise InvalidDate("time must be within 00:00..23:59")
}
{ year, month, day, hour, minute }
}
///|
pub fn DateTime::to_string(self : DateTime) -> String {
"\{pad4(self.year)}-\{pad2(self.month)}-\{pad2(self.day)}T\{pad2(self.hour)}:\{pad2(self.minute)}Z"
}
///|
pub fn DateTime::parse(input : String) -> DateTime raise ScheduleError {
let text = input.trim().to_owned()
if text.length() != 17 ||
text[4] != '-' ||
text[7] != '-' ||
text[10] != 'T' ||
text[13] != ':' ||
text[16] != 'Z' {
raise InvalidDate("expected RFC 3339 minute timestamp YYYY-MM-DDTHH:MMZ")
}
DateTime::new(
parse_number(text[0:4], "year"),
parse_number(text[5:7], "month"),
parse_number(text[8:10], "day"),
parse_number(text[11:13], "hour"),
parse_number(text[14:16], "minute"),
)
}
///|
pub fn DateTime::add_minutes(self : DateTime, amount : Int) -> DateTime {
if amount == 0 {
self
} else if amount > 0 {
let next_minute = self.minute + 1
if next_minute < 60 {
{ ..self, minute: next_minute }.add_minutes(amount - 1)
} else if self.hour < 23 {
{ ..self, hour: self.hour + 1, minute: 0 }.add_minutes(amount - 1)
} else {
self.next_day().add_minutes(amount - 1)
}
} else {
let previous_minute = self.minute - 1
if previous_minute >= 0 {
{ ..self, minute: previous_minute }.add_minutes(amount + 1)
} else if self.hour > 0 {
{ ..self, hour: self.hour - 1, minute: 59 }.add_minutes(amount + 1)
} else {
self.previous_day().add_minutes(amount + 1)
}
}
}
///|
pub fn DateTime::next_day(self : DateTime) -> DateTime {
if self.day < days_in_month(self.year, self.month) {
{ ..self, day: self.day + 1, hour: 0, minute: 0 }
} else if self.month < 12 {
{ year: self.year, month: self.month + 1, day: 1, hour: 0, minute: 0 }
} else {
{ year: self.year + 1, month: 1, day: 1, hour: 0, minute: 0 }
}
}
///|
fn DateTime::previous_day(self : DateTime) -> DateTime {
if self.day > 1 {
{ ..self, day: self.day - 1, hour: 23, minute: 59 }
} else if self.month > 1 {
let month = self.month - 1
{
year: self.year,
month,
day: days_in_month(self.year, month),
hour: 23,
minute: 59,
}
} else {
{ year: self.year - 1, month: 12, day: 31, hour: 23, minute: 59 }
}
}
///|
pub fn DateTime::weekday(self : DateTime) -> Int {
let mut y = self.year
let mut m = self.month
if m < 3 {
y = y - 1
m = m + 12
}
(self.day + 13 * (m + 1) / 5 + y + y / 4 - y / 100 + y / 400 + 6) % 7
}
///|
fn parse_number(input : StringView, name : String) -> Int raise ScheduleError {
@string.parse_int(input.to_owned()) catch {
_ => raise InvalidDate("invalid \{name}")
}
}
///|
fn pad2(value : Int) -> String {
if value < 10 {
"0\{value}"
} else {
"\{value}"
}
}
///|
fn pad4(value : Int) -> String {
if value < 10 {
"000\{value}"
} else if value < 100 {
"00\{value}"
} else if value < 1000 {
"0\{value}"
} else {
"\{value}"
}
}