|
|
|
|
@ -8,109 +8,119 @@ package tstime |
|
|
|
|
import ( |
|
|
|
|
"errors" |
|
|
|
|
"fmt" |
|
|
|
|
"strings" |
|
|
|
|
"sync" |
|
|
|
|
"time" |
|
|
|
|
|
|
|
|
|
"go4.org/mem" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
var memZ = mem.S("Z") |
|
|
|
|
|
|
|
|
|
// zoneOf returns the RFC3339 zone suffix (either "Z" or like
|
|
|
|
|
// "+08:30"), or the empty string if it's invalid or not something we
|
|
|
|
|
// want to cache.
|
|
|
|
|
func zoneOf(s string) string { |
|
|
|
|
if strings.HasSuffix(s, "Z") { |
|
|
|
|
return "Z" |
|
|
|
|
func zoneOf(s mem.RO) mem.RO { |
|
|
|
|
if mem.HasSuffix(s, memZ) { |
|
|
|
|
return memZ |
|
|
|
|
} |
|
|
|
|
if len(s) < len("2020-04-05T15:56:00+08:00") { |
|
|
|
|
if s.Len() < len("2020-04-05T15:56:00+08:00") { |
|
|
|
|
// Too short, invalid? Let time.Parse fail on it.
|
|
|
|
|
return "" |
|
|
|
|
return mem.S("") |
|
|
|
|
} |
|
|
|
|
zone := s[len(s)-len("+08:00"):] |
|
|
|
|
if c := zone[0]; c == '+' || c == '-' { |
|
|
|
|
min := zone[len("+08:"):] |
|
|
|
|
switch min { |
|
|
|
|
case "00", "15", "30": |
|
|
|
|
zone := s.SliceFrom(s.Len() - len("+08:00")) |
|
|
|
|
if c := zone.At(0); c == '+' || c == '-' { |
|
|
|
|
min := zone.SliceFrom(len("+08:")) |
|
|
|
|
if min.EqualString("00") || min.EqualString("15") || min.EqualString("30") { |
|
|
|
|
return zone |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return "" |
|
|
|
|
return mem.S("") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// locCache maps from zone offset suffix string ("+08:00") =>
|
|
|
|
|
// *time.Location (from FixedLocation).
|
|
|
|
|
// locCache maps from hash of zone offset suffix string ("+08:00") =>
|
|
|
|
|
// {zone string, *time.Location (from FixedLocation)}.
|
|
|
|
|
var locCache sync.Map |
|
|
|
|
|
|
|
|
|
func getLocation(zone, timeValue string) (*time.Location, error) { |
|
|
|
|
if zone == "Z" { |
|
|
|
|
type locCacheEntry struct { |
|
|
|
|
zone string |
|
|
|
|
loc *time.Location |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func getLocation(zone, timeValue mem.RO) (*time.Location, error) { |
|
|
|
|
if zone.EqualString("Z") { |
|
|
|
|
return time.UTC, nil |
|
|
|
|
} |
|
|
|
|
if loci, ok := locCache.Load(zone); ok { |
|
|
|
|
return loci.(*time.Location), nil |
|
|
|
|
key := zone.MapHash() |
|
|
|
|
if entry, ok := locCache.Load(key); ok { |
|
|
|
|
// We're keying only on a hash; double-check zone to ensure no spurious collisions.
|
|
|
|
|
e := entry.(locCacheEntry) |
|
|
|
|
if zone.EqualString(e.zone) { |
|
|
|
|
return e.loc, nil |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
// TODO(bradfitz): just parse it and call time.FixedLocation.
|
|
|
|
|
// For now, just have time.Parse do it once:
|
|
|
|
|
t, err := time.Parse(time.RFC3339Nano, timeValue) |
|
|
|
|
t, err := time.Parse(time.RFC3339Nano, timeValue.StringCopy()) |
|
|
|
|
if err != nil { |
|
|
|
|
return nil, err |
|
|
|
|
} |
|
|
|
|
loc := t.Location() |
|
|
|
|
locCache.LoadOrStore(zone, loc) |
|
|
|
|
locCache.LoadOrStore(key, locCacheEntry{zone: zone.StringCopy(), loc: loc}) |
|
|
|
|
return loc, nil |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Parse3339 is a wrapper around time.Parse(time.RFC3339Nano, s) that caches
|
|
|
|
|
// timezone Locations for future parses.
|
|
|
|
|
func Parse3339(s string) (time.Time, error) { |
|
|
|
|
func parse3339m(s mem.RO) (time.Time, error) { |
|
|
|
|
zone := zoneOf(s) |
|
|
|
|
if zone == "" { |
|
|
|
|
if zone.Len() == 0 { |
|
|
|
|
// Invalid or weird timezone offset. Use slow path,
|
|
|
|
|
// which'll probably return an error.
|
|
|
|
|
return time.Parse(time.RFC3339Nano, s) |
|
|
|
|
return time.Parse(time.RFC3339Nano, s.StringCopy()) |
|
|
|
|
} |
|
|
|
|
loc, err := getLocation(zone, s) |
|
|
|
|
if err != nil { |
|
|
|
|
return time.Time{}, err |
|
|
|
|
} |
|
|
|
|
s = s[:len(s)-len(zone)] // remove zone suffix
|
|
|
|
|
s = s.SliceTo(s.Len() - zone.Len()) // remove zone suffix
|
|
|
|
|
var year, mon, day, hr, min, sec, nsec int |
|
|
|
|
const baseLen = len("2020-04-05T15:56:00") |
|
|
|
|
if len(s) < baseLen || |
|
|
|
|
!parseInt(s[:4], &year) || |
|
|
|
|
s[4] != '-' || |
|
|
|
|
!parseInt(s[5:7], &mon) || |
|
|
|
|
s[7] != '-' || |
|
|
|
|
!parseInt(s[8:10], &day) || |
|
|
|
|
s[10] != 'T' || |
|
|
|
|
!parseInt(s[11:13], &hr) || |
|
|
|
|
s[13] != ':' || |
|
|
|
|
!parseInt(s[14:16], &min) || |
|
|
|
|
s[16] != ':' || |
|
|
|
|
!parseInt(s[17:19], &sec) { |
|
|
|
|
if s.Len() < baseLen || |
|
|
|
|
!parseInt(s.SliceTo(4), &year) || |
|
|
|
|
s.At(4) != '-' || |
|
|
|
|
!parseInt(s.Slice(5, 7), &mon) || |
|
|
|
|
s.At(7) != '-' || |
|
|
|
|
!parseInt(s.Slice(8, 10), &day) || |
|
|
|
|
s.At(10) != 'T' || |
|
|
|
|
!parseInt(s.Slice(11, 13), &hr) || |
|
|
|
|
s.At(13) != ':' || |
|
|
|
|
!parseInt(s.Slice(14, 16), &min) || |
|
|
|
|
s.At(16) != ':' || |
|
|
|
|
!parseInt(s.Slice(17, 19), &sec) { |
|
|
|
|
return time.Time{}, errors.New("invalid time") |
|
|
|
|
} |
|
|
|
|
nsStr := s[baseLen:] |
|
|
|
|
if nsStr != "" { |
|
|
|
|
if nsStr[0] != '.' { |
|
|
|
|
nsStr := s.SliceFrom(baseLen) |
|
|
|
|
if nsStr.Len() != 0 { |
|
|
|
|
if nsStr.At(0) != '.' { |
|
|
|
|
return time.Time{}, errors.New("invalid optional nanosecond prefix") |
|
|
|
|
} |
|
|
|
|
if !parseInt(nsStr[1:], &nsec) { |
|
|
|
|
return time.Time{}, fmt.Errorf("invalid optional nanosecond number %q", nsStr[1:]) |
|
|
|
|
nsStr = nsStr.SliceFrom(1) |
|
|
|
|
if !parseInt(nsStr, &nsec) { |
|
|
|
|
return time.Time{}, fmt.Errorf("invalid optional nanosecond number %q", nsStr.StringCopy()) |
|
|
|
|
} |
|
|
|
|
for i := 0; i < len("999999999")-(len(nsStr)-1); i++ { |
|
|
|
|
for i := 0; i < len("999999999")-nsStr.Len(); i++ { |
|
|
|
|
nsec *= 10 |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return time.Date(year, time.Month(mon), day, hr, min, sec, nsec, loc), nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func parseInt(s string, dst *int) bool { |
|
|
|
|
if len(s) == 0 || len(s) > len("999999999") { |
|
|
|
|
func parseInt(s mem.RO, dst *int) bool { |
|
|
|
|
if s.Len() == 0 || s.Len() > len("999999999") { |
|
|
|
|
*dst = 0 |
|
|
|
|
return false |
|
|
|
|
} |
|
|
|
|
n := 0 |
|
|
|
|
for i := 0; i < len(s); i++ { |
|
|
|
|
d := s[i] - '0' |
|
|
|
|
for i := 0; i < s.Len(); i++ { |
|
|
|
|
d := s.At(i) - '0' |
|
|
|
|
if d > 9 { |
|
|
|
|
*dst = 0 |
|
|
|
|
return false |
|
|
|
|
@ -120,3 +130,14 @@ func parseInt(s string, dst *int) bool { |
|
|
|
|
*dst = n |
|
|
|
|
return true |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Parse3339 is a wrapper around time.Parse(time.RFC3339Nano, s) that caches
|
|
|
|
|
// timezone Locations for future parses.
|
|
|
|
|
func Parse3339(s string) (time.Time, error) { |
|
|
|
|
return parse3339m(mem.S(s)) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Parse3339B is Parse3339 but for byte slices.
|
|
|
|
|
func Parse3339B(b []byte) (time.Time, error) { |
|
|
|
|
return parse3339m(mem.B(b)) |
|
|
|
|
} |
|
|
|
|
|