From 4f8fb0d34037ca38d66ed74e472522af47527f63 Mon Sep 17 00:00:00 2001 From: Sofia Rodrigues Date: Wed, 29 Apr 2026 02:33:03 -0300 Subject: [PATCH 1/3] feat: add Locale and LocaleSymbols for configurable date/time formatting --- src/Std/Time/Format/Basic.lean | 417 +++++++++++++------------------- src/Std/Time/Format/Locale.lean | 171 +++++++++++++ 2 files changed, 339 insertions(+), 249 deletions(-) create mode 100644 src/Std/Time/Format/Locale.lean diff --git a/src/Std/Time/Format/Basic.lean b/src/Std/Time/Format/Basic.lean index 4f0f6a113d84..3f0b2be5deec 100644 --- a/src/Std/Time/Format/Basic.lean +++ b/src/Std/Time/Format/Basic.lean @@ -7,6 +7,7 @@ module prelude public import Std.Time.Zoned +public import Std.Time.Format.Locale import Init.Data.String.TakeDrop import Init.Data.String.Search @@ -24,7 +25,6 @@ open Std.Internal.Parsec Lean PlainTime PlainDate TimeZone DateTime set_option linter.all true - /-- `Text` represents different text formatting styles. -/ @@ -584,7 +584,14 @@ structure FormatConfig where -/ allowLeapSeconds : Bool := false -deriving Inhabited, Repr + /-- + Locale-specific symbols used for formatting and parsing text fields such as month names, + weekday names, era designators, and AM/PM markers. + Default is `LocaleSymbols.enUS` (English, US). + -/ + locale : LocaleSymbols := LocaleSymbols.enUS + +deriving Inhabited /-- A specification on how to format a data or parse some string. @@ -599,7 +606,7 @@ structure GenericFormat (awareness : Awareness) where The format string used for parsing and formatting. -/ string : FormatString - deriving Inhabited, Repr + deriving Inhabited private def parseFormatPart : Parser FormatPart := (.modifier <$> parseModifier) @@ -640,119 +647,63 @@ private def rightTruncate (size : Nat) (n : Int) (cut : Bool := false) : String else sign ++ rightPad size '0' numStr -private def formatMonthLong : Month.Ordinal → String - | ⟨1, _⟩ => "January" - | ⟨2, _⟩ => "February" - | ⟨3, _⟩ => "March" - | ⟨4, _⟩ => "April" - | ⟨5, _⟩ => "May" - | ⟨6, _⟩ => "June" - | ⟨7, _⟩ => "July" - | ⟨8, _⟩ => "August" - | ⟨9, _⟩ => "September" - | ⟨10, _⟩ => "October" - | ⟨11, _⟩ => "November" - | ⟨12, _⟩ => "December" - -private def formatMonthShort : Month.Ordinal → String - | ⟨1, _⟩ => "Jan" - | ⟨2, _⟩ => "Feb" - | ⟨3, _⟩ => "Mar" - | ⟨4, _⟩ => "Apr" - | ⟨5, _⟩ => "May" - | ⟨6, _⟩ => "Jun" - | ⟨7, _⟩ => "Jul" - | ⟨8, _⟩ => "Aug" - | ⟨9, _⟩ => "Sep" - | ⟨10, _⟩ => "Oct" - | ⟨11, _⟩ => "Nov" - | ⟨12, _⟩ => "Dec" - -private def formatMonthNarrow : Month.Ordinal → String - | ⟨1, _⟩ => "J" - | ⟨2, _⟩ => "F" - | ⟨3, _⟩ => "M" - | ⟨4, _⟩ => "A" - | ⟨5, _⟩ => "M" - | ⟨6, _⟩ => "J" - | ⟨7, _⟩ => "J" - | ⟨8, _⟩ => "A" - | ⟨9, _⟩ => "S" - | ⟨10, _⟩ => "O" - | ⟨11, _⟩ => "N" - | ⟨12, _⟩ => "D" - -private def formatWeekdayLong : Weekday → String - | .sunday => "Sunday" - | .monday => "Monday" - | .tuesday => "Tuesday" - | .wednesday => "Wednesday" - | .thursday => "Thursday" - | .friday => "Friday" - | .saturday => "Saturday" - -private def formatWeekdayShort : Weekday → String - | .sunday => "Sun" - | .monday => "Mon" - | .tuesday => "Tue" - | .wednesday => "Wed" - | .thursday => "Thu" - | .friday => "Fri" - | .saturday => "Sat" - -private def formatWeekdayNarrow : Weekday → String - | .sunday => "S" - | .monday => "M" - | .tuesday => "T" - | .wednesday => "W" - | .thursday => "T" - | .friday => "F" - | .saturday => "S" - -private def formatEraShort : Year.Era → String - | .bce => "BCE" - | .ce => "CE" - -private def formatEraLong : Year.Era → String - | .bce => "Before Common Era" - | .ce => "Common Era" - -private def formatEraNarrow : Year.Era → String - | .bce => "B" - | .ce => "C" +private def eraIndex : Year.Era → Fin 2 + | .bce => 0 + | .ce => 1 + +private def formatMonthLong (symbols : LocaleSymbols) (month : Month.Ordinal) : String := + symbols.monthLong.get (month.sub 1 |>.toFin (by decide)) + +private def formatMonthShort (symbols : LocaleSymbols) (month : Month.Ordinal) : String := + symbols.monthShort.get (month.sub 1 |>.toFin (by decide)) + +private def formatMonthNarrow (symbols : LocaleSymbols) (month : Month.Ordinal) : String := + symbols.monthNarrow.get (month.sub 1 |>.toFin (by decide)) + +private def formatWeekdayLong (symbols : LocaleSymbols) (wd : Weekday) : String := + symbols.weekdayLong.get (wd.toOrdinal.sub 1 |>.toFin (by decide)) + +private def formatWeekdayShort (symbols : LocaleSymbols) (wd : Weekday) : String := + symbols.weekdayShort.get (wd.toOrdinal.sub 1 |>.toFin (by decide)) + +private def formatWeekdayNarrow (symbols : LocaleSymbols) (wd : Weekday) : String := + symbols.weekdayNarrow.get (wd.toOrdinal.sub 1 |>.toFin (by decide)) + +private def formatEraShort (symbols : LocaleSymbols) (era : Year.Era) : String := + symbols.eraShort.get (eraIndex era) + +private def formatEraLong (symbols : LocaleSymbols) (era : Year.Era) : String := + symbols.eraLong.get (eraIndex era) + +private def formatEraNarrow (symbols : LocaleSymbols) (era : Year.Era) : String := + symbols.eraNarrow.get (eraIndex era) private def formatQuarterNumber : Month.Quarter → String - |⟨1, _⟩ => "1" - |⟨2, _⟩ => "2" - |⟨3, _⟩ => "3" - |⟨4, _⟩ => "4" - -private def formatQuarterShort : Month.Quarter → String - | ⟨1, _⟩ => "Q1" - | ⟨2, _⟩ => "Q2" - | ⟨3, _⟩ => "Q3" - | ⟨4, _⟩ => "Q4" - -private def formatQuarterLong : Month.Quarter → String - | ⟨1, _⟩ => "1st quarter" - | ⟨2, _⟩ => "2nd quarter" - | ⟨3, _⟩ => "3rd quarter" - | ⟨4, _⟩ => "4th quarter" - -private def formatMarkerShort (marker : HourMarker) : String := + | ⟨1, _⟩ => "1" + | ⟨2, _⟩ => "2" + | ⟨3, _⟩ => "3" + | ⟨4, _⟩ => "4" + +private def formatQuarterShort (symbols : LocaleSymbols) (q : Month.Quarter) : String := + symbols.quarterShort.get (q.sub 1 |>.toFin (by decide)) + +private def formatQuarterLong (symbols : LocaleSymbols) (q : Month.Quarter) : String := + symbols.quarterLong.get (q.sub 1 |>.toFin (by decide)) + +private def formatMarkerShort (symbols : LocaleSymbols) (marker : HourMarker) : String := match marker with - | .am => "AM" - | .pm => "PM" + | .am => symbols.amShort + | .pm => symbols.pmShort -private def formatMarkerLong (marker : HourMarker) : String := +private def formatMarkerLong (symbols : LocaleSymbols) (marker : HourMarker) : String := match marker with - | .am => "Ante Meridiem" - | .pm => "Post Meridiem" + | .am => symbols.amLong + | .pm => symbols.pmLong -private def formatMarkerNarrow (marker : HourMarker) : String := +private def formatMarkerNarrow (symbols : LocaleSymbols) (marker : HourMarker) : String := match marker with - | .am => "A" - | .pm => "P" + | .am => symbols.amNarrow + | .pm => symbols.pmNarrow private def toSigned (data : Int) : String := if data < 0 then toString data else "+" ++ toString data @@ -801,13 +752,13 @@ def TypeFormat : Modifier → Type | .x _ => Offset | .Z _ => Offset -private def formatWith (modifier : Modifier) (data: TypeFormat modifier) : String := +private def formatWith (locale : LocaleSymbols) (modifier : Modifier) (data : TypeFormat modifier) : String := match modifier with | .G format => match format with - | .short => formatEraShort data - | .full => formatEraLong data - | .narrow => formatEraNarrow data + | .short => formatEraShort locale data + | .full => formatEraLong locale data + | .narrow => formatEraNarrow locale data | .y format => let info := data.toInt let info := if info ≤ 0 then -info + 1 else info @@ -827,16 +778,16 @@ private def formatWith (modifier : Modifier) (data: TypeFormat modifier) : Strin | .MorL format => match format with | .inl format => pad format.padding data.val - | .inr .short => formatMonthShort data - | .inr .full => formatMonthLong data - | .inr .narrow => formatMonthNarrow data + | .inr .short => formatMonthShort locale data + | .inr .full => formatMonthLong locale data + | .inr .narrow => formatMonthNarrow locale data | .d format => pad format.padding data.val | .Qorq format => match format with | .inl format => pad format.padding data.val - | .inr .short => formatQuarterShort data - | .inr .full => formatQuarterLong data + | .inr .short => formatQuarterShort locale data + | .inr .full => formatQuarterLong locale data | .inr .narrow => formatQuarterNumber data | .w format => pad format.padding data.val @@ -844,22 +795,22 @@ private def formatWith (modifier : Modifier) (data: TypeFormat modifier) : Strin pad format.padding data.val | .E format => match format with - | .short => formatWeekdayShort data - | .full => formatWeekdayLong data - | .narrow => formatWeekdayNarrow data + | .short => formatWeekdayShort locale data + | .full => formatWeekdayLong locale data + | .narrow => formatWeekdayNarrow locale data | .eorc format => match format with | .inl format => pad format.padding data.toOrdinal.val - | .inr .short => formatWeekdayShort data - | .inr .full => formatWeekdayLong data - | .inr .narrow => formatWeekdayNarrow data + | .inr .short => formatWeekdayShort locale data + | .inr .full => formatWeekdayLong locale data + | .inr .narrow => formatWeekdayNarrow locale data | .F format => pad format.padding data.val | .a format => match format with - | .short => formatMarkerShort data - | .full => formatMarkerLong data - | .narrow => formatMarkerNarrow data + | .short => formatMarkerShort locale data + | .full => formatMarkerLong locale data + | .narrow => formatMarkerNarrow locale data | .h format => pad format.padding (data.val % 12) | .K format => pad format.padding (data.val % 12) | .k format => pad format.padding data.val @@ -953,89 +904,63 @@ private def dateFromModifier (date : DateTime tz) : TypeFormat modifier := | .x _ => tz.offset | .Z _ => tz.offset -private def parseMonthLong : Parser Month.Ordinal - := pstring "January" *> pure ⟨1, by decide⟩ - <|> pstring "February" *> pure ⟨2, by decide⟩ - <|> pstring "March" *> pure ⟨3, by decide⟩ - <|> pstring "April" *> pure ⟨4, by decide⟩ - <|> pstring "May" *> pure ⟨5, by decide⟩ - <|> pstring "June" *> pure ⟨6, by decide⟩ - <|> pstring "July" *> pure ⟨7, by decide⟩ - <|> pstring "August" *> pure ⟨8, by decide⟩ - <|> pstring "September" *> pure ⟨9, by decide⟩ - <|> pstring "October" *> pure ⟨10, by decide⟩ - <|> pstring "November" *> pure ⟨11, by decide⟩ - <|> pstring "December" *> pure ⟨12, by decide⟩ +private def parseFromSymbols {α : Type} (pairs : Array (String × α)) : Parser α := + pairs.foldl (fun acc (s, v) => acc <|> pstring s *> pure v) (fail "no match") + +private def monthPairs (arr : Vector String 12) : Array (String × Month.Ordinal) := + arr.mapFinIdx (fun idx val n => (val, Bounded.LE.ofFin ⟨idx, n⟩ |>.add 1)) |>.toArray + +private def weekdayOfIndex : Nat → Weekday + | 0 => .sunday + | 1 => .monday + | 2 => .tuesday + | 3 => .wednesday + | 4 => .thursday + | 5 => .friday + | _ => .saturday + +private def weekdayPairs (arr : Vector String 7) : Array (String × Weekday) := + arr.mapFinIdx (fun idx val n => (val, .ofOrdinal <| Bounded.LE.ofFin ⟨idx, n⟩ |>.add 1)) |>.toArray + +private def eraOfIndex : Nat → Year.Era + | 0 => .bce + | _ => .ce + +private def eraPairs (arr : Vector String 2) : Array (String × Year.Era) := + arr.mapFinIdx (fun idx val _ => (val, eraOfIndex idx)) |>.toArray + +private def quarterPairs (arr : Vector String 4) : Array (String × Month.Quarter) := + arr.mapFinIdx (fun idx val n => (val, Bounded.LE.ofFin ⟨idx, n⟩ |>.add 1)) |>.toArray + +private def parseMonthLong (symbols : LocaleSymbols) : Parser Month.Ordinal := + parseFromSymbols (monthPairs symbols.monthLong) /-- -Parses a short value of a `Month.Ordinal` +Parses a short month name using the given locale symbols and returns the corresponding `Month.Ordinal`. -/ -def parseMonthShort : Parser Month.Ordinal - := pstring "Jan" *> pure ⟨1, by decide⟩ - <|> pstring "Feb" *> pure ⟨2, by decide⟩ - <|> pstring "Mar" *> pure ⟨3, by decide⟩ - <|> pstring "Apr" *> pure ⟨4, by decide⟩ - <|> pstring "May" *> pure ⟨5, by decide⟩ - <|> pstring "Jun" *> pure ⟨6, by decide⟩ - <|> pstring "Jul" *> pure ⟨7, by decide⟩ - <|> pstring "Aug" *> pure ⟨8, by decide⟩ - <|> pstring "Sep" *> pure ⟨9, by decide⟩ - <|> pstring "Oct" *> pure ⟨10, by decide⟩ - <|> pstring "Nov" *> pure ⟨11, by decide⟩ - <|> pstring "Dec" *> pure ⟨12, by decide⟩ - -private def parseMonthNarrow : Parser Month.Ordinal - := pstring "J" *> pure ⟨1, by decide⟩ - <|> pstring "F" *> pure ⟨2, by decide⟩ - <|> pstring "M" *> pure ⟨3, by decide⟩ - <|> pstring "A" *> pure ⟨4, by decide⟩ - <|> pstring "M" *> pure ⟨5, by decide⟩ - <|> pstring "J" *> pure ⟨6, by decide⟩ - <|> pstring "J" *> pure ⟨7, by decide⟩ - <|> pstring "A" *> pure ⟨8, by decide⟩ - <|> pstring "S" *> pure ⟨9, by decide⟩ - <|> pstring "O" *> pure ⟨10, by decide⟩ - <|> pstring "N" *> pure ⟨11, by decide⟩ - <|> pstring "D" *> pure ⟨12, by decide⟩ - -private def parseWeekdayLong : Parser Weekday - := pstring "Sunday" *> pure Weekday.sunday - <|> pstring "Monday" *> pure Weekday.monday - <|> pstring "Tuesday" *> pure Weekday.tuesday - <|> pstring "Wednesday" *> pure Weekday.wednesday - <|> pstring "Thursday" *> pure Weekday.thursday - <|> pstring "Friday" *> pure Weekday.friday - <|> pstring "Saturday" *> pure Weekday.saturday - -private def parseWeekdayShort : Parser Weekday - := pstring "Sun" *> pure Weekday.sunday - <|> pstring "Mon" *> pure Weekday.monday - <|> pstring "Tue" *> pure Weekday.tuesday - <|> pstring "Wed" *> pure Weekday.wednesday - <|> pstring "Thu" *> pure Weekday.thursday - <|> pstring "Fri" *> pure Weekday.friday - <|> pstring "Sat" *> pure Weekday.saturday - -private def parseWeekdayNarrow : Parser Weekday - := pstring "S" *> pure Weekday.sunday - <|> pstring "M" *> pure Weekday.monday - <|> pstring "T" *> pure Weekday.tuesday - <|> pstring "W" *> pure Weekday.wednesday - <|> pstring "T" *> pure Weekday.thursday - <|> pstring "F" *> pure Weekday.friday - <|> pstring "S" *> pure Weekday.saturday - -private def parseEraShort : Parser Year.Era - := pstring "BCE" *> pure Year.Era.bce - <|> pstring "CE" *> pure Year.Era.ce - -private def parseEraLong : Parser Year.Era - := pstring "Before Common Era" *> pure Year.Era.bce - <|> pstring "Common Era" *> pure Year.Era.ce - -private def parseEraNarrow : Parser Year.Era - := pstring "B" *> pure Year.Era.bce - <|> pstring "C" *> pure Year.Era.ce +def parseMonthShort (symbols : LocaleSymbols) : Parser Month.Ordinal := + parseFromSymbols (monthPairs symbols.monthShort) + +private def parseMonthNarrow (symbols : LocaleSymbols) : Parser Month.Ordinal := + parseFromSymbols (monthPairs symbols.monthNarrow) + +private def parseWeekdayLong (symbols : LocaleSymbols) : Parser Weekday := + parseFromSymbols (weekdayPairs symbols.weekdayLong) + +private def parseWeekdayShort (symbols : LocaleSymbols) : Parser Weekday := + parseFromSymbols (weekdayPairs symbols.weekdayShort) + +private def parseWeekdayNarrow (symbols : LocaleSymbols) : Parser Weekday := + parseFromSymbols (weekdayPairs symbols.weekdayNarrow) + +private def parseEraShort (symbols : LocaleSymbols) : Parser Year.Era := + parseFromSymbols (eraPairs symbols.eraShort) + +private def parseEraLong (symbols : LocaleSymbols) : Parser Year.Era := + parseFromSymbols (eraPairs symbols.eraLong) + +private def parseEraNarrow (symbols : LocaleSymbols) : Parser Year.Era := + parseFromSymbols (eraPairs symbols.eraNarrow) private def parseQuarterNumber : Parser Month.Quarter := pstring "1" *> pure ⟨1, by decide⟩ @@ -1043,29 +968,20 @@ private def parseQuarterNumber : Parser Month.Quarter <|> pstring "3" *> pure ⟨3, by decide⟩ <|> pstring "4" *> pure ⟨4, by decide⟩ -private def parseQuarterLong : Parser Month.Quarter - := pstring "1st quarter" *> pure ⟨1, by decide⟩ - <|> pstring "2nd quarter" *> pure ⟨2, by decide⟩ - <|> pstring "3rd quarter" *> pure ⟨3, by decide⟩ - <|> pstring "4th quarter" *> pure ⟨4, by decide⟩ +private def parseQuarterLong (symbols : LocaleSymbols) : Parser Month.Quarter := + parseFromSymbols (quarterPairs symbols.quarterLong) -private def parseQuarterShort : Parser Month.Quarter - := pstring "Q1" *> pure ⟨1, by decide⟩ - <|> pstring "Q2" *> pure ⟨2, by decide⟩ - <|> pstring "Q3" *> pure ⟨3, by decide⟩ - <|> pstring "Q4" *> pure ⟨4, by decide⟩ +private def parseQuarterShort (symbols : LocaleSymbols) : Parser Month.Quarter := + parseFromSymbols (quarterPairs symbols.quarterShort) -private def parseMarkerShort : Parser HourMarker - := pstring "AM" *> pure HourMarker.am - <|> pstring "PM" *> pure HourMarker.pm +private def parseMarkerShort (symbols : LocaleSymbols) : Parser HourMarker := + (pstring symbols.amShort *> pure .am) <|> (pstring symbols.pmShort *> pure .pm) -private def parseMarkerLong : Parser HourMarker - := pstring "Ante Meridiem" *> pure HourMarker.am - <|> pstring "Post Meridiem" *> pure HourMarker.pm +private def parseMarkerLong (symbols : LocaleSymbols) : Parser HourMarker := + (pstring symbols.amLong *> pure .am) <|> (pstring symbols.pmLong *> pure .pm) -private def parseMarkerNarrow : Parser HourMarker - := pstring "A" *> pure HourMarker.am - <|> pstring "P" *> pure HourMarker.pm +private def parseMarkerNarrow (symbols : LocaleSymbols) : Parser HourMarker := + (pstring symbols.amNarrow *> pure .am) <|> (pstring symbols.pmNarrow *> pure .pm) private def exactly (parse : Parser α) (size : Nat) : Parser (Array α) := let rec go (acc : Array α) (count : Nat) : Parser (Array α) := @@ -1158,9 +1074,9 @@ private def parseOffset (withMinutes : Reason) (withSeconds : Reason) (withColon private def parseWith (config : FormatConfig) : (mod : Modifier) → Parser (TypeFormat mod) | .G format => match format with - | .short => parseEraShort - | .full => parseEraLong - | .narrow => parseEraNarrow + | .short => parseEraShort config.locale + | .full => parseEraLong config.locale + | .narrow => parseEraNarrow config.locale | .y format => match format with | .any => Int.ofNat <$> parseAtLeastNum 1 @@ -1177,35 +1093,35 @@ private def parseWith (config : FormatConfig) : (mod : Modifier) → Parser (Typ | .MorL format => match format with | .inl format => parseNatToBounded (parseFlexibleNum format.padding) - | .inr .short => parseMonthShort - | .inr .full => parseMonthLong - | .inr .narrow => parseMonthNarrow + | .inr .short => parseMonthShort config.locale + | .inr .full => parseMonthLong config.locale + | .inr .narrow => parseMonthNarrow config.locale | .d format => parseNatToBounded (parseFlexibleNum format.padding) | .Qorq format => match format with | .inl format => parseNatToBounded (parseFlexibleNum format.padding) - | .inr .short => parseQuarterShort - | .inr .full => parseQuarterLong + | .inr .short => parseQuarterShort config.locale + | .inr .full => parseQuarterLong config.locale | .inr .narrow => parseQuarterNumber | .w format => parseNatToBounded (parseFlexibleNum format.padding) | .W format => parseNatToBounded (parseFlexibleNum format.padding) | .E format => match format with - | .short => parseWeekdayShort - | .full => parseWeekdayLong - | .narrow => parseWeekdayNarrow + | .short => parseWeekdayShort config.locale + | .full => parseWeekdayLong config.locale + | .narrow => parseWeekdayNarrow config.locale | .eorc format => match format with | .inl format => Weekday.ofOrdinal <$> parseNatToBounded (parseFlexibleNum format.padding) - | .inr .short => parseWeekdayShort - | .inr .full => parseWeekdayLong - | .inr .narrow => parseWeekdayNarrow + | .inr .short => parseWeekdayShort config.locale + | .inr .full => parseWeekdayLong config.locale + | .inr .narrow => parseWeekdayNarrow config.locale | .F format => parseNatToBounded (parseFlexibleNum format.padding) | .a format => match format with - | .short => parseMarkerShort - | .full => parseMarkerLong - | .narrow => parseMarkerNarrow + | .short => parseMarkerShort config.locale + | .full => parseMarkerLong config.locale + | .narrow => parseMarkerNarrow config.locale | .h format => parseNatToBounded (parseFlexibleNum format.padding) | .K format => parseNatToBounded (parseFlexibleNum format.padding) | .k format => parseNatToBounded (parseFlexibleNum format.padding) @@ -1266,9 +1182,9 @@ private def parseWith (config : FormatConfig) : (mod : Modifier) → Parser (Typ (skipString "Z" *> pure Offset.zero) <|> (parseOffset .yes .optional true) -private def formatPartWithDate (date : DateTime tz) (part : FormatPart) : String := +private def formatPartWithDate (locale : LocaleSymbols) (date : DateTime tz) (part : FormatPart) : String := match part with - | .modifier mod => formatWith mod (dateFromModifier date) + | .modifier mod => formatWith locale mod (dateFromModifier date) | .string s => s set_option linter.missingDocs false in -- TODO @@ -1433,10 +1349,11 @@ def spec! (input : String) (config : FormatConfig := {}) : GenericFormat tz := Formats a `DateTime` value into a string using the given `GenericFormat`. -/ def format (format : GenericFormat aw) (date : DateTime tz) : String := + let locale := format.config.locale let mapper (part : FormatPart) := match aw with - | .any => formatPartWithDate date part - | .only tz => formatPartWithDate (date.convertTimeZone tz) part + | .any => formatPartWithDate locale date part + | .only tz => formatPartWithDate locale (date.convertTimeZone tz) part format.string.map mapper |> String.join @@ -1499,8 +1416,9 @@ def parseBuilder! [Inhabited α] (format : GenericFormat aw) (builder : FormatT Formats the date using the format into a String, using a `getInfo` function to get the information needed to build the `String`. -/ def formatGeneric (format : GenericFormat aw) (getInfo : (typ : Modifier) → Option (TypeFormat typ)) : Option String := + let locale := format.config.locale let rec go (data : String) : (format : FormatString) → Option String - | .modifier x :: xs => do go (data ++ formatWith x (← getInfo x)) xs + | .modifier x :: xs => do go (data ++ formatWith locale x (← getInfo x)) xs | .string x :: xs => go (data ++ x) xs | [] => some data go "" format.string @@ -1509,8 +1427,9 @@ def formatGeneric (format : GenericFormat aw) (getInfo : (typ : Modifier) → Op Constructs a `FormatType` function to format a date into a string using a `GenericFormat`. -/ def formatBuilder (format : GenericFormat aw) : FormatType String format.string := + let locale := format.config.locale let rec go (data : String) : (format : FormatString) → FormatType String format - | .modifier x :: xs => fun res => go (data ++ formatWith x res) xs + | .modifier x :: xs => fun res => go (data ++ formatWith locale x res) xs | .string x :: xs => go (data ++ x) xs | [] => data go "" format.string diff --git a/src/Std/Time/Format/Locale.lean b/src/Std/Time/Format/Locale.lean new file mode 100644 index 000000000000..8905611836b6 --- /dev/null +++ b/src/Std/Time/Format/Locale.lean @@ -0,0 +1,171 @@ +/- +Copyright (c) 2026 Lean FRO, LLC. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Sofia Rodrigues +-/ +module + +prelude +public import Init.Data.Vector.Extract +public import Std.Time.Date.Unit.Weekday + +public section + +namespace Std +namespace Time + +set_option linter.all true + +/-- +`LocaleSymbols` contains locale-specific strings needed for date/time formatting and parsing. + +Arrays are 0-indexed: `monthLong[0]` = January, `weekdayLong[0]` = Sunday, `eraShort[0]` = BCE. +-/ +structure LocaleSymbols where + + /-- + Full month names (12 elements, index 0 = January). + -/ + monthLong : Vector String 12 + + /-- + Abbreviated month names (12 elements, index 0 = Jan). + -/ + monthShort : Vector String 12 + + /-- + Narrow month names (12 elements, index 0 = J). + -/ + monthNarrow : Vector String 12 + + /-- + Full weekday names (7 elements, index 0 = Sunday). + -/ + weekdayLong : Vector String 7 + + /-- + Abbreviated weekday names (7 elements, index 0 = Sun). + -/ + weekdayShort : Vector String 7 + + /-- + Narrow weekday names (7 elements, index 0 = S). + -/ + weekdayNarrow : Vector String 7 + + /-- + Short era names (2 elements: index 0 = BCE, index 1 = CE). + -/ + eraShort : Vector String 2 + + /-- + Full era names (2 elements: index 0 = Before Common Era, index 1 = Common Era). + -/ + eraLong : Vector String 2 + + /-- + Narrow era names (2 elements: index 0 = B, index 1 = C). + -/ + eraNarrow : Vector String 2 + + /-- + Short quarter names (4 elements, index 0 = Q1). + -/ + quarterShort : Vector String 4 + + /-- + Full quarter names (4 elements, index 0 = 1st quarter). + -/ + quarterLong : Vector String 4 + + /-- + Narrow quarter names (4 elements, index 0 = 1). + -/ + quarterNarrow : Vector String 4 + + /-- + Short AM marker. + -/ + amShort : String + + /-- + Short PM marker. + -/ + pmShort : String + + /-- + Full AM marker. + -/ + amLong : String + + /-- + Full PM marker. + -/ + pmLong : String + + /-- + Narrow AM marker. + -/ + amNarrow : String + + /-- + Narrow PM marker. + -/ + pmNarrow : String + +namespace LocaleSymbols + +/-- +English (US) locale symbols. +-/ +def enUS : LocaleSymbols where + monthLong := Vector.mk #["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"] (by decide) + monthShort := Vector.mk #["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] (by decide) + monthNarrow := Vector.mk #["J", "F", "M", "A", "M", "J", "J", "A", "S", "O", "N", "D"] (by decide) + weekdayLong := Vector.mk #["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] (by decide) + weekdayShort := Vector.mk #["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] (by decide) + weekdayNarrow := Vector.mk #["M", "T", "W", "T", "F", "S", "S"] (by decide) + eraShort := Vector.mk #["BCE", "CE"] (by decide) + eraLong := Vector.mk #["Before Common Era", "Common Era"] (by decide) + eraNarrow := Vector.mk #["B", "C"] (by decide) + quarterShort := Vector.mk #["Q1", "Q2", "Q3", "Q4"] (by decide) + quarterLong := Vector.mk #["1st quarter", "2nd quarter", "3rd quarter", "4th quarter"] (by decide) + quarterNarrow := Vector.mk #["1", "2", "3", "4"] (by decide) + amShort := "AM" + pmShort := "PM" + amLong := "Ante Meridiem" + pmLong := "Post Meridiem" + amNarrow := "A" + pmNarrow := "P" + +end LocaleSymbols + +/-- +`Locale` contains locale-specific configuration for date/time formatting and parsing, +combining calendar conventions (e.g. first day of week) with locale symbols. +-/ +structure Locale where + + /-- + The first day of the week for this locale (e.g., `Weekday.sunday` for US, `Weekday.monday` for ISO 8601). + -/ + firstDayOfWeek : Weekday := .sunday + + /-- + Locale-specific symbols used for formatting and parsing text fields. + -/ + symbols : LocaleSymbols := LocaleSymbols.enUS + +namespace Locale + +/-- +English (US) locale. +-/ +def enUS : Locale where + firstDayOfWeek := .sunday + symbols := LocaleSymbols.enUS + +end Locale + +end Time +end Std From 9300dd6856cfd98222439c73b6bff14385d8b167 Mon Sep 17 00:00:00 2001 From: Sofia Rodrigues Date: Wed, 29 Apr 2026 02:47:26 -0300 Subject: [PATCH 2/3] fix: first day of week --- src/Std/Time/Date/PlainDate.lean | 16 ++++--- src/Std/Time/DateTime/PlainDateTime.lean | 14 +++--- src/Std/Time/Format/Basic.lean | 56 ++++++++++++------------ src/Std/Time/Zoned/DateTime.lean | 14 +++--- src/Std/Time/Zoned/ZonedDateTime.lean | 14 +++--- tests/elab/timeFormats.lean | 4 +- 6 files changed, 57 insertions(+), 61 deletions(-) diff --git a/src/Std/Time/Date/PlainDate.lean b/src/Std/Time/Date/PlainDate.lean index 0a486e026293..2b6936f0444f 100644 --- a/src/Std/Time/Date/PlainDate.lean +++ b/src/Std/Time/Date/PlainDate.lean @@ -310,10 +310,10 @@ Determines the week of the month for the given `PlainDate`. The week of the mont on the day of the month and the weekday. Each week starts on Monday because the entire library is based on the Gregorian Calendar. -/ -def alignedWeekOfMonth (date : PlainDate) : Week.Ordinal.OfMonth := - let weekday := date.withDaysClip 1 |>.weekday |>.toOrdinal |>.sub 1 - let days := date.day |>.sub 1 |>.addBounds weekday - days |>.ediv 7 (by decide) |>.add 1 +def alignedWeekOfMonth (date : PlainDate) (firstDay : Weekday := .monday) : Week.Ordinal.OfMonth := + let day1Ord := (date.withDaysClip 1).weekday.toOrdinal.val + let offset := (day1Ord - firstDay.toOrdinal.val + 7) % 7 + Bounded.LE.ofNatWrapping ((date.day.val - 1 + offset) / 7 + 1) (by decide) /-- Sets the date to the specified `desiredWeekday`. If the `desiredWeekday` is the same as the current weekday, @@ -335,14 +335,16 @@ def withWeekday (date : PlainDate) (desiredWeekday : Weekday) : PlainDate := date.addDays (Day.Offset.ofInt offset.toInt) /-- -Calculates the week of the year starting Monday for a given year. +Calculates the week of the year for a given date, using `firstDay` as the first day of the week. -/ -def weekOfYear (date : PlainDate) : Week.Ordinal := +def weekOfYear (date : PlainDate) (firstDay : Weekday := .monday) : Week.Ordinal := let y := date.year + let posInWeek : Bounded.LE 1 7 := + .ofNatWrapping ((date.weekday.toOrdinal.val - firstDay.toOrdinal.val + 7) % 7 + 1) (by decide) let w := Bounded.LE.exact 10 |>.addBounds date.dayOfYear - |>.subBounds date.weekday.toOrdinal + |>.subBounds posInWeek |>.ediv 7 (by decide) if h : w.val < 1 then diff --git a/src/Std/Time/DateTime/PlainDateTime.lean b/src/Std/Time/DateTime/PlainDateTime.lean index 4f14a4198177..ce731a9629a5 100644 --- a/src/Std/Time/DateTime/PlainDateTime.lean +++ b/src/Std/Time/DateTime/PlainDateTime.lean @@ -489,11 +489,11 @@ def inLeapYear (date : PlainDateTime) : Bool := date.year.isLeap /-- -Determines the week of the year for the given `PlainDateTime`. +Determines the week of the year for the given `PlainDateTime`, using `firstDay` as the start of the week. -/ @[inline] -def weekOfYear (date : PlainDateTime) : Week.Ordinal := - date.date.weekOfYear +def weekOfYear (date : PlainDateTime) (firstDay : Weekday := .monday) : Week.Ordinal := + date.date.weekOfYear firstDay /-- Returns the unaligned week of the month for a `PlainDateTime` (day divided by 7, plus 1). @@ -502,13 +502,11 @@ def weekOfMonth (date : PlainDateTime) : Bounded.LE 1 5 := date.date.weekOfMonth /-- -Determines the week of the month for the given `PlainDateTime`. The week of the month is calculated based -on the day of the month and the weekday. Each week starts on Monday because the entire library is -based on the Gregorian Calendar. +Determines the week of the month for the given `PlainDateTime`, using `firstDay` as the start of the week. -/ @[inline] -def alignedWeekOfMonth (date : PlainDateTime) : Week.Ordinal.OfMonth := - date.date.alignedWeekOfMonth +def alignedWeekOfMonth (date : PlainDateTime) (firstDay : Weekday := .monday) : Week.Ordinal.OfMonth := + date.date.alignedWeekOfMonth firstDay /-- Transforms a tuple of a `PlainDateTime` into a `Day.Ordinal.OfYear`. diff --git a/src/Std/Time/Format/Basic.lean b/src/Std/Time/Format/Basic.lean index 3f0b2be5deec..e7899a55e570 100644 --- a/src/Std/Time/Format/Basic.lean +++ b/src/Std/Time/Format/Basic.lean @@ -585,11 +585,11 @@ structure FormatConfig where allowLeapSeconds : Bool := false /-- - Locale-specific symbols used for formatting and parsing text fields such as month names, - weekday names, era designators, and AM/PM markers. - Default is `LocaleSymbols.enUS` (English, US). + Locale configuration for formatting and parsing, including locale-specific symbols and the first + day of the week used for week-of-year and week-of-month calculations. + Default is `Locale.enUS` (English, US). -/ - locale : LocaleSymbols := LocaleSymbols.enUS + locale : Locale := Locale.enUS deriving Inhabited @@ -871,7 +871,7 @@ private def formatWith (locale : LocaleSymbols) (modifier : Modifier) (data : Ty then "Z" else toIsoString data true (data.second.val % 60 ≠ 0) true -private def dateFromModifier (date : DateTime tz) : TypeFormat modifier := +private def dateFromModifier (firstDay : Weekday) (date : DateTime tz) : TypeFormat modifier := match modifier with | .G _ => date.era | .y _ => date.year @@ -880,8 +880,8 @@ private def dateFromModifier (date : DateTime tz) : TypeFormat modifier := | .MorL _ => date.month | .d _ => date.day | .Qorq _ => date.quarter - | .w _ => date.weekOfYear - | .W _ => date.alignedWeekOfMonth + | .w _ => date.weekOfYear firstDay + | .W _ => date.alignedWeekOfMonth firstDay | .E _ => date.weekday | .eorc _ => date.weekday | .F _ => date.weekOfMonth @@ -1074,9 +1074,9 @@ private def parseOffset (withMinutes : Reason) (withSeconds : Reason) (withColon private def parseWith (config : FormatConfig) : (mod : Modifier) → Parser (TypeFormat mod) | .G format => match format with - | .short => parseEraShort config.locale - | .full => parseEraLong config.locale - | .narrow => parseEraNarrow config.locale + | .short => parseEraShort config.locale.symbols + | .full => parseEraLong config.locale.symbols + | .narrow => parseEraNarrow config.locale.symbols | .y format => match format with | .any => Int.ofNat <$> parseAtLeastNum 1 @@ -1093,35 +1093,35 @@ private def parseWith (config : FormatConfig) : (mod : Modifier) → Parser (Typ | .MorL format => match format with | .inl format => parseNatToBounded (parseFlexibleNum format.padding) - | .inr .short => parseMonthShort config.locale - | .inr .full => parseMonthLong config.locale - | .inr .narrow => parseMonthNarrow config.locale + | .inr .short => parseMonthShort config.locale.symbols + | .inr .full => parseMonthLong config.locale.symbols + | .inr .narrow => parseMonthNarrow config.locale.symbols | .d format => parseNatToBounded (parseFlexibleNum format.padding) | .Qorq format => match format with | .inl format => parseNatToBounded (parseFlexibleNum format.padding) - | .inr .short => parseQuarterShort config.locale - | .inr .full => parseQuarterLong config.locale + | .inr .short => parseQuarterShort config.locale.symbols + | .inr .full => parseQuarterLong config.locale.symbols | .inr .narrow => parseQuarterNumber | .w format => parseNatToBounded (parseFlexibleNum format.padding) | .W format => parseNatToBounded (parseFlexibleNum format.padding) | .E format => match format with - | .short => parseWeekdayShort config.locale - | .full => parseWeekdayLong config.locale - | .narrow => parseWeekdayNarrow config.locale + | .short => parseWeekdayShort config.locale.symbols + | .full => parseWeekdayLong config.locale.symbols + | .narrow => parseWeekdayNarrow config.locale.symbols | .eorc format => match format with | .inl format => Weekday.ofOrdinal <$> parseNatToBounded (parseFlexibleNum format.padding) - | .inr .short => parseWeekdayShort config.locale - | .inr .full => parseWeekdayLong config.locale - | .inr .narrow => parseWeekdayNarrow config.locale + | .inr .short => parseWeekdayShort config.locale.symbols + | .inr .full => parseWeekdayLong config.locale.symbols + | .inr .narrow => parseWeekdayNarrow config.locale.symbols | .F format => parseNatToBounded (parseFlexibleNum format.padding) | .a format => match format with - | .short => parseMarkerShort config.locale - | .full => parseMarkerLong config.locale - | .narrow => parseMarkerNarrow config.locale + | .short => parseMarkerShort config.locale.symbols + | .full => parseMarkerLong config.locale.symbols + | .narrow => parseMarkerNarrow config.locale.symbols | .h format => parseNatToBounded (parseFlexibleNum format.padding) | .K format => parseNatToBounded (parseFlexibleNum format.padding) | .k format => parseNatToBounded (parseFlexibleNum format.padding) @@ -1182,9 +1182,9 @@ private def parseWith (config : FormatConfig) : (mod : Modifier) → Parser (Typ (skipString "Z" *> pure Offset.zero) <|> (parseOffset .yes .optional true) -private def formatPartWithDate (locale : LocaleSymbols) (date : DateTime tz) (part : FormatPart) : String := +private def formatPartWithDate (locale : Locale) (date : DateTime tz) (part : FormatPart) : String := match part with - | .modifier mod => formatWith locale mod (dateFromModifier date) + | .modifier mod => formatWith locale.symbols mod (dateFromModifier locale.firstDayOfWeek date) | .string s => s set_option linter.missingDocs false in -- TODO @@ -1416,7 +1416,7 @@ def parseBuilder! [Inhabited α] (format : GenericFormat aw) (builder : FormatT Formats the date using the format into a String, using a `getInfo` function to get the information needed to build the `String`. -/ def formatGeneric (format : GenericFormat aw) (getInfo : (typ : Modifier) → Option (TypeFormat typ)) : Option String := - let locale := format.config.locale + let locale := format.config.locale.symbols let rec go (data : String) : (format : FormatString) → Option String | .modifier x :: xs => do go (data ++ formatWith locale x (← getInfo x)) xs | .string x :: xs => go (data ++ x) xs @@ -1427,7 +1427,7 @@ def formatGeneric (format : GenericFormat aw) (getInfo : (typ : Modifier) → Op Constructs a `FormatType` function to format a date into a string using a `GenericFormat`. -/ def formatBuilder (format : GenericFormat aw) : FormatType String format.string := - let locale := format.config.locale + let locale := format.config.locale.symbols let rec go (data : String) : (format : FormatString) → FormatType String format | .modifier x :: xs => fun res => go (data ++ formatWith locale x res) xs | .string x :: xs => go (data ++ x) xs diff --git a/src/Std/Time/Zoned/DateTime.lean b/src/Std/Time/Zoned/DateTime.lean index 8049652e0143..be28fdce32f7 100644 --- a/src/Std/Time/Zoned/DateTime.lean +++ b/src/Std/Time/Zoned/DateTime.lean @@ -432,11 +432,11 @@ def dayOfYear (date : DateTime tz) : Day.Ordinal.OfYear date.year.isLeap := ValidDate.dayOfYear ⟨⟨date.month, date.day⟩, date.date.get.date.valid⟩ /-- -Determines the week of the year for the given `DateTime`. +Determines the week of the year for the given `DateTime`, using `firstDay` as the start of the week. -/ @[inline] -def weekOfYear (date : DateTime tz) : Week.Ordinal := - date.date.get.weekOfYear +def weekOfYear (date : DateTime tz) (firstDay : Weekday := .monday) : Week.Ordinal := + date.date.get.weekOfYear firstDay /-- Returns the unaligned week of the month for a `DateTime` (day divided by 7, plus 1). @@ -445,13 +445,11 @@ def weekOfMonth (date : DateTime tz) : Bounded.LE 1 5 := date.date.get.weekOfMonth /-- -Determines the week of the month for the given `DateTime`. The week of the month is calculated based -on the day of the month and the weekday. Each week starts on Monday because the entire library is -based on the Gregorian Calendar. +Determines the week of the month for the given `DateTime`, using `firstDay` as the start of the week. -/ @[inline] -def alignedWeekOfMonth (date : DateTime tz) : Week.Ordinal.OfMonth := - date.date.get.alignedWeekOfMonth +def alignedWeekOfMonth (date : DateTime tz) (firstDay : Weekday := .monday) : Week.Ordinal.OfMonth := + date.date.get.alignedWeekOfMonth firstDay /-- Determines the quarter of the year for the given `DateTime`. diff --git a/src/Std/Time/Zoned/ZonedDateTime.lean b/src/Std/Time/Zoned/ZonedDateTime.lean index 062b0a50de25..19eeda40d356 100644 --- a/src/Std/Time/Zoned/ZonedDateTime.lean +++ b/src/Std/Time/Zoned/ZonedDateTime.lean @@ -224,11 +224,11 @@ def dayOfYear (date : ZonedDateTime) : Day.Ordinal.OfYear date.year.isLeap := ValidDate.dayOfYear ⟨(date.month, date.day), date.date.get.date.valid⟩ /-- -Determines the week of the year for the given `ZonedDateTime`. +Determines the week of the year for the given `ZonedDateTime`, using `firstDay` as the start of the week. -/ @[inline] -def weekOfYear (date : ZonedDateTime) : Week.Ordinal := - date.date.get.weekOfYear +def weekOfYear (date : ZonedDateTime) (firstDay : Weekday := .monday) : Week.Ordinal := + date.date.get.weekOfYear firstDay /-- Returns the unaligned week of the month for a `ZonedDateTime` (day divided by 7, plus 1). @@ -237,13 +237,11 @@ def weekOfMonth (date : ZonedDateTime) : Internal.Bounded.LE 1 5 := date.date.get.weekOfMonth /-- -Determines the week of the month for the given `ZonedDateTime`. The week of the month is calculated based -on the day of the month and the weekday. Each week starts on Monday because the entire library is -based on the Gregorian Calendar. +Determines the week of the month for the given `ZonedDateTime`, using `firstDay` as the start of the week. -/ @[inline] -def alignedWeekOfMonth (date : ZonedDateTime) : Week.Ordinal.OfMonth := - date.date.get.alignedWeekOfMonth +def alignedWeekOfMonth (date : ZonedDateTime) (firstDay : Weekday := .monday) : Week.Ordinal.OfMonth := + date.date.get.alignedWeekOfMonth firstDay /-- Determines the quarter of the year for the given `ZonedDateTime`. diff --git a/tests/elab/timeFormats.lean b/tests/elab/timeFormats.lean index baa6571f87d5..5a8cf3f56d46 100644 --- a/tests/elab/timeFormats.lean +++ b/tests/elab/timeFormats.lean @@ -339,13 +339,13 @@ info: "3 03 3rd quarter 3" #eval zoned₄.format "Q QQ QQQQ QQQQQ" /-- -info: "28 28 028 0028" +info: "29 29 029 0029" -/ #guard_msgs in #eval zoned₄.format "w ww www wwww" /-- -info: "2 02 002 0002" +info: "3 03 003 0003" -/ #guard_msgs in #eval zoned₄.format "W WW WWW WWWW" From bbb07e82ef397c3700a3aad49b500b2f294aa859 Mon Sep 17 00:00:00 2001 From: Sofia Rodrigues Date: Wed, 29 Apr 2026 19:20:57 -0300 Subject: [PATCH 3/3] fix: locale aware operations --- src/Std/Time/Date/PlainDate.lean | 18 ++++++ src/Std/Time/DateTime/PlainDateTime.lean | 8 +++ src/Std/Time/Format.lean | 18 +++--- src/Std/Time/Format/Basic.lean | 82 +++++++++++++++++------- src/Std/Time/Format/Locale.lean | 6 +- src/Std/Time/Notation.lean | 1 + src/Std/Time/Notation/Spec.lean | 1 + src/Std/Time/Zoned/DateTime.lean | 8 +++ src/Std/Time/Zoned/ZonedDateTime.lean | 8 +++ tests/elab/timeFormats.lean | 14 ++-- 10 files changed, 122 insertions(+), 42 deletions(-) diff --git a/src/Std/Time/Date/PlainDate.lean b/src/Std/Time/Date/PlainDate.lean index 2b6936f0444f..eb86d52b8b6a 100644 --- a/src/Std/Time/Date/PlainDate.lean +++ b/src/Std/Time/Date/PlainDate.lean @@ -357,6 +357,24 @@ def weekOfYear (date : PlainDate) (firstDay : Weekday := .monday) : Week.Ordinal let w := w.truncateBottom h |>.truncateTop (Int.le_trans h₁ y.weeks.property.right) w +/-- +Returns the week-based year for the given `PlainDate`, using `firstDay` as the start of the week. +The week-based year may differ from the calendar year for dates near the start or end of the year. +-/ +def weekYear (date : PlainDate) (firstDay : Weekday := .monday) : Year.Offset := + let y := date.year + let posInWeek : Bounded.LE 1 7 := + .ofNatWrapping ((date.weekday.toOrdinal.val - firstDay.toOrdinal.val + 7) % 7 + 1) (by decide) + + let w := Bounded.LE.exact 10 + |>.addBounds date.dayOfYear + |>.subBounds posInWeek + |>.ediv 7 (by decide) + + if w.val < 1 then y - 1 + else if w.val > y.weeks.val then y + 1 + else y + instance : HAdd PlainDate Day.Offset PlainDate where hAdd := addDays diff --git a/src/Std/Time/DateTime/PlainDateTime.lean b/src/Std/Time/DateTime/PlainDateTime.lean index ce731a9629a5..451574a1022e 100644 --- a/src/Std/Time/DateTime/PlainDateTime.lean +++ b/src/Std/Time/DateTime/PlainDateTime.lean @@ -495,6 +495,14 @@ Determines the week of the year for the given `PlainDateTime`, using `firstDay` def weekOfYear (date : PlainDateTime) (firstDay : Weekday := .monday) : Week.Ordinal := date.date.weekOfYear firstDay +/-- +Returns the week-based year for the given `PlainDateTime`, using `firstDay` as the start of the week. +The week-based year may differ from the calendar year for dates near the start or end of the year. +-/ +@[inline] +def weekYear (date : PlainDateTime) (firstDay : Weekday := .monday) : Year.Offset := + date.date.weekYear firstDay + /-- Returns the unaligned week of the month for a `PlainDateTime` (day divided by 7, plus 1). -/ diff --git a/src/Std/Time/Format.lean b/src/Std/Time/Format.lean index 846dce68f3a0..989575c90e2c 100644 --- a/src/Std/Time/Format.lean +++ b/src/Std/Time/Format.lean @@ -178,9 +178,9 @@ end TimeZone namespace PlainDate /-- -Formats a `PlainDate` using a specific format. +Formats a `PlainDate` using a specific format and locale. -/ -def format (date : PlainDate) (format : String) : String := +def format (date : PlainDate) (format : String) (locale : Locale := .enUS) : String := let format : Except String (GenericFormat .any) := GenericFormat.spec format match format with | .error err => s!"error: {err}" @@ -189,10 +189,11 @@ def format (date : PlainDate) (format : String) : String := | .G _ => some date.era | .y _ => some date.year | .u _ => some date.year + | .Y _ => some (date.weekYear locale.firstDayOfWeek) | .D _ => some (Sigma.mk date.year.isLeap date.dayOfYear) | .Qorq _ => some date.quarter - | .w _ => some date.weekOfYear - | .W _ => some date.alignedWeekOfMonth + | .w _ => some (date.weekOfYear locale.firstDayOfWeek) + | .W _ => some (date.alignedWeekOfMonth locale.firstDayOfWeek) | .MorL _ => some date.month | .d _ => some date.day | .E _ => some date.weekday @@ -442,9 +443,9 @@ end ZonedDateTime namespace PlainDateTime /-- -Formats a `PlainDateTime` using a specific format. +Formats a `PlainDateTime` using a specific format and locale. -/ -def format (date : PlainDateTime) (format : String) : String := +def format (date : PlainDateTime) (format : String) (locale : Locale := .enUS) : String := let format : Except String (GenericFormat .any) := GenericFormat.spec format match format with | .error err => s!"error: {err}" @@ -453,10 +454,11 @@ def format (date : PlainDateTime) (format : String) : String := | .G _ => some date.era | .y _ => some date.year | .u _ => some date.year + | .Y _ => some (date.weekYear locale.firstDayOfWeek) | .D _ => some (Sigma.mk date.year.isLeap date.dayOfYear) | .Qorq _ => some date.quarter - | .w _ => some date.weekOfYear - | .W _ => some date.alignedWeekOfMonth + | .w _ => some (date.weekOfYear locale.firstDayOfWeek) + | .W _ => some (date.alignedWeekOfMonth locale.firstDayOfWeek) | .MorL _ => some date.month | .d _ => some date.day | .E _ => some date.weekday diff --git a/src/Std/Time/Format/Basic.lean b/src/Std/Time/Format/Basic.lean index e7899a55e570..eab48c2769ca 100644 --- a/src/Std/Time/Format/Basic.lean +++ b/src/Std/Time/Format/Basic.lean @@ -306,6 +306,12 @@ inductive Modifier -/ | u (presentation : Year) + /-- + `Y`: Week-based year (e.g., 2004, 04, -0001, -1). + The week-based year may differ from the calendar year at the start or end of the year. + -/ + | Y (presentation : Year) + /-- `D`: Day of year (e.g., 189). -/ @@ -484,6 +490,7 @@ private def parseModifier : Parser Modifier := (parseText Modifier.G =<< many1Chars (pchar 'G')) <|> parseYear Modifier.y =<< many1Chars (pchar 'y') <|> parseYear Modifier.u =<< many1Chars (pchar 'u') + <|> parseYear Modifier.Y =<< many1Chars (pchar 'Y') <|> parseNumber Modifier.D =<< many1Chars (pchar 'D') <|> parseNumberText Modifier.MorL =<< many1Chars (pchar 'M') <|> parseNumberText Modifier.MorL =<< many1Chars (pchar 'L') @@ -725,6 +732,7 @@ def TypeFormat : Modifier → Type | .G _ => Year.Era | .y _ => Year.Offset | .u _ => Year.Offset + | .Y _ => Year.Offset | .D _ => Sigma Day.Ordinal.OfYear | .MorL _ => Month.Ordinal | .d _ => Day.Ordinal @@ -752,18 +760,18 @@ def TypeFormat : Modifier → Type | .x _ => Offset | .Z _ => Offset -private def formatWith (locale : LocaleSymbols) (modifier : Modifier) (data : TypeFormat modifier) : String := +private def formatWith (locale : Locale) (modifier : Modifier) (data : TypeFormat modifier) : String := match modifier with | .G format => match format with - | .short => formatEraShort locale data - | .full => formatEraLong locale data - | .narrow => formatEraNarrow locale data + | .short => formatEraShort locale.symbols data + | .full => formatEraLong locale.symbols data + | .narrow => formatEraNarrow locale.symbols data | .y format => let info := data.toInt let info := if info ≤ 0 then -info + 1 else info match format with - | .any => pad 0 (data.toInt) + | .any => pad 0 info | .twoDigit => pad 2 (info % 100) | .fourDigit => pad 4 info | .extended n => pad n info @@ -773,21 +781,29 @@ private def formatWith (locale : LocaleSymbols) (modifier : Modifier) (data : Ty | .twoDigit => pad 2 (data.toInt % 100) | .fourDigit => pad 4 data.toInt | .extended n => pad n data.toInt + | .Y format => + let info := data.toInt + let info := if info ≤ 0 then -info + 1 else info + match format with + | .any => pad 0 info + | .twoDigit => pad 2 (info % 100) + | .fourDigit => pad 4 info + | .extended n => pad n info | .D format => pad format.padding data.snd.val | .MorL format => match format with | .inl format => pad format.padding data.val - | .inr .short => formatMonthShort locale data - | .inr .full => formatMonthLong locale data - | .inr .narrow => formatMonthNarrow locale data + | .inr .short => formatMonthShort locale.symbols data + | .inr .full => formatMonthLong locale.symbols data + | .inr .narrow => formatMonthNarrow locale.symbols data | .d format => pad format.padding data.val | .Qorq format => match format with | .inl format => pad format.padding data.val - | .inr .short => formatQuarterShort locale data - | .inr .full => formatQuarterLong locale data + | .inr .short => formatQuarterShort locale.symbols data + | .inr .full => formatQuarterLong locale.symbols data | .inr .narrow => formatQuarterNumber data | .w format => pad format.padding data.val @@ -795,22 +811,25 @@ private def formatWith (locale : LocaleSymbols) (modifier : Modifier) (data : Ty pad format.padding data.val | .E format => match format with - | .short => formatWeekdayShort locale data - | .full => formatWeekdayLong locale data - | .narrow => formatWeekdayNarrow locale data + | .short => formatWeekdayShort locale.symbols data + | .full => formatWeekdayLong locale.symbols data + | .narrow => formatWeekdayNarrow locale.symbols data | .eorc format => match format with - | .inl format => pad format.padding data.toOrdinal.val - | .inr .short => formatWeekdayShort locale data - | .inr .full => formatWeekdayLong locale data - | .inr .narrow => formatWeekdayNarrow locale data + | .inl format => + let firstOrd : Int := locale.firstDayOfWeek.toOrdinal.val + let dayOrd : Int := data.toOrdinal.val + pad format.padding ((dayOrd - firstOrd + 7) % 7 + 1) + | .inr .short => formatWeekdayShort locale.symbols data + | .inr .full => formatWeekdayLong locale.symbols data + | .inr .narrow => formatWeekdayNarrow locale.symbols data | .F format => pad format.padding data.val | .a format => match format with - | .short => formatMarkerShort locale data - | .full => formatMarkerLong locale data - | .narrow => formatMarkerNarrow locale data + | .short => formatMarkerShort locale.symbols data + | .full => formatMarkerLong locale.symbols data + | .narrow => formatMarkerNarrow locale.symbols data | .h format => pad format.padding (data.val % 12) | .K format => pad format.padding (data.val % 12) | .k format => pad format.padding data.val @@ -876,6 +895,7 @@ private def dateFromModifier (firstDay : Weekday) (date : DateTime tz) : TypeFor | .G _ => date.era | .y _ => date.year | .u _ => date.year + | .Y _ => date.weekYear firstDay | .D _ => Sigma.mk _ date.dayOfYear | .MorL _ => date.month | .d _ => date.day @@ -1089,6 +1109,12 @@ private def parseWith (config : FormatConfig) : (mod : Modifier) → Parser (Typ | .twoDigit => (2000 + ·) <$> Int.ofNat <$> parseNum 2 | .fourDigit => parseSigned <| parseNum 4 | .extended n => parseSigned <| parseNum n + | .Y format => + match format with + | .any => parseSigned <| parseAtLeastNum 1 + | .twoDigit => (2000 + ·) <$> Int.ofNat <$> parseNum 2 + | .fourDigit => parseSigned <| parseNum 4 + | .extended n => parseSigned <| parseNum n | .D format => Sigma.mk true <$> parseNatToBounded (parseFlexibleNum format.padding) | .MorL format => match format with @@ -1112,7 +1138,12 @@ private def parseWith (config : FormatConfig) : (mod : Modifier) → Parser (Typ | .narrow => parseWeekdayNarrow config.locale.symbols | .eorc format => match format with - | .inl format => Weekday.ofOrdinal <$> parseNatToBounded (parseFlexibleNum format.padding) + | .inl format => do + let n ← parseFlexibleNum format.padding + if ¬ (1 ≤ n ∧ n ≤ 7) then fail "need a natural number in the interval of 1 to 7" + let firstOrd : Int := config.locale.firstDayOfWeek.toOrdinal.val + let absOrd : Int := ((n : Int) - 1 + firstOrd - 1) % 7 + 1 + return Weekday.ofOrdinal (Bounded.LE.ofNatWrapping absOrd (by decide)) | .inr .short => parseWeekdayShort config.locale.symbols | .inr .full => parseWeekdayLong config.locale.symbols | .inr .narrow => parseWeekdayNarrow config.locale.symbols @@ -1184,7 +1215,7 @@ private def parseWith (config : FormatConfig) : (mod : Modifier) → Parser (Typ private def formatPartWithDate (locale : Locale) (date : DateTime tz) (part : FormatPart) : String := match part with - | .modifier mod => formatWith locale.symbols mod (dateFromModifier locale.firstDayOfWeek date) + | .modifier mod => formatWith locale mod (dateFromModifier locale.firstDayOfWeek date) | .string s => s set_option linter.missingDocs false in -- TODO @@ -1200,6 +1231,7 @@ private structure DateBuilder where G : Option Year.Era := none y : Option Year.Offset := none u : Option Year.Offset := none + Y : Option Year.Offset := none D : Option (Sigma Day.Ordinal.OfYear) := none MorL : Option Month.Ordinal := none d : Option Day.Ordinal := none @@ -1235,6 +1267,7 @@ private def insert (date : DateBuilder) (modifier : Modifier) (data : TypeFormat | .G _ => { date with G := some data } | .y _ => { date with y := some data } | .u _ => { date with u := some data } + | .Y _ => { date with Y := some data } | .D _ => { date with D := some data } | .MorL _ => { date with MorL := some data } | .d _ => { date with d := some data } @@ -1284,6 +1317,7 @@ private def build (builder : DateBuilder) (aw : Awareness) : Option aw.type := let year := builder.u <|> ((convertYearAndEra · era) <$> builder.y) + <|> builder.Y |>.getD 0 let hour : Option (Bounded.LE 0 23) := @@ -1416,7 +1450,7 @@ def parseBuilder! [Inhabited α] (format : GenericFormat aw) (builder : FormatT Formats the date using the format into a String, using a `getInfo` function to get the information needed to build the `String`. -/ def formatGeneric (format : GenericFormat aw) (getInfo : (typ : Modifier) → Option (TypeFormat typ)) : Option String := - let locale := format.config.locale.symbols + let locale := format.config.locale let rec go (data : String) : (format : FormatString) → Option String | .modifier x :: xs => do go (data ++ formatWith locale x (← getInfo x)) xs | .string x :: xs => go (data ++ x) xs @@ -1427,7 +1461,7 @@ def formatGeneric (format : GenericFormat aw) (getInfo : (typ : Modifier) → Op Constructs a `FormatType` function to format a date into a string using a `GenericFormat`. -/ def formatBuilder (format : GenericFormat aw) : FormatType String format.string := - let locale := format.config.locale.symbols + let locale := format.config.locale let rec go (data : String) : (format : FormatString) → FormatType String format | .modifier x :: xs => fun res => go (data ++ formatWith locale x res) xs | .string x :: xs => go (data ++ x) xs diff --git a/src/Std/Time/Format/Locale.lean b/src/Std/Time/Format/Locale.lean index 8905611836b6..bcef0a478431 100644 --- a/src/Std/Time/Format/Locale.lean +++ b/src/Std/Time/Format/Locale.lean @@ -39,17 +39,17 @@ structure LocaleSymbols where monthNarrow : Vector String 12 /-- - Full weekday names (7 elements, index 0 = Sunday). + Full weekday names (7 elements, index 0 = Monday). -/ weekdayLong : Vector String 7 /-- - Abbreviated weekday names (7 elements, index 0 = Sun). + Abbreviated weekday names (7 elements, index 0 = Mon). -/ weekdayShort : Vector String 7 /-- - Narrow weekday names (7 elements, index 0 = S). + Narrow weekday names (7 elements, index 0 = M). -/ weekdayNarrow : Vector String 7 diff --git a/src/Std/Time/Notation.lean b/src/Std/Time/Notation.lean index 9d02a01a9d92..e568cebdcd5e 100644 --- a/src/Std/Time/Notation.lean +++ b/src/Std/Time/Notation.lean @@ -59,6 +59,7 @@ private meta def convertModifier : Modifier → MacroM (TSyntax `term) | .G p => do `(Std.Time.Modifier.G $(← convertText p)) | .y p => do `(Std.Time.Modifier.y $(← convertYear p)) | .u p => do `(Std.Time.Modifier.u $(← convertYear p)) + | .Y p => do `(Std.Time.Modifier.Y $(← convertYear p)) | .D p => do `(Std.Time.Modifier.D $(← convertNumber p)) | .MorL p => match p with diff --git a/src/Std/Time/Notation/Spec.lean b/src/Std/Time/Notation/Spec.lean index d3efdd9a45b2..014093bdd604 100644 --- a/src/Std/Time/Notation/Spec.lean +++ b/src/Std/Time/Notation/Spec.lean @@ -57,6 +57,7 @@ private meta def convertModifier : Modifier → MacroM (TSyntax `term) | .G p => do `(Std.Time.Modifier.G $(← convertText p)) | .y p => do `(Std.Time.Modifier.y $(← convertYear p)) | .u p => do `(Std.Time.Modifier.u $(← convertYear p)) + | .Y p => do `(Std.Time.Modifier.Y $(← convertYear p)) | .D p => do `(Std.Time.Modifier.D $(← convertNumber p)) | .MorL p => match p with diff --git a/src/Std/Time/Zoned/DateTime.lean b/src/Std/Time/Zoned/DateTime.lean index be28fdce32f7..4229d2c63146 100644 --- a/src/Std/Time/Zoned/DateTime.lean +++ b/src/Std/Time/Zoned/DateTime.lean @@ -438,6 +438,14 @@ Determines the week of the year for the given `DateTime`, using `firstDay` as th def weekOfYear (date : DateTime tz) (firstDay : Weekday := .monday) : Week.Ordinal := date.date.get.weekOfYear firstDay +/-- +Returns the week-based year for the given `DateTime`, using `firstDay` as the start of the week. +The week-based year may differ from the calendar year for dates near the start or end of the year. +-/ +@[inline] +def weekYear (date : DateTime tz) (firstDay : Weekday := .monday) : Year.Offset := + date.date.get.weekYear firstDay + /-- Returns the unaligned week of the month for a `DateTime` (day divided by 7, plus 1). -/ diff --git a/src/Std/Time/Zoned/ZonedDateTime.lean b/src/Std/Time/Zoned/ZonedDateTime.lean index 19eeda40d356..34a939d8e6db 100644 --- a/src/Std/Time/Zoned/ZonedDateTime.lean +++ b/src/Std/Time/Zoned/ZonedDateTime.lean @@ -230,6 +230,14 @@ Determines the week of the year for the given `ZonedDateTime`, using `firstDay` def weekOfYear (date : ZonedDateTime) (firstDay : Weekday := .monday) : Week.Ordinal := date.date.get.weekOfYear firstDay +/-- +Returns the week-based year for the given `ZonedDateTime`, using `firstDay` as the start of the week. +The week-based year may differ from the calendar year for dates near the start or end of the year. +-/ +@[inline] +def weekYear (date : ZonedDateTime) (firstDay : Weekday := .monday) : Year.Offset := + date.date.get.weekYear firstDay + /-- Returns the unaligned week of the month for a `ZonedDateTime` (day divided by 7, plus 1). -/ diff --git a/tests/elab/timeFormats.lean b/tests/elab/timeFormats.lean index 5a8cf3f56d46..82037c99d8e2 100644 --- a/tests/elab/timeFormats.lean +++ b/tests/elab/timeFormats.lean @@ -357,7 +357,7 @@ info: "Sun Sun Sun Sunday S" #eval zoned₄.format "E EE EEE EEEE EEEEE" /-- -info: "7 07 Sun Sunday S" +info: "1 01 Sun Sunday S" -/ #guard_msgs in #eval zoned₄.format "e ee eee eeee eeeee" @@ -536,13 +536,13 @@ info: "3 03 3rd quarter 3" #eval datetime₄.format "Q QQ QQQQ QQQQQ" /-- -info: "28 28 028 0028" +info: "29 29 029 0029" -/ #guard_msgs in #eval datetime₄.format "w ww www wwww" /-- -info: "2 02 002 0002" +info: "3 03 003 0003" -/ #guard_msgs in #eval datetime₄.format "W WW WWW WWWW" @@ -554,7 +554,7 @@ info: "Sun Sun Sun Sunday S" #eval datetime₄.format "E EE EEE EEEE EEEEE" /-- -info: "7 07 Sun Sunday S" +info: "1 01 Sun Sunday S" -/ #guard_msgs in #eval datetime₄.format "e ee eee eeee eeeee" @@ -752,13 +752,13 @@ info: "3 03 3rd quarter 3" #eval date₄.format "Q QQ QQQQ QQQQQ" /-- -info: "28 28 028 0028" +info: "29 29 029 0029" -/ #guard_msgs in #eval date₄.format "w ww www wwww" /-- -info: "2 02 002 0002" +info: "3 03 003 0003" -/ #guard_msgs in #eval date₄.format "W WW WWW WWWW" @@ -770,7 +770,7 @@ info: "Sun Sun Sun Sunday S" #eval date₄.format "E EE EEE EEEE EEEEE" /-- -info: "7 07 Sun Sunday S" +info: "1 01 Sun Sunday S" -/ #guard_msgs in #eval date₄.format "e ee eee eeee eeeee"