package interpres import ( "fmt" "regexp" "strings" "time" ) // TOML distinguishes four date-time kinds. interpres decodes an offset // date-time to a plain time.Time (it carries a zone), and uses the wrapper // types below for the local variants so callers can tell them apart. // LocalDateTime is a TOML local date-time with no offset, e.g. // 1979-05-27T07:32:00. The embedded time.Time is in UTC. type LocalDateTime struct{ time.Time } // LocalDate is a TOML local date with no time or offset, e.g. 1979-05-27. // The embedded time.Time is at midnight UTC. type LocalDate struct{ time.Time } // LocalTime is a TOML local time with no date or offset, e.g. 07:32:00.999999. // The embedded time.Time uses the zero date. type LocalTime struct{ time.Time } // String returns the TOML-canonical rendering of the local date-time, e.g. // "1979-05-27T07:32:00" or "...:00.000000123" when the time has a fractional // second. The fractional component is zero-padded to nanosecond precision. func (ldt LocalDateTime) String() string { base := ldt.Format("2006-01-02T15:04:05") if ns := ldt.Nanosecond(); ns > 0 { return base + "." + fmt.Sprintf("%09d", ns) } return base } // String returns the TOML-canonical rendering of the local date, e.g. // "1979-05-27". func (ld LocalDate) String() string { return ld.Format("2006-01-02") } // String returns the TOML-canonical rendering of the local time, e.g. // "07:32:00" or "...:00.000000123" when the time has a fractional second. // The fractional component is zero-padded to nanosecond precision. func (lt LocalTime) String() string { base := lt.Format("15:04:05") if ns := lt.Nanosecond(); ns > 0 { return base + "." + fmt.Sprintf("%09d", ns) } return base } var ( offsetDateTimeLayouts = []string{ "2006-01-02T15:04:05.999999999Z07:00", "2006-01-02T15:04:05Z07:00", "2006-01-02 15:04:05.999999999Z07:00", "2006-01-02 15:04:05Z07:00", } localDateTimeLayouts = []string{ "2006-01-02T15:04:05.999999999", "2006-01-02T15:04:05", "2006-01-02 15:04:05.999999999", "2006-01-02 15:04:05", } localTimeLayouts = []string{ "15:04:05.999999999", "15:04:05", } ) // dateTimeShape enforces the strict TOML grammar (two-digit components) that // time.Parse would otherwise accept loosely (e.g. a single-digit hour). var dateTimeShape = regexp.MustCompile( `^\d{4}-\d{2}-\d{2}([Tt ]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}:\d{2})?)?$` + `|^\d{2}:\d{2}:\d{2}(\.\d+)?$`, ) // parseDateTime classifies and parses a bare token as a TOML date-time value. // It returns the decoded value (time.Time, LocalDateTime, LocalDate, or // LocalTime) and whether the token was a date-time at all. func parseDateTime(tok string) (any, bool) { if tok == "" || tok[0] < '0' || tok[0] > '9' { return nil, false } if !strings.ContainsAny(tok, "-:") { return nil, false } if !dateTimeShape.MatchString(tok) { return nil, false } // The ABNF accepts lowercase "t"/"z"; time.Parse only matches uppercase. norm := strings.ToUpper(tok) for _, layout := range offsetDateTimeLayouts { if t, err := time.Parse(layout, norm); err == nil { return t, true } } for _, layout := range localDateTimeLayouts { if t, err := time.Parse(layout, norm); err == nil { return LocalDateTime{t}, true } } if t, err := time.Parse("2006-01-02", norm); err == nil { return LocalDate{t}, true } for _, layout := range localTimeLayouts { if t, err := time.Parse(layout, norm); err == nil { return LocalTime{t}, true } } return nil, false } // isDateToken reports whether s is exactly a YYYY-MM-DD date, used to detect a // space-separated date-time written as "datetime". func isDateToken(s string) bool { if len(s) != 10 { return false } for i := 0; i < len(s); i++ { if i == 4 || i == 7 { if s[i] != '-' { return false } } else if !isDecDigit(s[i]) { return false } } return true }