131 lines
3.8 KiB
Go
131 lines
3.8 KiB
Go
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 "date<space>time".
|
|
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
|
|
}
|