blob: 4f69c8aaae4d9b0586bff20a0e518ca5b608eb2b [file] [edit]
package timestamp
import (
"errors"
"fmt"
"strconv"
"strings"
"time"
)
// These are additional predefined layouts for use in Time.Format and Time.Parse
// with --since and --until parameters for `docker logs` and `docker events`
const (
rFC3339Local = "2006-01-02T15:04:05" // RFC3339 with local timezone
rFC3339NanoLocal = "2006-01-02T15:04:05.999999999" // RFC3339Nano with local timezone
dateWithZone = "2006-01-02Z07:00" // RFC3339 with time at 00:00:00
dateLocal = "2006-01-02" // RFC3339 with local timezone and time at 00:00:00
)
// Parse tries to parse given string as golang duration, then RFC3339 time and
// finally as a Unix timestamp. The returned time is normalized to UTC.
//
// In case of duration input, the returned timestamp is computed as the given
// reference time minus the amount of the duration.
func Parse(value string, reference time.Time) (time.Time, error) {
if strings.TrimSpace(value) == "" {
return time.Time{}, errors.New("failed to parse value as time or duration: value is empty")
}
if d, err := time.ParseDuration(value); value != "0" && err == nil {
return reference.Add(-d).UTC(), nil
}
var format string
// if the string has a Z or a + or three dashes use parse otherwise use parseinlocation
parseInLocation := !strings.ContainsAny(value, "zZ+") && strings.Count(value, "-") != 3
if strings.Contains(value, ".") {
if parseInLocation {
format = rFC3339NanoLocal
} else {
format = time.RFC3339Nano
}
} else if strings.Contains(value, "T") {
// we want the number of colons in the T portion of the timestamp
tcolons := strings.Count(value, ":")
// if parseInLocation is off and we have a +/- zone offset (not Z) then
// there will be an extra colon in the input for the tz offset subtract that
// colon from the tcolons count
if !parseInLocation && !strings.ContainsAny(value, "zZ") && tcolons > 0 {
tcolons--
}
if parseInLocation {
switch tcolons {
case 0:
format = "2006-01-02T15"
case 1:
format = "2006-01-02T15:04"
default:
format = rFC3339Local
}
} else {
switch tcolons {
case 0:
format = "2006-01-02T15Z07:00"
case 1:
format = "2006-01-02T15:04Z07:00"
default:
format = time.RFC3339
}
}
} else if parseInLocation {
format = dateLocal
} else {
format = dateWithZone
}
var t time.Time
var err error
if parseInLocation {
t, err = time.ParseInLocation(format, value, time.FixedZone(reference.Zone()))
} else {
t, err = time.Parse(format, value)
}
if err != nil {
// if there is a `-` then it's an RFC3339 like timestamp
if strings.Contains(value, "-") {
return time.Time{}, err // was probably an RFC3339 like timestamp but the parser failed with an error
}
t, err = parseTimestamp(value)
if err != nil {
return time.Time{}, fmt.Errorf("failed to parse value as time or duration: %w", err)
}
}
return t.UTC(), nil
}
// ParseUnixTimestamp parses a UNIX timestamp with optional nanoseconds
// and returns it as a [time.Time].
//
// Value should be formatted as
//
// "%d.%09d" (seconds.nanoseconds)
//
// It accepts either:
// - "seconds"
// - "seconds.nanoseconds"
//
// If the nanoseconds has less than 9 digits it is right-padded with zeros.
// If it has more than 9 digits it is truncated.
//
// Empty values ("") produce a zero-value, with no error.
func ParseUnixTimestamp(value string) (time.Time, error) {
if value == "" {
return time.Time{}, nil
}
t, err := parseTimestamp(value)
if err != nil {
return time.Time{}, fmt.Errorf("invalid timestamp %q: %w", value, err)
}
return t, nil
}
func parseTimestamp(value string) (time.Time, error) {
s, n, ok := strings.Cut(value, ".")
sec, err := strconv.ParseInt(s, 10, 64)
if err != nil {
var numErr *strconv.NumError
if errors.As(err, &numErr) {
err = numErr.Err
}
return time.Time{}, fmt.Errorf("invalid seconds %q: %w", s, err)
}
if !ok || n == "" || n == "000000000" || n == "0" {
// Fast path for "zero" nanoseconds
return time.Unix(sec, 0).UTC(), nil
}
// The documented format is 9 digits. Historically we allowed more and
// truncated to nanoseconds precision. Let's add some sensible limit;
// 20 digits would be a full uint64 value.
const maxFracDigits = 20
if len(n) > maxFracDigits {
return time.Time{}, fmt.Errorf("invalid nanoseconds: length %d exceeds maximum %d", len(n), maxFracDigits)
}
// Nanoseconds must be digits only (reject '+' / '-' and other junk).
for i := range len(n) {
if c := n[i]; c < '0' || c > '9' {
return time.Time{}, fmt.Errorf("invalid nanoseconds: invalid character %q at position %d", c, i)
}
}
// cap at 9 digits, or pad to 9 digits
if len(n) > 9 {
n = n[:9]
} else if len(n) < 9 {
n += strings.Repeat("0", 9-len(n))
}
nsec, err := strconv.ParseInt(n, 10, 64)
if err != nil {
// this should never fail, but just in case
return time.Time{}, fmt.Errorf("invalid nanoseconds %q: %w", n, err)
}
return time.Unix(sec, nsec).UTC(), nil
}