///|
/// Reports invalid civil date and clock values.
pub(all) enum DateTimeError {
InvalidYear(Int)
InvalidMonth(Int)
InvalidDay(Int)
InvalidHour(Int)
InvalidMinute(Int)
InvalidSecond(Int)
} derive(Eq, Debug)
///|
pub fn DateTimeError::message(self : DateTimeError) -> String {
match self {
InvalidYear(value) => "year must be between 1 and 9999, got \{value}"
InvalidMonth(value) => "month must be between 1 and 12, got \{value}"
InvalidDay(value) => "day is outside the selected month, got \{value}"
InvalidHour(value) => "hour must be between 0 and 23, got \{value}"
InvalidMinute(value) => "minute must be between 0 and 59, got \{value}"
InvalidSecond(value) => "second must be between 0 and 59, got \{value}"
}
}
///|
/// A Gregorian calendar date without a time zone.
pub struct CivilDate {
year : Int
month : Int
day : Int
} derive(Eq, Compare, Debug)
///|
pub fn CivilDate::new(
year : Int,
month : Int,
day : Int,
) -> Result[CivilDate, DateTimeError] {
if year < 1 || year > 9999 {
return Err(InvalidYear(year))
}
if month < 1 || month > 12 {
return Err(InvalidMonth(month))
}
if day < 1 || day > days_in_month(year, month) {
return Err(InvalidDay(day))
}
Ok({ year, month, day })
}
///|
pub fn CivilDate::year(self : CivilDate) -> Int {
self.year
}
///|
pub fn CivilDate::month(self : CivilDate) -> Int {
self.month
}
///|
pub fn CivilDate::day(self : CivilDate) -> Int {
self.day
}
///|
pub fn CivilDate::to_iso_string(self : CivilDate) -> String {
"\{pad_year(self.year)}-\{pad_two(self.month)}-\{pad_two(self.day)}"
}
///|
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 {
2 => if is_leap_year(year) { 29 } else { 28 }
4 | 6 | 9 | 11 => 30
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31
_ => 0
}
}
///|
pub(all) enum Weekday {
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
Sunday
} derive(Eq, Compare, Debug)
///|
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 CivilDate::weekday(self : CivilDate) -> Weekday {
let offsets = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4]
let adjusted_year = if self.month < 3 { self.year - 1 } else { self.year }
let sunday_based = (
adjusted_year +
adjusted_year / 4 -
adjusted_year / 100 +
adjusted_year / 400 +
offsets[self.month - 1] +
self.day
) %
7
match sunday_based {
0 => Sunday
1 => Monday
2 => Tuesday
3 => Wednesday
4 => Thursday
5 => Friday
_ => Saturday
}
}
///|
/// A wall-clock time without a date or time zone.
pub struct CivilTime {
hour : Int
minute : Int
second : Int
} derive(Eq, Compare, Debug)
///|
pub fn CivilTime::new(
hour : Int,
minute : Int,
second : Int,
) -> Result[CivilTime, DateTimeError] {
if hour < 0 || hour > 23 {
return Err(InvalidHour(hour))
}
if minute < 0 || minute > 59 {
return Err(InvalidMinute(minute))
}
if second < 0 || second > 59 {
return Err(InvalidSecond(second))
}
Ok({ hour, minute, second })
}
///|
pub fn CivilTime::hour(self : CivilTime) -> Int {
self.hour
}
///|
pub fn CivilTime::minute(self : CivilTime) -> Int {
self.minute
}
///|
pub fn CivilTime::second(self : CivilTime) -> Int {
self.second
}
///|
pub fn CivilTime::to_iso_string(self : CivilTime) -> String {
"\{pad_two(self.hour)}:\{pad_two(self.minute)}:\{pad_two(self.second)}"
}
///|
pub(all) enum DateStyle {
Numeric
Short
Medium
Long
Full
} derive(Eq, Debug)
///|
pub(all) enum HourCycle {
LocaleDefault
Hour12
Hour24
} derive(Eq, Debug)
///|
pub struct DateTimeFormatter {
locale : Locale
date_style : DateStyle
hour_cycle : HourCycle
include_seconds : Bool
} derive(Eq, Debug)
///|
pub fn DateTimeFormatter::new(locale : Locale) -> DateTimeFormatter {
{
locale,
date_style: Medium,
hour_cycle: LocaleDefault,
include_seconds: false,
}
}
///|
pub fn DateTimeFormatter::with_date_style(
self : DateTimeFormatter,
style : DateStyle,
) -> DateTimeFormatter {
{ ..self, date_style: style }
}
///|
pub fn DateTimeFormatter::with_hour_cycle(
self : DateTimeFormatter,
cycle : HourCycle,
) -> DateTimeFormatter {
{ ..self, hour_cycle: cycle }
}
///|
pub fn DateTimeFormatter::with_seconds(
self : DateTimeFormatter,
enabled : Bool,
) -> DateTimeFormatter {
{ ..self, include_seconds: enabled }
}
///|
fn pad_two(value : Int) -> String {
if value < 10 {
"0\{value}"
} else {
value.to_string()
}
}
///|
fn pad_year(value : Int) -> String {
let rendered = value.to_string()
repeat_string("0", 4 - rendered.length()) + rendered
}
///|
fn month_name(locale : Locale, month : Int, short : Bool) -> String {
match locale.language() {
"zh" | "ja" | "ko" => month.to_string()
"fr" =>
if short {
[
"janv.", "f\u{e9}vr.", "mars", "avr.", "mai", "juin", "juil.", "ao\u{fb}t",
"sept.", "oct.", "nov.", "d\u{e9}c.",
][month - 1]
} else {
[
"janvier", "f\u{e9}vrier", "mars", "avril", "mai", "juin", "juillet", "ao\u{fb}t",
"septembre", "octobre", "novembre", "d\u{e9}cembre",
][month - 1]
}
"de" =>
if short {
[
"Jan.", "Feb.", "M\u{e4}rz", "Apr.", "Mai", "Juni", "Juli", "Aug.", "Sept.",
"Okt.", "Nov.", "Dez.",
][month - 1]
} else {
[
"Januar", "Februar", "M\u{e4}rz", "April", "Mai", "Juni", "Juli", "August",
"September", "Oktober", "November", "Dezember",
][month - 1]
}
"es" =>
[
"enero", "febrero", "marzo", "abril", "mayo", "junio", "julio", "agosto",
"septiembre", "octubre", "noviembre", "diciembre",
][month - 1]
_ =>
if short {
[
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov",
"Dec",
][month - 1]
} else {
[
"January", "February", "March", "April", "May", "June", "July", "August",
"September", "October", "November", "December",
][month - 1]
}
}
}
///|
fn weekday_name(locale : Locale, weekday : Weekday, short : Bool) -> String {
let index = weekday.iso_number() - 1
match locale.language() {
"zh" =>
if short {
[
"\u{4e00}", "\u{4e8c}", "\u{4e09}", "\u{56db}", "\u{4e94}", "\u{516d}",
"\u{65e5}",
][index]
} else {
[
"\u{661f}\u{671f}\u{4e00}", "\u{661f}\u{671f}\u{4e8c}", "\u{661f}\u{671f}\u{4e09}",
"\u{661f}\u{671f}\u{56db}", "\u{661f}\u{671f}\u{4e94}", "\u{661f}\u{671f}\u{516d}",
"\u{661f}\u{671f}\u{65e5}",
][index]
}
"fr" =>
if short {
["lun.", "mar.", "mer.", "jeu.", "ven.", "sam.", "dim."][index]
} else {
[
"lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche",
][index]
}
"de" =>
if short {
["Mo.", "Di.", "Mi.", "Do.", "Fr.", "Sa.", "So."][index]
} else {
[
"Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag",
][index]
}
_ =>
if short {
["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"][index]
} else {
[
"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday",
][index]
}
}
}
///|
fn is_month_first(locale : Locale) -> Bool {
if locale.language() != "en" {
return false
}
match locale.region() {
Some("GB") | Some("AU") | Some("NZ") | Some("IE") => false
_ => true
}
}
///|
pub fn DateTimeFormatter::format_date(
self : DateTimeFormatter,
date : CivilDate,
) -> String {
let language = self.locale.language()
match self.date_style {
Numeric =>
if language == "zh" || language == "ja" {
"\{date.year}/\{date.month}/\{date.day}"
} else if is_month_first(self.locale) {
"\{date.month}/\{date.day}/\{date.year}"
} else {
"\{pad_two(date.day)}/\{pad_two(date.month)}/\{date.year}"
}
Short =>
if language == "zh" || language == "ja" {
"\{date.year}-\{pad_two(date.month)}-\{pad_two(date.day)}"
} else if is_month_first(self.locale) {
"\{month_name(self.locale, date.month, true)} \{date.day}, \{date.year}"
} else {
"\{date.day} \{month_name(self.locale, date.month, true)} \{date.year}"
}
Medium | Long =>
if language == "zh" {
"\{date.year}\u{5e74}\{date.month}\u{6708}\{date.day}\u{65e5}"
} else if language == "ja" {
"\{date.year}\u{5e74}\{date.month}\u{6708}\{date.day}\u{65e5}"
} else if is_month_first(self.locale) {
"\{month_name(self.locale, date.month, self.date_style == Medium)} \{date.day}, \{date.year}"
} else {
"\{date.day} \{month_name(self.locale, date.month, self.date_style == Medium)} \{date.year}"
}
Full => {
let core = self.with_date_style(Long).format_date(date)
if language == "zh" {
"\{core}\{weekday_name(self.locale, date.weekday(), false)}"
} else {
"\{weekday_name(self.locale, date.weekday(), false)}, \{core}"
}
}
}
}
///|
fn locale_prefers_hour12(locale : Locale) -> Bool {
if locale.language() != "en" {
return false
}
match locale.region() {
Some("GB") | Some("AU") | Some("NZ") | Some("IE") => false
_ => true
}
}
///|
pub fn DateTimeFormatter::format_time(
self : DateTimeFormatter,
time : CivilTime,
) -> String {
let hour12 = match self.hour_cycle {
Hour12 => true
Hour24 => false
LocaleDefault => locale_prefers_hour12(self.locale)
}
let tail = if self.include_seconds { ":\{pad_two(time.second)}" } else { "" }
if hour12 {
let period = if time.hour < 12 { "AM" } else { "PM" }
let hour = match time.hour {
0 => 12
value if value > 12 => value - 12
value => value
}
"\{hour}:\{pad_two(time.minute)}\{tail} \{period}"
} else {
"\{pad_two(time.hour)}:\{pad_two(time.minute)}\{tail}"
}
}
///|
pub fn DateTimeFormatter::format_datetime(
self : DateTimeFormatter,
date : CivilDate,
time : CivilTime,
) -> String {
self.format_date(date) + ", " + self.format_time(time)
}