///|
/// A validated proleptic Gregorian calendar date for numeric formatting.
pub(all) struct CalendarDate {
year : Int32
month : Int32
day : Int32
}
///|
/// Creates a calendar date, rejecting years outside 0..=9999, months outside
/// 1..=12, and days outside the month's length (including leap-year rules).
pub fn CalendarDate::new(
year~ : Int32,
month~ : Int32,
day~ : Int32,
) -> CalendarDate? {
guard year >= 0 && year <= 9999 && month >= 1 && month <= 12 else {
return None
}
guard day >= 1 && day <= days_in_month(year, month) else { return None }
Some({ year, month, day, })
}
///|
/// Formats a date in the locale's numeric field order and separator, with a
/// four-digit year and zero-padded month and day: `11/09/2026` for `en-US`,
/// `09.11.2026` for `de`, `2026/11/09` for `ja`. Unlisted locales use the
/// ISO-style `2026-11-09` order.
pub fn Locale::format_date(self : Locale, date : CalendarDate) -> String {
let pattern = self.date_pattern()
let year = pad_number(date.year, 4)
let month = pad_number(date.month, 2)
let day = pad_number(date.day, 2)
let fields : Array[String] = match pattern.order {
YearFirst => [year, month, day]
MonthFirst => [month, day, year]
DayFirst => [day, month, year]
}
fields.join(pattern.separator) + pattern.suffix
}
///|
priv enum DateOrder {
YearFirst
MonthFirst
DayFirst
}
///|
priv struct DatePattern {
order : DateOrder
separator : String
suffix : String
}
///|
fn Locale::date_pattern(self : Locale) -> DatePattern {
let language = self.language()
let region = self.region().unwrap_or("")
if language == "en" {
return match region {
"GB" | "AU" | "NZ" | "IE" | "IN" | "ZA" =>
{ order: DayFirst, separator: "/", suffix: "", }
"CA" => { order: YearFirst, separator: "-", suffix: "", }
_ => { order: MonthFirst, separator: "/", suffix: "", }
}
}
match language {
"de" | "ru" | "uk" | "pl" | "cs" | "da" | "fi" | "no" | "nb" =>
{ order: DayFirst, separator: ".", suffix: "", }
"fr"
| "es"
| "it"
| "pt"
| "ar"
| "hi"
| "tr"
| "el"
| "th"
| "vi"
| "id" => { order: DayFirst, separator: "/", suffix: "", }
"nl" => { order: DayFirst, separator: "-", suffix: "", }
"ja" | "zh" => { order: YearFirst, separator: "/", suffix: "", }
// CLDR ko/hu numeric patterns are `y. MM. dd.` — the final field keeps
// its period, so the separator alone does not describe the output.
"ko" | "hu" => { order: YearFirst, separator: ". ", suffix: ".", }
_ => { order: YearFirst, separator: "-", suffix: "", }
}
}
///|
fn days_in_month(year : Int32, month : Int32) -> Int32 {
match month {
2 =>
if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) {
29
} else {
28
}
4 | 6 | 9 | 11 => 30
_ => 31
}
}
///|
fn pad_number(value : Int32, width : Int32) -> String {
let text = value.to_string()
let padding = width - text.char_length()
if padding > 0 {
"0".repeat(padding) + text
} else {
text
}
}