///|
/// A proleptic Gregorian calendar date.
pub struct Date {
year : Int
month : Int
day : Int
} derive(Eq, Compare, Debug)
///|
/// Clock time without a time-zone offset.
pub struct Time {
hour : Int
minute : Int
second : Int
} derive(Eq, Compare, Debug)
///|
/// A local date and time. RRuleLab deliberately keeps local time separate
/// from time-zone conversion so recurrence arithmetic remains deterministic.
pub struct DateTime {
date : Date
time : Time
} derive(Eq, Compare, Debug)
///|
/// Days of the week use ISO order (Monday first).
pub enum Weekday {
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
Sunday
} derive(Eq, Compare, Debug)
///|
pub suberror DateTimeError {
InvalidDate(Int, Int, Int)
InvalidTime(Int, Int, Int)
InvalidFormat(String)
} derive(Eq, 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 Date::new(
year : Int,
month : Int,
day : Int,
) -> Date raise DateTimeError {
let limit = days_in_month(year, month)
if year < 1 || limit == 0 || day < 1 || day > limit {
raise InvalidDate(year, month, day)
}
{ year, month, day }
}
///|
pub fn Time::new(
hour : Int,
minute : Int,
second : Int,
) -> Time raise DateTimeError {
if hour < 0 ||
hour > 23 ||
minute < 0 ||
minute > 59 ||
second < 0 ||
second > 60 {
raise InvalidTime(hour, minute, second)
}
{ hour, minute, second }
}
///|
pub fn DateTime::new(
year : Int,
month : Int,
day : Int,
hour : Int,
minute : Int,
second : Int,
) -> DateTime raise DateTimeError {
{ date: Date::new(year, month, day), time: Time::new(hour, minute, second) }
}
///|
fn pad2(value : Int) -> String {
if value < 10 {
"0" + value.to_string()
} else {
value.to_string()
}
}
///|
fn pad4(value : Int) -> String {
if value < 10 {
"000" + value.to_string()
} else if value < 100 {
"00" + value.to_string()
} else if value < 1000 {
"0" + value.to_string()
} else {
value.to_string()
}
}
///|
pub fn Date::to_iso_string(self : Date) -> String {
pad4(self.year) + "-" + pad2(self.month) + "-" + pad2(self.day)
}
///|
pub fn Time::to_iso_string(self : Time) -> String {
pad2(self.hour) + ":" + pad2(self.minute) + ":" + pad2(self.second)
}
///|
pub fn DateTime::to_iso_string(self : DateTime) -> String {
self.date.to_iso_string() + "T" + self.time.to_iso_string()
}
///|
fn parse_decimal(text : String, label : String) -> Int raise DateTimeError {
@string.parse_int(text, base=10) catch {
_ => raise InvalidFormat("invalid " + label + ": " + text)
}
}
///|
pub fn parse_date(text : String) -> Date raise DateTimeError {
if text.length() != 10 ||
text[4:5].to_owned() != "-" ||
text[7:8].to_owned() != "-" {
raise InvalidFormat("expected YYYY-MM-DD: " + text)
}
Date::new(
parse_decimal(text[:4].to_owned(), "year"),
parse_decimal(text[5:7].to_owned(), "month"),
parse_decimal(text[8:10].to_owned(), "day"),
)
}
///|
pub fn parse_basic_date(text : String) -> Date raise DateTimeError {
if text.length() != 8 {
raise InvalidFormat("expected YYYYMMDD: " + text)
}
Date::new(
parse_decimal(text[:4].to_owned(), "year"),
parse_decimal(text[4:6].to_owned(), "month"),
parse_decimal(text[6:8].to_owned(), "day"),
)
}
///|
pub fn parse_time(text : String) -> Time raise DateTimeError {
if text.length() != 8 ||
text[2:3].to_owned() != ":" ||
text[5:6].to_owned() != ":" {
raise InvalidFormat("expected HH:MM:SS: " + text)
}
Time::new(
parse_decimal(text[:2].to_owned(), "hour"),
parse_decimal(text[3:5].to_owned(), "minute"),
parse_decimal(text[6:8].to_owned(), "second"),
)
}
///|
pub fn parse_datetime(text : String) -> DateTime raise DateTimeError {
if text.length() == 15 && text[8:9].to_owned() == "T" {
return DateTime::new(
parse_decimal(text[:4].to_owned(), "year"),
parse_decimal(text[4:6].to_owned(), "month"),
parse_decimal(text[6:8].to_owned(), "day"),
parse_decimal(text[9:11].to_owned(), "hour"),
parse_decimal(text[11:13].to_owned(), "minute"),
parse_decimal(text[13:15].to_owned(), "second"),
)
}
if text.length() == 19 && text[10:11].to_owned() == "T" {
return {
date: parse_date(text[:10].to_owned()),
time: parse_time(text[11:19].to_owned()),
}
}
raise InvalidFormat("expected basic or extended date-time: " + text)
}
///|
/// Howard Hinnant's civil calendar conversion, shifted to Unix epoch days.
pub fn Date::to_epoch_day(self : Date) -> Int {
let adjusted_year = if self.month <= 2 { self.year - 1 } else { self.year }
let era = if adjusted_year >= 0 {
adjusted_year / 400
} else {
(adjusted_year - 399) / 400
}
let year_of_era = adjusted_year - era * 400
let shifted_month = if self.month > 2 {
self.month - 3
} else {
self.month + 9
}
let day_of_year = (153 * shifted_month + 2) / 5 + self.day - 1
let day_of_era = year_of_era * 365 +
year_of_era / 4 -
year_of_era / 100 +
day_of_year
era * 146097 + day_of_era - 719468
}
///|
pub fn date_from_epoch_day(epoch_day : Int) -> Date {
let serial = epoch_day + 719468
let era = if serial >= 0 {
serial / 146097
} else {
(serial - 146096) / 146097
}
let day_of_era = serial - era * 146097
let year_of_era = (
day_of_era - day_of_era / 1460 + day_of_era / 36524 - day_of_era / 146096
) /
365
let provisional_year = year_of_era + era * 400
let day_of_year = day_of_era -
(365 * year_of_era + year_of_era / 4 - year_of_era / 100)
let month_prime = (5 * day_of_year + 2) / 153
let day = day_of_year - (153 * month_prime + 2) / 5 + 1
let month = if month_prime < 10 { month_prime + 3 } else { month_prime - 9 }
let year = provisional_year + (if month <= 2 { 1 } else { 0 })
{ year, month, day }
}
///|
pub fn Date::add_days(self : Date, amount : Int) -> Date {
date_from_epoch_day(self.to_epoch_day() + amount)
}
///|
pub fn Date::add_months(self : Date, amount : Int) -> Date {
let month_index = self.year * 12 + self.month - 1 + amount
let year = if month_index >= 0 {
month_index / 12
} else {
(month_index - 11) / 12
}
let month = month_index - year * 12 + 1
let day = self.day.min(days_in_month(year, month))
{ year, month, day }
}
///|
pub fn Date::add_years(self : Date, amount : Int) -> Date {
let year = self.year + amount
let day = self.day.min(days_in_month(year, self.month))
{ year, month: self.month, day }
}
///|
pub fn Date::day_of_year(self : Date) -> Int {
let mut total = self.day
for month = 1; month < self.month; month = month + 1 {
total = total + days_in_month(self.year, month)
}
total
}
///|
pub fn Date::weekday(self : Date) -> Weekday {
let index = ((self.to_epoch_day() + 3) % 7 + 7) % 7
match index {
0 => Monday
1 => Tuesday
2 => Wednesday
3 => Thursday
4 => Friday
5 => Saturday
_ => Sunday
}
}
///|
pub fn Weekday::iso_number(self : Weekday) -> Int {
match self {
Monday => 1
Tuesday => 2
Wednesday => 3
Thursday => 4
Friday => 5
Saturday => 6
Sunday => 7
}
}
///|
pub fn Weekday::code(self : Weekday) -> String {
match self {
Monday => "MO"
Tuesday => "TU"
Wednesday => "WE"
Thursday => "TH"
Friday => "FR"
Saturday => "SA"
Sunday => "SU"
}
}
///|
pub fn weekday_from_code(code : String) -> Weekday? {
match code.to_upper() {
"MO" => Some(Monday)
"TU" => Some(Tuesday)
"WE" => Some(Wednesday)
"TH" => Some(Thursday)
"FR" => Some(Friday)
"SA" => Some(Saturday)
"SU" => Some(Sunday)
_ => None
}
}
///|
pub fn DateTime::to_epoch_second(self : DateTime) -> Int64 {
self.date.to_epoch_day().to_int64() * 86400L +
self.time.hour.to_int64() * 3600L +
self.time.minute.to_int64() * 60L +
self.time.second.to_int64()
}
///|
pub fn DateTime::add_seconds(self : DateTime, amount : Int64) -> DateTime {
let total = self.to_epoch_second() + amount
let day = if total >= 0L { total / 86400L } else { (total - 86399L) / 86400L }
let seconds = total - day * 86400L
{
date: date_from_epoch_day(day.to_int()),
time: {
hour: (seconds / 3600L).to_int(),
minute: (seconds % 3600L / 60L).to_int(),
second: (seconds % 60L).to_int(),
},
}
}
///|
pub fn DateTime::add_days(self : DateTime, amount : Int) -> DateTime {
{ date: self.date.add_days(amount), time: self.time }
}
///|
pub fn DateTime::add_months(self : DateTime, amount : Int) -> DateTime {
{ date: self.date.add_months(amount), time: self.time }
}
///|
pub fn DateTime::add_years(self : DateTime, amount : Int) -> DateTime {
{ date: self.date.add_years(amount), time: self.time }
}