Files

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
}