///|
/// A date, time or timestamp field that is out of range.
pub(all) suberror DateTimeError {
InvalidDate(String)
InvalidTime(String)
} derive(Debug, Eq)
///|
/// A calendar date (proleptic Gregorian).
pub struct Date {
year : Int32
month : Int32
day : Int32
} derive(Debug)
///|
fn is_leap_year(year : Int32) -> Bool {
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}
///|
fn days_in_month(year : Int32, month : Int32) -> Int32 {
match month {
2 => if is_leap_year(year) { 29 } else { 28 }
4 | 6 | 9 | 11 => 30
_ => 31
}
}
///|
/// Build a Gregorian date with a year in -999999999..999999999.
/// Raise `DateTimeError::InvalidDate` for an invalid year, month or day,
/// including February 29 in a non-leap year.
///
/// ```mbt check
/// test {
/// let leap_day : @value.Date = @value.Date(year=2024, month=2, day=29)
/// @expect.expect(leap_day.day).to_equal(29)
/// }
/// ```
#alias(new, deprecated="Use Date(...) instead")
pub fn Date::Date(
year~ : Int32,
month~ : Int32,
day~ : Int32,
) -> Date raise DateTimeError {
// The range of Java's LocalDate, which partiql-lang-kotlin uses; the
// extended conformance tests have years such as -1 and 19998.
if year < -999999999 || year > 999999999 {
raise InvalidDate("year \{year} is not in -999999999..999999999")
}
if month < 1 || month > 12 {
raise InvalidDate("month \{month} is not in 1..12")
}
if day < 1 || day > days_in_month(year, month) {
raise InvalidDate("day \{day} is not valid for \{year}-\{month}")
}
{ year, month, day, }
}
///|
/// A time of day. `offset` is minutes east of UTC; `None` means WITHOUT TIME
/// ZONE. `precision` is the number of fractional-second digits (TIME(p)).
pub struct Time {
hour : Int32
minute : Int32
second : Int32
nanos : Int32
precision : Int32
offset : Int32?
} derive(Debug)
///|
fn pow10(n : Int32) -> Int32 {
let mut result = 1
for _ in 0.. Time raise DateTimeError {
if hour < 0 || hour > 23 {
raise InvalidTime("hour \{hour} is not in 0..23")
}
if minute < 0 || minute > 59 {
raise InvalidTime("minute \{minute} is not in 0..59")
}
if second < 0 || second > 59 {
raise InvalidTime("second \{second} is not in 0..59")
}
if precision < 0 || precision > 9 {
raise InvalidTime("precision \{precision} is not in 0..9")
}
if nanos < 0 || nanos > 999999999 {
raise InvalidTime("nanos \{nanos} is not in 0..999999999")
}
if nanos % pow10(9 - precision) != 0 {
raise InvalidTime("nanos \{nanos} needs more than \{precision} digits")
}
if offset is Some(minutes) && (minutes < -1439 || minutes > 1439) {
raise InvalidTime("offset \{minutes} is not in -1439..1439")
}
{ hour, minute, second, nanos, precision, offset, }
}
///|
/// A date and a time. The time's offset is the timestamp's offset.
pub struct Timestamp {
date : Date
time : Time
} derive(Debug)
///|
/// Combine an already validated date and time. Preserve the time's precision
/// and optional UTC offset; perform no time zone conversion.
#alias(new, deprecated="Use Timestamp(...) instead")
pub fn Timestamp::Timestamp(date : Date, time : Time) -> Timestamp {
{ date, time, }
}
///|
/// Days since 1970-01-01 (Howard Hinnant's days_from_civil).
fn days_from_civil(year : Int32, month : Int32, day : Int32) -> Int64 {
let y = (if month <= 2 { year - 1 } else { year }).to_int64()
let era = (if y >= 0L { y } else { y - 399L }) / 400L
let yoe = y - era * 400L
let m = month.to_int64()
let doy = (153L * (if m > 2L { m - 3L } else { m + 9L }) + 2L) / 5L +
day.to_int64() -
1L
let doe = yoe * 365L + yoe / 4L - yoe / 100L + doy
era * 146097L + doe - 719468L
}
///|
fn Time::seconds_of_day(self : Time) -> Int64 {
(self.hour * 3600 + self.minute * 60 + self.second).to_int64()
}
///|
fn Time::offset_seconds(self : Time) -> Int64 {
match self.offset {
Some(minutes) => (minutes * 60).to_int64()
None => 0L
}
}
///|
/// Fields as written, ignoring the offset.
fn Time::local_key(self : Time) -> (Int64, Int32) {
(self.seconds_of_day(), self.nanos)
}
///|
/// UTC seconds of day, modulo 86,400.
fn Time::utc_key(self : Time) -> (Int64, Int32) {
let s = (self.seconds_of_day() - self.offset_seconds()) % 86400L
(if s < 0L { s + 86400L } else { s }, self.nanos)
}
///|
fn Timestamp::local_key(self : Timestamp) -> (Int64, Int32) {
let days = days_from_civil(self.date.year, self.date.month, self.date.day)
(days * 86400L + self.time.seconds_of_day(), self.time.nanos)
}
///|
/// Seconds since 1970-01-01T00:00:00Z and nanos.
fn Timestamp::utc_key(self : Timestamp) -> (Int64, Int32) {
let (seconds, nanos) = self.local_key()
(seconds - self.time.offset_seconds(), nanos)
}