///|
pub fn DateTime::DateTime(
year : Int,
month : Int,
day : Int,
hour? : Int = 0,
minute? : Int = 0,
second? : Int = 0,
is_date? : Bool = false,
is_utc? : Bool = false,
) -> DateTime {
DateTime::{ year, month, day, hour, minute, second, is_date, is_utc }
}
///|
pub fn is_leap_year(year : Int) -> Bool {
year % 400 == 0 || (year % 4 == 0 && year % 100 != 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 is_valid_date(year : Int, month : Int, day : Int) -> Bool {
year >= 1 &&
month >= 1 &&
month <= 12 &&
day >= 1 &&
day <= days_in_month(year, month)
}
///|
pub fn DateTime::is_valid(self : DateTime) -> Bool {
is_valid_date(self.year, self.month, self.day) &&
self.hour >= 0 &&
self.hour <= 23 &&
self.minute >= 0 &&
self.minute <= 59 &&
self.second >= 0 &&
self.second <= 60
}
///|
pub fn DateTime::date_key(self : DateTime) -> Int {
date_number(self.year, self.month, self.day)
}
///|
///|
pub fn DateTime::compare(self : DateTime, other : DateTime) -> Int {
let date_cmp = self.date_key().compare(other.date_key())
if date_cmp != 0 {
return date_cmp
}
let self_seconds = self.hour * 3600 + self.minute * 60 + self.second
let other_seconds = other.hour * 3600 + other.minute * 60 + other.second
self_seconds.compare(other_seconds)
}
///|
pub fn DateTime::before(self : DateTime, other : DateTime) -> Bool {
self.compare(other) < 0
}
///|
pub fn DateTime::after(self : DateTime, other : DateTime) -> Bool {
self.compare(other) > 0
}
///|
pub fn DateTime::same_day(self : DateTime, other : DateTime) -> Bool {
self.year == other.year && self.month == other.month && self.day == other.day
}
///|
pub fn DateTime::add_days(self : DateTime, days : Int) -> DateTime {
let (year, month, day) = civil_from_date_number(self.date_key() + days)
DateTime::{
year,
month,
day,
hour: self.hour,
minute: self.minute,
second: self.second,
is_date: self.is_date,
is_utc: self.is_utc,
}
}
///|
pub fn DateTime::add_seconds(self : DateTime, seconds : Int) -> DateTime {
let total = self.hour * 3600 + self.minute * 60 + self.second + seconds
let mut day_delta = total / 86400
let mut second_of_day = total % 86400
if second_of_day < 0 {
second_of_day = second_of_day + 86400
day_delta = day_delta - 1
}
let shifted = self.add_days(day_delta)
DateTime::{
year: shifted.year,
month: shifted.month,
day: shifted.day,
hour: second_of_day / 3600,
minute: second_of_day % 3600 / 60,
second: second_of_day % 60,
is_date: self.is_date,
is_utc: self.is_utc,
}
}
///|
pub fn DateTime::seconds_until(self : DateTime, other : DateTime) -> Int {
let day_part = self.days_until(other) * 86400
let self_time = self.hour * 3600 + self.minute * 60 + self.second
let other_time = other.hour * 3600 + other.minute * 60 + other.second
day_part + other_time - self_time
}
///|
pub fn DateTime::add_months(self : DateTime, months : Int) -> DateTime {
let zero_based = self.year * 12 + (self.month - 1) + months
let new_year = zero_based / 12
let new_month = zero_based % 12 + 1
let max_day = days_in_month(new_year, new_month)
let new_day = if self.day > max_day { max_day } else { self.day }
DateTime::{
year: new_year,
month: new_month,
day: new_day,
hour: self.hour,
minute: self.minute,
second: self.second,
is_date: self.is_date,
is_utc: self.is_utc,
}
}
///|
pub fn DateTime::add_years(self : DateTime, years : Int) -> DateTime {
let new_year = self.year + years
let max_day = days_in_month(new_year, self.month)
let new_day = if self.day > max_day { max_day } else { self.day }
DateTime::{
year: new_year,
month: self.month,
day: new_day,
hour: self.hour,
minute: self.minute,
second: self.second,
is_date: self.is_date,
is_utc: self.is_utc,
}
}
///|
pub fn DateTime::days_until(self : DateTime, other : DateTime) -> Int {
other.date_key() - self.date_key()
}
///|
pub fn DateTime::months_until(self : DateTime, other : DateTime) -> Int {
(other.year - self.year) * 12 + (other.month - self.month)
}
///|
pub fn DateTime::years_until(self : DateTime, other : DateTime) -> Int {
other.year - self.year
}
///|
pub fn DateTime::weekday(self : DateTime) -> Weekday {
match positive_mod(self.date_key() - 1, 7) {
0 => MO
1 => TU
2 => WE
3 => TH
4 => FR
5 => SA
_ => SU
}
}
///|
pub fn DateTime::format(self : DateTime) -> String {
let date = "\{pad4(self.year)}-\{pad2(self.month)}-\{pad2(self.day)}"
if self.is_date {
date
} else {
let suffix = if self.is_utc { "Z" } else { "" }
date +
"T\{pad2(self.hour)}:\{pad2(self.minute)}:\{pad2(self.second)}\{suffix}"
}
}
///|
pub fn DateTime::to_ics_value(self : DateTime) -> String {
let date = "\{pad4(self.year)}\{pad2(self.month)}\{pad2(self.day)}"
if self.is_date {
date
} else {
let suffix = if self.is_utc { "Z" } else { "" }
date +
"T\{pad2(self.hour)}\{pad2(self.minute)}\{pad2(self.second)}\{suffix}"
}
}
///|
pub impl Show for DateTime with fn to_string(self) {
self.format()
}
///|
fn date_number(year : Int, month : Int, day : Int) -> Int {
let mut total = (year - 1) * 365
total = total + (year - 1) / 4
total = total - (year - 1) / 100
total = total + (year - 1) / 400
for m in 1.. (Int, Int, Int) {
let mut year = number / 366 + 1
while date_number(year + 1, 1, 1) <= number {
year = year + 1
}
while date_number(year, 1, 1) > number {
year = year - 1
}
let mut month = 1
while month < 12 && date_number(year, month + 1, 1) <= number {
month = month + 1
}
let day = number - date_number(year, month, 1) + 1
(year, month, day)
}
///|
fn positive_mod(value : Int, divisor : Int) -> Int {
let raw = value % divisor
if raw < 0 {
raw + divisor
} else {
raw
}
}
///|
fn pad2(n : Int) -> String {
if n < 10 {
"0\{n}"
} else {
"\{n}"
}
}
///|
fn pad4(n : Int) -> String {
if n < 10 {
"000\{n}"
} else if n < 100 {
"00\{n}"
} else if n < 1000 {
"0\{n}"
} else {
"\{n}"
}
}
///|
fn parse_digits(input : String, start : Int, len : Int) -> Int? {
if start < 0 || len <= 0 || start + len > input.length() {
return None
}
let mut value = 0
for i in start..<(start + len) {
match input.get_char(i) {
Some(c) if c >= '0' && c <= '9' =>
value = value * 10 + (c.to_int() - '0'.to_int())
_ => return None
}
}
Some(value)
}
///|
pub fn parse_datetime(
value : String,
is_date_hint? : Bool = false,
) -> Result[DateTime, MoonCalError] {
let raw = value.trim().to_owned()
if raw.length() == 8 || is_date_hint {
match
(
parse_digits(raw, 0, 4),
parse_digits(raw, 4, 2),
parse_digits(raw, 6, 2),
) {
(Some(year), Some(month), Some(day)) if raw.length() == 8 &&
is_valid_date(year, month, day) =>
Ok(DateTime(year, month, day, is_date=true))
_ => Err(InvalidDateTime(value=raw))
}
} else {
let has_utc = raw.has_suffix("Z")
let core = if has_utc { raw[:raw.length() - 1].to_owned() } else { raw }
if core.length() != 15 || !core[8:9].to_owned().equal_ignore_ascii_case("T") {
return Err(InvalidDateTime(value~))
}
match
(
parse_digits(core, 0, 4),
parse_digits(core, 4, 2),
parse_digits(core, 6, 2),
parse_digits(core, 9, 2),
parse_digits(core, 11, 2),
parse_digits(core, 13, 2),
) {
(
Some(year),
Some(month),
Some(day),
Some(hour),
Some(minute),
Some(second),
) if is_valid_date(year, month, day) &&
hour >= 0 &&
hour <= 23 &&
minute >= 0 &&
minute <= 59 &&
second >= 0 &&
second <= 60 =>
Ok(
DateTime(
year,
month,
day,
hour~,
minute~,
second~,
is_date=false,
is_utc=has_utc,
),
)
_ => Err(InvalidDateTime(value~))
}
}
}