lean4-htt/src/Std/Time/Format/Basic.lean
Sofia Rodrigues d95a2ee35e
feat: add FormatConfig for GenericFormat with leap second validation (#7584)
This PR introduces a structure called `FormatConfig`, which provides
additional configuration options for `GenericFormat`, such as whether
leap seconds should be allowed during parsing. By default, this option
is set to `false`.

This PR also fixes certain flaws to make the implementation less
permissive by:

- Disallowing the final leap second, such as `2016-12-31T23:59:60Z`,
when `allowLeapSeconds = false`.
- Disallowing invalid leap seconds, such as `2017-06-30T23:59:60Z`, when
`allowLeapSeconds = false`.
- Disallowing leap-minute time zones, such as
`2016-12-31T00:00:00+2360`, and out-of-range time zones, such as
`2016-12-31T00:00:00+2490`.

These changes ensure that Lean aligns with TypeScript's behavior, as
outlined in this table:
https://github.com/cedar-policy/cedar-spec/pull/519#issuecomment-2613547897.
2025-03-27 13:25:23 +00:00

1532 lines
46 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/-
Copyright (c) 2024 Lean FRO, LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Sofia Rodrigues
-/
prelude
import Std.Internal.Parsec
import Std.Time.Date
import Std.Time.Time
import Std.Time.Zoned
import Std.Time.DateTime
/-!
This module defines the `Formatter` types. It is based on the Java's `DateTimeFormatter` format.
-/
namespace Std
namespace Time
open Internal
open Std.Internal.Parsec.String
open Std.Internal.Parsec Lean PlainTime PlainDate TimeZone DateTime
set_option linter.all true
/--
`Text` represents different text formatting styles.
-/
inductive Text
/-- Short form (e.g., "Tue") -/
| short
/-- Full form (e.g., "Tuesday") -/
| full
/-- Narrow form (e.g., "T") -/
| narrow
deriving Repr, Inhabited
namespace Text
/--
`classify` classifies the number of pattern letters into a `Text` type.
-/
def classify (num : Nat) : Option Text :=
if num < 4 then
some (.short)
else if num = 4 then
some (.full)
else if num = 5 then
some (.narrow)
else
none
end Text
/--
`Number` represents different number formatting styles.
-/
structure Number where
/--
The number of digits to pad, based on the count of pattern letters.
-/
padding : Nat
deriving Repr, Inhabited
/--
`classifyNumberText` classifies the number of pattern letters into either a `Number` or `Text`.
-/
def classifyNumberText : Nat → Option (Number ⊕ Text)
| n => if n < 3 then some (.inl ⟨n⟩) else .inr <$> (Text.classify n)
/--
`Fraction` represents the fraction of a second, which can either be full nanoseconds
or a truncated form with fewer digits.
-/
inductive Fraction
/-- Nanosecond precision (up to 9 digits) -/
| nano
/-- Fewer digits (truncated precision) -/
| truncated (digits : Nat)
deriving Repr, Inhabited
namespace Fraction
/--
`classify` classifies the number of pattern letters into either a `Fraction`. It's used for `nano`.
-/
def classify (nat : Nat) : Option Fraction :=
if nat < 9 then
some (.truncated nat)
else if nat = 9 then
some (.nano)
else
none
end Fraction
/--
`Year` represents different year formatting styles based on the number of pattern letters.
-/
inductive Year
/-- Any size (e.g., "19000000000000") -/
| any
/-- Two-digit year format (e.g., "23" for 2023) -/
| twoDigit
/-- Four-digit year format (e.g., "2023") -/
| fourDigit
/-- Extended year format for more than 4 digits (e.g., "002023") -/
| extended (num : Nat)
deriving Repr, Inhabited
namespace Year
/--
`classify` classifies the number of pattern letters into a `Year` format.
-/
def classify (num : Nat) : Option Year :=
if num = 1 then
some .any
else if num = 2 then
some .twoDigit
else if num = 4 then
some .fourDigit
else if num > 4 num = 3 then
some (.extended num)
else
none
end Year
/--
`ZoneId` represents different time zone ID formats based on the number of pattern letters.
-/
inductive ZoneId
/-- Short form of time zone (e.g., "PST") -/
| short
/-- Full form of time zone (e.g., "Pacific Standard Time") -/
| full
deriving Repr, Inhabited
namespace ZoneId
/--
`classify` classifies the number of pattern letters into a `ZoneId` format.
- If 2 letters, it returns the short form.
- If 4 letters, it returns the full form.
- Otherwise, it returns none.
-/
def classify (num : Nat) : Option ZoneId :=
if num = 2 then
some (.short)
else if num = 4 then
some (.full)
else
none
end ZoneId
/--
`ZoneName` represents different zone name formats based on the number of pattern letters and
whether daylight saving time is considered.
-/
inductive ZoneName
/-- Short form of zone name (e.g., "PST") -/
| short
/-- Full form of zone name (e.g., "Pacific Standard Time") -/
| full
deriving Repr, Inhabited
namespace ZoneName
/--
`classify` classifies the number of pattern letters and the letter type ('z' or 'v')
into a `ZoneName` format.
- For 'z', if less than 4 letters, it returns the short form; if 4 letters, it returns the full form.
- For 'v', if 1 letter, it returns the short form; if 4 letters, it returns the full form.
- Otherwise, it returns none.
-/
def classify (letter : Char) (num : Nat) : Option ZoneName :=
if letter = 'z' then
if num < 4 then
some (.short)
else if num = 4 then
some (.full)
else
none
else if letter = 'v' then
if num = 1 then
some (.short)
else if num = 4 then
some (.full)
else
none
else
none
end ZoneName
/--
`OffsetX` represents different offset formats based on the number of pattern letters.
The output will vary between the number of pattern letters, whether it's the hour, minute, second,
and whether colons are used.
-/
inductive OffsetX
/-- Only the hour is output (e.g., "+01") -/
| hour
/-- Hour and minute without colon (e.g., "+0130") -/
| hourMinute
/-- Hour and minute with colon (e.g., "+01:30") -/
| hourMinuteColon
/-- Hour, minute, and second without colon (e.g., "+013015") -/
| hourMinuteSecond
/-- Hour, minute, and second with colon (e.g., "+01:30:15") -/
| hourMinuteSecondColon
deriving Repr, Inhabited
namespace OffsetX
/--
`classify` classifies the number of pattern letters into an `OffsetX` format.
-/
def classify (num : Nat) : Option OffsetX :=
if num = 1 then
some (.hour)
else if num = 2 then
some (.hourMinute)
else if num = 3 then
some (.hourMinuteColon)
else if num = 4 then
some (.hourMinuteSecond)
else if num = 5 then
some (.hourMinuteSecondColon)
else
none
end OffsetX
/--
`OffsetO` represents localized offset text formats based on the number of pattern letters.
-/
inductive OffsetO
/-- Short form of the localized offset (e.g., "GMT+8") -/
| short
/-- Full form of the localized offset (e.g., "GMT+08:00") -/
| full
deriving Repr, Inhabited
namespace OffsetO
/--
`classify` classifies the number of pattern letters into an `OffsetO` format.
-/
def classify (num : Nat) : Option OffsetO :=
match num with
| 1 => some (.short)
| 4 => some (.full)
| _ => none
end OffsetO
/--
`OffsetZ` represents different offset formats based on the number of pattern letters (capital 'Z').
-/
inductive OffsetZ
/-- Hour and minute without colon (e.g., "+0130") -/
| hourMinute
/-- Localized offset text in full form (e.g., "GMT+08:00") -/
| full
/-- Hour, minute, and second with colon (e.g., "+01:30:15") -/
| hourMinuteSecondColon
deriving Repr, Inhabited
namespace OffsetZ
/--
`classify` classifies the number of pattern letters into an `OffsetZ` format.
-/
def classify (num : Nat) : Option OffsetZ :=
match num with
| 1 | 2 | 3 => some (.hourMinute)
| 4 => some (.full)
| 5 => some (.hourMinuteSecondColon)
| _ => none
end OffsetZ
/--
The `Modifier` inductive type represents various formatting options for date and time components,
matching the format symbols used in date and time strings.
These modifiers can be applied in formatting functions to generate custom date and time outputs.
-/
inductive Modifier
/--
`G`: Era (e.g., AD, Anno Domini, A).
-/
| G (presentation : Text)
/--
`y`: Year of era (e.g., 2004, 04, 0002, 2).
-/
| y (presentation : Year)
/--
`u`: Year (e.g., 2004, 04, -0001, -1).
-/
| u (presentation : Year)
/--
`D`: Day of year (e.g., 189).
-/
| D (presentation : Number)
/--
`M`: Month of year as number or text (e.g., 7, 07, Jul, July, J).
-/
| MorL (presentation : Number ⊕ Text)
/--
`d`: Day of month (e.g., 10).
-/
| d (presentation : Number)
/--
`Q`: Quarter of year as number or text (e.g., 3, 03, Q3, 3rd quarter).
-/
| Qorq (presentation : Number ⊕ Text)
/--
`w`: Week of week-based year (e.g., 27).
-/
| w (presentation : Number)
/--
`W`: Week of month (e.g., 4).
-/
| W (presentation : Number)
/--
`E`: Day of week as text (e.g., Tue, Tuesday, T).
-/
| E (presentation : Text)
/--
`e`: Localized day of week as number or text (e.g., 2, 02, Tue, Tuesday, T).
-/
| eorc (presentation : Number ⊕ Text)
/--
`F`: Aligned week of month (e.g., 3).
-/
| F (presentation : Number)
/--
`a`: AM/PM of day (e.g., PM).
-/
| a (presentation : Text)
/--
`h`: Clock hour of AM/PM (1-12) (e.g., 12).
-/
| h (presentation : Number)
/--
`K`: Hour of AM/PM (0-11) (e.g., 0).
-/
| K (presentation : Number)
/--
`k`: Clock hour of day (1-24) (e.g., 24).
-/
| k (presentation : Number)
/--
`H`: Hour of day (0-23) (e.g., 0).
-/
| H (presentation : Number)
/--
`m`: Minute of hour (e.g., 30).
-/
| m (presentation : Number)
/--
`s`: Second of minute (e.g., 55).
-/
| s (presentation : Number)
/--
`S`: Fraction of second (e.g., 978).
-/
| S (presentation : Fraction)
/--
`A`: Millisecond of day (e.g., 1234).
-/
| A (presentation : Number)
/--
`n`: Nanosecond of second (e.g., 987654321).
-/
| n (presentation : Number)
/--
`N`: Nanosecond of day (e.g., 1234000000).
-/
| N (presentation : Number)
/--
`V`: Time zone ID (e.g., America/Los_Angeles, Z, -08:30).
-/
| V
/--
`z`: Time zone name (e.g., Pacific Standard Time, PST).
-/
| z (presentation : ZoneName)
/--
`O`: Localized zone offset (e.g., GMT+8, GMT+08:00, UTC-08:00).
-/
| O (presentation : OffsetO)
/--
`X`: Zone offset with 'Z' for zero (e.g., Z, -08, -0830, -08:30).
-/
| X (presentation : OffsetX)
/--
`x`: Zone offset without 'Z' (e.g., +0000, -08, -0830, -08:30).
-/
| x (presentation : OffsetX)
/--
`Z`: Zone offset with 'Z' for UTC (e.g., +0000, -0800, -08:00).
-/
| Z (presentation : OffsetZ)
deriving Repr, Inhabited
/--
`abstractParse` abstracts the parsing logic for any type that has a classify function.
It takes a constructor function to build the `Modifier` and a classify function that maps the pattern length to a specific type.
-/
private def parseMod (constructor : α → Modifier) (classify : Nat → Option α) (p : String) : Parser Modifier :=
let len := p.length
match classify len with
| some res => pure (constructor res)
| none => fail s!"invalid quantity of characters for '{p.get 0}'"
private def parseText (constructor : Text → Modifier) (p : String) : Parser Modifier :=
parseMod constructor Text.classify p
private def parseFraction (constructor : Fraction → Modifier) (p : String) : Parser Modifier :=
parseMod constructor Fraction.classify p
private def parseNumber (constructor : Number → Modifier) (p : String) : Parser Modifier :=
pure (constructor ⟨p.length⟩)
private def parseYear (constructor : Year → Modifier) (p : String) : Parser Modifier :=
parseMod constructor Year.classify p
private def parseOffsetX (constructor : OffsetX → Modifier) (p : String) : Parser Modifier :=
parseMod constructor OffsetX.classify p
private def parseOffsetZ (constructor : OffsetZ → Modifier) (p : String) : Parser Modifier :=
parseMod constructor OffsetZ.classify p
private def parseOffsetO (constructor : OffsetO → Modifier) (p : String) : Parser Modifier :=
parseMod constructor OffsetO.classify p
private def parseZoneId (p : String) : Parser Modifier :=
if p.length = 2 then pure .V else fail s!"invalid quantity of characters for '{p.get 0}'"
private def parseNumberText (constructor : (Number ⊕ Text) → Modifier) (p : String) : Parser Modifier :=
parseMod constructor classifyNumberText p
private def parseZoneName (constructor : ZoneName → Modifier) (p : String) : Parser Modifier :=
let len := p.length
match ZoneName.classify (p.get 0) len with
| some res => pure (constructor res)
| none => fail s!"invalid quantity of characters for '{p.get 0}'"
private def parseModifier : Parser Modifier
:= (parseText Modifier.G =<< many1Chars (pchar 'G'))
<|> parseYear Modifier.y =<< many1Chars (pchar 'y')
<|> parseYear Modifier.u =<< many1Chars (pchar 'u')
<|> parseNumber Modifier.D =<< many1Chars (pchar 'D')
<|> parseNumberText Modifier.MorL =<< many1Chars (pchar 'M')
<|> parseNumberText Modifier.MorL =<< many1Chars (pchar 'L')
<|> parseNumber Modifier.d =<< many1Chars (pchar 'd')
<|> parseNumberText Modifier.Qorq =<< many1Chars (pchar 'Q')
<|> parseNumberText Modifier.Qorq =<< many1Chars (pchar 'q')
<|> parseNumber Modifier.w =<< many1Chars (pchar 'w')
<|> parseNumber Modifier.W =<< many1Chars (pchar 'W')
<|> parseText Modifier.E =<< many1Chars (pchar 'E')
<|> parseNumberText Modifier.eorc =<< many1Chars (pchar 'e')
<|> parseNumberText Modifier.eorc =<< many1Chars (pchar 'c')
<|> parseNumber Modifier.F =<< many1Chars (pchar 'F')
<|> parseText Modifier.a =<< many1Chars (pchar 'a')
<|> parseNumber Modifier.h =<< many1Chars (pchar 'h')
<|> parseNumber Modifier.K =<< many1Chars (pchar 'K')
<|> parseNumber Modifier.k =<< many1Chars (pchar 'k')
<|> parseNumber Modifier.H =<< many1Chars (pchar 'H')
<|> parseNumber Modifier.m =<< many1Chars (pchar 'm')
<|> parseNumber Modifier.s =<< many1Chars (pchar 's')
<|> parseFraction Modifier.S =<< many1Chars (pchar 'S')
<|> parseNumber Modifier.A =<< many1Chars (pchar 'A')
<|> parseNumber Modifier.n =<< many1Chars (pchar 'n')
<|> parseNumber Modifier.N =<< many1Chars (pchar 'N')
<|> parseZoneId =<< many1Chars (pchar 'V')
<|> parseZoneName Modifier.z =<< many1Chars (pchar 'z')
<|> parseOffsetO Modifier.O =<< many1Chars (pchar 'O')
<|> parseOffsetX Modifier.X =<< many1Chars (pchar 'X')
<|> parseOffsetX Modifier.x =<< many1Chars (pchar 'x')
<|> parseOffsetZ Modifier.Z =<< many1Chars (pchar 'Z')
/--
The part of a formatting string. A string is just a text and a modifier is in the format described in
the `Modifier` type.
-/
inductive FormatPart
/--
A string literal.
-/
| string (val : String)
/--
A modifier that renders some data into text.
-/
| modifier (modifier : Modifier)
deriving Repr
instance : Coe String FormatPart where
coe := .string
instance : Coe Modifier FormatPart where
coe := .modifier
/--
The format of date and time string.
-/
abbrev FormatString := List FormatPart
/--
If the format is aware of some timezone data it parses or if it parses any timezone.
-/
inductive Awareness
/-- The format only parses a single timezone. -/
| only : TimeZone → Awareness
/-- The format parses any timezone. -/
| any
namespace Awareness
instance : Coe TimeZone Awareness where
coe := .only
@[simp]
private def type (x : Awareness) : Type :=
match x with
| .any => ZonedDateTime
| .only tz => DateTime tz
instance : Inhabited (type aw) where
default := by
simp [type]
split <;> exact Inhabited.default
private def getD (x : Awareness) (default : TimeZone) : TimeZone :=
match x with
| .any => default
| .only tz => tz
end Awareness
/--
Configuration options for formatting and parsing date/time strings.
-/
structure FormatConfig where
/--
Whether to allow leap seconds, such as `2016-12-31T23:59:60Z`.
Default is `false`.
-/
allowLeapSeconds : Bool := false
deriving Inhabited, Repr
/--
A specification on how to format a data or parse some string.
-/
structure GenericFormat (awareness : Awareness) where
/--
Configuration options for formatting behavior.
-/
config : FormatConfig
/--
The format string used for parsing and formatting.
-/
string : FormatString
deriving Inhabited, Repr
private def parseFormatPart : Parser FormatPart
:= (.modifier <$> parseModifier)
<|> (pchar '\\') *> any <&> (.string ∘ toString)
<|> (pchar '\"' *> many1Chars (satisfy (· ≠ '\"')) <* pchar '\"') <&> .string
<|> (pchar '\'' *> many1Chars (satisfy (· ≠ '\'')) <* pchar '\'') <&> .string
<|> many1Chars (satisfy (fun x => ¬Char.isAlpha x ∧ x ≠ '\'' ∧ x ≠ '\"')) <&> .string
private def specParser : Parser FormatString :=
(Array.toList <$> many parseFormatPart) <* eof
private def specParse (s : String) : Except String FormatString :=
specParser.run s
-- Pretty printer
private def leftPad (n : Nat) (a : Char) (s : String) : String :=
"".pushn a (n - s.length) ++ s
private def rightPad (n : Nat) (a : Char) (s : String) : String :=
s ++ "".pushn a (n - s.length)
private def pad (size : Nat) (n : Int) (cut : Bool := false) : String :=
let (sign, n) := if n < 0 then ("-", -n) else ("", n)
let numStr := toString n
if numStr.length > size then
sign ++ if cut then numStr.drop (numStr.length - size) else numStr
else
sign ++ leftPad size '0' numStr
private def rightTruncate (size : Nat) (n : Int) (cut : Bool := false) : String :=
let (sign, n) := if n < 0 then ("-", -n) else ("", n)
let numStr := toString n
if numStr.length > size then
sign ++ if cut then numStr.take size else numStr
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 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 :=
match marker with
| .am => "AM"
| .pm => "PM"
private def formatMarkerLong (marker : HourMarker) : String :=
match marker with
| .am => "Ante Meridiem"
| .pm => "Post Meridiem"
private def formatMarkerNarrow (marker : HourMarker) : String :=
match marker with
| .am => "A"
| .pm => "P"
private def toSigned (data : Int) : String :=
if data < 0 then toString data else "+" ++ toString data
private def toIsoString (offset : Offset) (withMinutes : Bool) (withSeconds : Bool) (colon : Bool) : String :=
let (sign, time) := if offset.second.val ≥ 0 then ("+", offset.second) else ("-", -offset.second)
let time := PlainTime.ofSeconds time
let pad := leftPad 2 '0' ∘ toString
let data := s!"{sign}{pad time.hour.val}"
let data := if withMinutes then s!"{data}{if colon then ":" else ""}{pad time.minute.val}" else data
let data := if withSeconds ∧ time.second.val ≠ 0 then s!"{data}{if colon then ":" else ""}{pad time.second.val}" else data
data
private def TypeFormat : Modifier → Type
| .G _ => Year.Era
| .y _ => Year.Offset
| .u _ => Year.Offset
| .D _ => Sigma Day.Ordinal.OfYear
| .MorL _ => Month.Ordinal
| .d _ => Day.Ordinal
| .Qorq _ => Month.Quarter
| .w _ => Week.Ordinal
| .W _ => Week.Ordinal.OfMonth
| .E _ => Weekday
| .eorc _ => Weekday
| .F _ => Bounded.LE 1 5
| .a _ => HourMarker
| .h _ => Bounded.LE 1 12
| .K _ => Bounded.LE 0 11
| .k _ => Bounded.LE 1 24
| .H _ => Hour.Ordinal
| .m _ => Minute.Ordinal
| .s _ => Second.Ordinal true
| .S _ => Nanosecond.Ordinal
| .A _ => Millisecond.Offset
| .n _ => Nanosecond.Ordinal
| .N _ => Nanosecond.Offset
| .V => String
| .z _ => String
| .O _ => Offset
| .X _ => Offset
| .x _ => Offset
| .Z _ => Offset
private def formatWith (modifier : Modifier) (data: TypeFormat modifier) : String :=
match modifier with
| .G format =>
match format with
| .short => formatEraShort data
| .full => formatEraLong data
| .narrow => formatEraNarrow 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)
| .twoDigit => pad 2 (info % 100)
| .fourDigit => pad 4 info
| .extended n => pad n info
| .u format =>
match format with
| .any => pad 0 (data.toInt)
| .twoDigit => pad 2 (data.toInt % 100)
| .fourDigit => pad 4 data.toInt
| .extended n => pad n data.toInt
| .D format =>
pad format.padding data.snd.val
| .MorL format =>
match format with
| .inl format => pad format.padding data.val
| .inr .short => formatMonthShort data
| .inr .full => formatMonthLong data
| .inr .narrow => formatMonthNarrow 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 .narrow => formatQuarterNumber data
| .w format =>
pad format.padding data.val
| .W format =>
pad format.padding data.val
| .E format =>
match format with
| .short => formatWeekdayShort data
| .full => formatWeekdayLong data
| .narrow => formatWeekdayNarrow 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
| .F format =>
pad format.padding data.val
| .a format =>
match format with
| .short => formatMarkerShort data
| .full => formatMarkerLong data
| .narrow => formatMarkerNarrow data
| .h format => pad format.padding (data.val % 12)
| .K format => pad format.padding (data.val % 12)
| .k format => pad format.padding data.val
| .H format => pad format.padding data.val
| .m format => pad format.padding data.val
| .s format => pad format.padding data.val
| .S format =>
match format with
| .nano => pad 9 data.val
| .truncated n => rightTruncate n data.val (cut := true)
| .A format =>
pad format.padding data.val
| .n format =>
pad format.padding data.val
| .N format =>
pad format.padding data.val
| .V => data
| .z format =>
match format with
| .short => data
| .full => data
| .O format =>
match format with
| .short => s!"GMT{toSigned data.second.toHours.toInt}"
| .full => s!"GMT{toIsoString data true false true}"
| .X format =>
if data.second == 0 then
"Z"
else
match format with
| .hour => toIsoString data false false false
| .hourMinute => toIsoString data true false false
| .hourMinuteColon => toIsoString data true false true
| .hourMinuteSecond => toIsoString data true true false
| .hourMinuteSecondColon => toIsoString data true true true
| .x format =>
match format with
| .hour =>
toIsoString data (data.second.toMinutes.val % 60 ≠ 0) false false
| .hourMinute =>
toIsoString data true false false
| .hourMinuteColon =>
toIsoString data true (data.second.val % 60 ≠ 0) true
| .hourMinuteSecond =>
toIsoString data true (data.second.val % 60 ≠ 0) false
| .hourMinuteSecondColon =>
toIsoString data true true true
| .Z format =>
match format with
| .hourMinute =>
toIsoString data true false false
| .full =>
if data.second.val = 0
then "GMT"
else s!"GMT{toIsoString data true false true}"
| .hourMinuteSecondColon =>
if data.second == 0
then "Z"
else toIsoString data true (data.second.val % 60 ≠ 0) true
private def dateFromModifier (date : DateTime tz) : TypeFormat modifier :=
match modifier with
| .G _ => date.era
| .y _ => date.year
| .u _ => date.year
| .D _ => Sigma.mk _ date.dayOfYear
| .MorL _ => date.month
| .d _ => date.day
| .Qorq _ => date.quarter
| .w _ => date.weekOfYear
| .W _ => date.alignedWeekOfMonth
| .E _ => date.weekday
| .eorc _ => date.weekday
| .F _ => date.weekOfMonth
| .a _ => HourMarker.ofOrdinal date.hour
| .h _ => HourMarker.toRelative date.hour |>.fst
| .K _ => date.hour.emod 12 (by decide)
| .k _ => date.hour.shiftTo1BasedHour
| .H _ => date.hour
| .m _ => date.minute
| .s _ => date.date.get.time.second
| .S _ => date.nanosecond
| .A _ => date.date.get.time.toMilliseconds
| .n _ => date.nanosecond
| .N _ => date.date.get.time.toNanoseconds
| .V => tz.name
| .z .short => tz.abbreviation
| .z .full => tz.name
| .O _ => tz.offset
| .X _ => tz.offset
| .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⟩
/--
Parses a short value of a `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
private def parseQuarterNumber : Parser Month.Quarter
:= pstring "1" *> pure ⟨1, by decide⟩
<|> pstring "2" *> pure ⟨2, by decide⟩
<|> 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 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 parseMarkerShort : Parser HourMarker
:= pstring "AM" *> pure HourMarker.am
<|> pstring "PM" *> pure HourMarker.pm
private def parseMarkerLong : Parser HourMarker
:= pstring "Ante Meridiem" *> pure HourMarker.am
<|> pstring "Post Meridiem" *> pure HourMarker.pm
private def parseMarkerNarrow : Parser HourMarker
:= pstring "A" *> pure HourMarker.am
<|> pstring "P" *> pure HourMarker.pm
private def exactly (parse : Parser α) (size : Nat) : Parser (Array α) :=
let rec go (acc : Array α) (count : Nat) : Parser (Array α) :=
if count ≥ size then
pure acc
else do
let res ← parse
go (acc.push res) count.succ
termination_by size - count
go #[] 12
private def exactlyChars (parse : Parser Char) (size : Nat) : Parser String :=
let rec go (acc : String) (count : Nat) : Parser String :=
if count ≥ size then
pure acc
else do
let res ← parse
go (acc.push res) count.succ
termination_by size - count
go "" 0
private def parseSigned (parser : Parser Nat) : Parser Int := do
let signed ← optional (pstring "-")
let res ← parser
return if signed.isSome then -res else res
private def parseNum (size : Nat) : Parser Nat :=
String.toNat! <$> exactlyChars (satisfy Char.isDigit) size
private def parseAtLeastNum (size : Nat) : Parser Nat :=
String.toNat! <$> do
let start ← exactlyChars (satisfy Char.isDigit) size
let end_ ← manyChars (satisfy Char.isDigit)
pure (start ++ end_)
private def parseFlexibleNum (size : Nat) : Parser Nat :=
if size = 1 then parseAtLeastNum 1 else parseNum size
private def parseFractionNum (size : Nat) (pad : Nat) : Parser Nat :=
String.toNat! <$> rightPad pad '0' <$> exactlyChars (satisfy Char.isDigit) size
private def parseIdentifier : Parser String :=
many1Chars (satisfy (fun x => x.isAlpha x.isDigit x = '_' x = '-' x = '/'))
private def parseNatToBounded { n m : Nat } (parser : Parser Nat) : Parser (Bounded.LE n m) := do
let res ← parser
if h : n ≤ res ∧ res ≤ m then
return Bounded.LE.ofNat' res h
else
fail s!"need a natural number in the interval of {n} to {m}"
private inductive Reason
| yes
| no
| optional
private def parseOffset (withMinutes : Reason) (withSeconds : Reason) (withColon : Bool) : Parser Offset := do
let sign ← (pchar '+' *> pure 1) <|> (pchar '-' *> pure (-1))
let hours : Hour.Offset ← UnitVal.ofInt <$> parseNum 2
if hours.val < 0 hours.val > 23 then
fail s!"invalid hour offset: {hours.val}. Must be between 0 and 23."
let colon := if withColon then pchar ':' else pure ':'
let parseUnit {n} (reason : Reason) : Parser (Option (UnitVal n)) :=
match reason with
| .yes => some <$> (colon *> UnitVal.ofInt <$> parseNum 2)
| .no => pure none
| .optional => optional (colon *> UnitVal.ofInt <$> parseNum 2)
let minutes : Option Minute.Offset ← parseUnit withMinutes
if let some m := minutes then
if m.val > 59 then
fail s!"invalid minute offset: {m.val}. Must be between 0 and 59."
let seconds : Option Second.Offset ← parseUnit withSeconds
if let some s := seconds then
if s.val > 59 then
fail s!"invalid second offset: {s.val}. Must be between 0 and 59."
let hours := hours.toSeconds + (minutes.getD 0).toSeconds + (seconds.getD 0)
return Offset.ofSeconds ⟨hours.val * sign⟩
private def parseWith (config : FormatConfig) : (mod : Modifier) → Parser (TypeFormat mod)
| .G format =>
match format with
| .short => parseEraShort
| .full => parseEraLong
| .narrow => parseEraNarrow
| .y format =>
match format with
| .any => Int.ofNat <$> parseAtLeastNum 1
| .twoDigit => (2000 + ·) <$> Int.ofNat <$> parseNum 2
| .fourDigit => Int.ofNat <$> parseNum 4
| .extended n => Int.ofNat <$> parseNum n
| .u 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
| .inl format => parseNatToBounded (parseFlexibleNum format.padding)
| .inr .short => parseMonthShort
| .inr .full => parseMonthLong
| .inr .narrow => parseMonthNarrow
| .d format => parseNatToBounded (parseFlexibleNum format.padding)
| .Qorq format =>
match format with
| .inl format => parseNatToBounded (parseFlexibleNum format.padding)
| .inr .short => parseQuarterShort
| .inr .full => parseQuarterLong
| .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
| .eorc format =>
match format with
| .inl format => Weekday.ofOrdinal <$> parseNatToBounded (parseFlexibleNum format.padding)
| .inr .short => parseWeekdayShort
| .inr .full => parseWeekdayLong
| .inr .narrow => parseWeekdayNarrow
| .F format => parseNatToBounded (parseFlexibleNum format.padding)
| .a format =>
match format with
| .short => parseMarkerShort
| .full => parseMarkerLong
| .narrow => parseMarkerNarrow
| .h format => parseNatToBounded (parseFlexibleNum format.padding)
| .K format => parseNatToBounded (parseFlexibleNum format.padding)
| .k format => parseNatToBounded (parseFlexibleNum format.padding)
| .H format => parseNatToBounded (parseFlexibleNum format.padding)
| .m format => parseNatToBounded (parseFlexibleNum format.padding)
| .s format =>
if config.allowLeapSeconds then
parseNatToBounded (parseFlexibleNum format.padding)
else do
let res : Bounded.LE 0 59 ← parseNatToBounded (parseFlexibleNum format.padding)
return res.expandTop (by decide)
| .S format =>
match format with
| .nano => parseNatToBounded (parseFlexibleNum 9)
| .truncated n => parseNatToBounded (parseFractionNum n 9)
| .A format => Millisecond.Offset.ofNat <$> (parseFlexibleNum format.padding)
| .n format => parseNatToBounded (parseFlexibleNum format.padding)
| .N format => Nanosecond.Offset.ofNat <$> (parseFlexibleNum format.padding)
| .V => parseIdentifier
| .z format =>
match format with
| .short => parseIdentifier
| .full => parseIdentifier
| .O format =>
match format with
| .short => pstring "GMT" *> parseOffset .no .no false
| .full => pstring "GMT" *> parseOffset .yes .optional false
| .X format =>
let p : Parser Offset :=
match format with
| .hour => parseOffset .no .no false
| .hourMinute => parseOffset .yes .no false
| .hourMinuteColon => parseOffset .yes .no true
| .hourMinuteSecond => parseOffset .yes .yes false
| .hourMinuteSecondColon => parseOffset .yes .yes true
p <|> (pstring "Z" *> pure (Offset.ofSeconds 0))
| .x format =>
match format with
| .hour =>
parseOffset .optional .no false
| .hourMinute =>
parseOffset .yes .no false
| .hourMinuteColon =>
parseOffset .yes .optional true
| .hourMinuteSecond =>
parseOffset .yes .optional false
| .hourMinuteSecondColon =>
parseOffset .yes .yes true
| .Z format =>
match format with
| .hourMinute =>
parseOffset .yes .no false
| .full => do
skipString "GMT"
let res ← optional (parseOffset .yes .no true)
return res.getD Offset.zero
| .hourMinuteSecondColon =>
(skipString "Z" *> pure Offset.zero)
<|> (parseOffset .yes .optional true)
private def formatPartWithDate (date : DateTime tz) (part : FormatPart) : String :=
match part with
| .modifier mod => formatWith mod (dateFromModifier date)
| .string s => s
@[simp]
private def FormatType (result : Type) : FormatString → Type
| .modifier entry :: xs => (TypeFormat entry) → (FormatType result xs)
| .string _ :: xs => (FormatType result xs)
| [] => result
namespace GenericFormat
private structure DateBuilder where
G : Option Year.Era := none
y : Option Year.Offset := none
u : Option Year.Offset := none
D : Option (Sigma Day.Ordinal.OfYear) := none
MorL : Option Month.Ordinal := none
d : Option Day.Ordinal := none
Qorq : Option Month.Quarter := none
w : Option Week.Ordinal := none
W : Option Week.Ordinal.OfMonth := none
E : Option Weekday := none
eorc : Option Weekday := none
F : Option (Bounded.LE 1 5) := none
a : Option HourMarker := none
h : Option (Bounded.LE 1 12) := none
K : Option (Bounded.LE 0 11) := none
k : Option (Bounded.LE 1 24) := none
H : Option Hour.Ordinal := none
m : Option Minute.Ordinal := none
s : Option (Second.Ordinal true) := none
S : Option Nanosecond.Ordinal := none
A : Option Millisecond.Offset := none
n : Option Nanosecond.Ordinal := none
N : Option Nanosecond.Offset := none
V : Option String := none
z : Option String := none
zabbrev : Option String := none
O : Option Offset := none
X : Option Offset := none
x : Option Offset := none
Z : Option Offset := none
namespace DateBuilder
private def insert (date : DateBuilder) (modifier : Modifier) (data : TypeFormat modifier) : DateBuilder :=
match modifier with
| .G _ => { date with G := some data }
| .y _ => { date with y := some data }
| .u _ => { date with u := some data }
| .D _ => { date with D := some data }
| .MorL _ => { date with MorL := some data }
| .d _ => { date with d := some data }
| .Qorq _ => { date with Qorq := some data }
| .w _ => { date with w := some data }
| .W _ => { date with W := some data }
| .E _ => { date with E := some data }
| .eorc _ => { date with eorc := some data }
| .F _ => { date with F := some data }
| .a _ => { date with a := some data }
| .h _ => { date with h := some data }
| .K _ => { date with K := some data }
| .k _ => { date with k := some data }
| .H _ => { date with H := some data }
| .m _ => { date with m := some data }
| .s _ => { date with s := some data }
| .S _ => { date with S := some data }
| .A _ => { date with A := some data }
| .n _ => { date with n := some data }
| .N _ => { date with N := some data }
| .V => { date with V := some data }
| .z .full => { date with z := some data }
| .z .short => { date with zabbrev := some data }
| .O _ => { date with O := some data }
| .X _ => { date with X := some data }
| .x _ => { date with x := some data }
| .Z _ => { date with Z := some data }
private def convertYearAndEra (year : Year.Offset) : Year.Era → Year.Offset
| .ce => year
| .bce => -(year + 1)
private def build (builder : DateBuilder) (aw : Awareness) : Option aw.type :=
let offset := builder.O <|> builder.X <|> builder.x <|> builder.Z |>.getD Offset.zero
let tz : TimeZone := {
offset,
name := builder.V <|> builder.z |>.getD (offset.toIsoString true),
abbreviation := builder.zabbrev |>.getD (offset.toIsoString true),
isDST := false,
}
let month := builder.MorL |>.getD 0
let day := builder.d |>.getD 0
let era := (builder.G.getD .ce)
let year
:= builder.u
<|> ((convertYearAndEra · era) <$> builder.y)
|>.getD 0
let hour : Option (Bounded.LE 0 23) :=
if let some marker := builder.a then
marker.toAbsolute <$> builder.h
<|> marker.toAbsolute <$> ((Bounded.LE.add · 1) <$> builder.K)
else
none
let hour :=
hour <|> (
let one : Option (Bounded.LE 0 23) := builder.H
let other : Option (Bounded.LE 0 23) := (Bounded.LE.sub · 1) <$> builder.k
(one <|> other))
|>.getD ⟨0, by decide⟩
let minute := builder.m |>.getD 0
let second := builder.s |>.getD 0
let nano := (builder.n <|> builder.S) |>.getD 0
let time
:= PlainTime.ofNanoseconds <$> builder.N
<|> PlainTime.ofMilliseconds <$> builder.A
|>.getD (PlainTime.mk hour minute second nano)
let datetime : Option PlainDateTime :=
if valid : year.Valid month day then
let date : PlainDate := { year, month, day, valid }
some { date, time }
else
none
match aw with
| .only newTz => (ofPlainDateTime · newTz) <$> datetime
| .any => (ZonedDateTime.ofPlainDateTime · (ZoneRules.ofTimeZone tz)) <$> datetime
end DateBuilder
private def parseWithDate (date : DateBuilder) (config : FormatConfig) (mod : FormatPart) : Parser DateBuilder := do
match mod with
| .modifier s => do
let res ← parseWith config s
return date.insert s res
| .string s => pstring s *> pure date
/--
Constructs a new `GenericFormat` specification for a date-time string. Modifiers can be combined to create
custom formats, such as "YYYY, MMMM, D".
-/
def spec (input : String) (config : FormatConfig := {}) : Except String (GenericFormat tz) := do
let string ← specParser.run input
return ⟨config, string⟩
/--
Builds a `GenericFormat` from the input string. If parsing fails, it will panic
-/
def spec! (input : String) (config : FormatConfig := {}) : GenericFormat tz :=
match specParser.run input with
| .ok res => ⟨config, res⟩
| .error res => panic! res
/--
Formats a `DateTime` value into a string using the given `GenericFormat`.
-/
def format (format : GenericFormat aw) (date : DateTime tz) : String :=
let mapper (part : FormatPart) :=
match aw with
| .any => formatPartWithDate date part
| .only tz => formatPartWithDate (date.convertTimeZone tz) part
format.string.map mapper
|> String.join
private def parser (format : FormatString) (config : FormatConfig) (aw : Awareness) : Parser (aw.type) :=
let rec go (builder : DateBuilder) (x : FormatString) : Parser aw.type :=
match x with
| x :: xs => parseWithDate builder config x >>= (go · xs)
| [] =>
match builder.build aw with
| some res => pure res
| none => fail "could not parse the date"
go {} format
/--
Parser for a format with a builder.
-/
def builderParser (format: FormatString) (config : FormatConfig) (func: FormatType (Option α) format) : Parser α :=
let rec go (format : FormatString) (func: FormatType (Option α) format) : Parser α :=
match format with
| .modifier x :: xs => do
let res ← parseWith config x
go xs (func res)
| .string s :: xs => skipString s *> (go xs func)
| [] =>
match func with
| some res => eof *> pure res
| none => fail "invalid date."
go format func
/--
Parses the input string into a `ZoneDateTime`.
-/
def parse (format : GenericFormat aw) (input : String) : Except String aw.type :=
(parser format.string format.config aw <* eof).run input
/--
Parses the input string into a `ZoneDateTime` and panics if its wrong.
-/
def parse! (format : GenericFormat aw) (input : String) : aw.type :=
match parse format input with
| .ok res => res
| .error err => panic! err
/--
Parses an input string using a builder function to produce a value.
-/
def parseBuilder (format : GenericFormat aw) (builder : FormatType (Option α) format.string) (input : String) : Except String α :=
(builderParser format.string format.config builder).run input
/--
Parses an input string using a builder function, panicking on errors.
-/
def parseBuilder! [Inhabited α] (format : GenericFormat aw) (builder : FormatType (Option α) format.string) (input : String) : α :=
match parseBuilder format builder input with
| .ok res => res
| .error err => panic! err
/--
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 rec go (data : String) : (format : FormatString) → Option String
| .modifier x :: xs => do go (data ++ formatWith x (← getInfo x)) xs
| .string x :: xs => go (data ++ x) xs
| [] => data
go "" format.string
/--
Constructs a `FormatType` function to format a date into a string using a `GenericFormat`.
-/
def formatBuilder (format : GenericFormat aw) : FormatType String format.string :=
let rec go (data : String) : (format : FormatString) → FormatType String format
| .modifier x :: xs => fun res => go (data ++ formatWith x res) xs
| .string x :: xs => go (data ++ x) xs
| [] => data
go "" format.string
end GenericFormat
/--
Typeclass for formatting and parsing values with the given format type.
-/
class Format (f : Type) (typ : Type → f → Type) where
/--
Converts a format `f` into a string.
-/
format : (fmt : f) → typ String fmt
/--
Parses a string into a format using the provided format type `f`.
-/
parse : (fmt : f) → typ (Option α) fmt → String → Except String α
instance : Format (GenericFormat aw) (FormatType · ·.string) where
format := GenericFormat.formatBuilder
parse := GenericFormat.parseBuilder
end Time