summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--pkg/timetype/timestamp.go131
-rw-r--r--pkg/timetype/timestamp_test.go95
2 files changed, 226 insertions, 0 deletions
diff --git a/pkg/timetype/timestamp.go b/pkg/timetype/timestamp.go
new file mode 100644
index 000000000..eb904a574
--- /dev/null
+++ b/pkg/timetype/timestamp.go
@@ -0,0 +1,131 @@
+package timetype
+
+// code adapted from https://github.com/moby/moby/blob/master/api/types/time/timestamp.go
+
+import (
+ "fmt"
+ "math"
+ "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
+)
+
+// GetTimestamp tries to parse given string as golang duration,
+// then RFC3339 time and finally as a Unix timestamp. If
+// any of these were successful, it returns a Unix timestamp
+// as string otherwise returns the given value back.
+// In case of duration input, the returned timestamp is computed
+// as the given reference time minus the amount of the duration.
+func GetTimestamp(value string, reference time.Time) (string, error) {
+ if d, err := time.ParseDuration(value); value != "0" && err == nil {
+ return strconv.FormatInt(reference.Add(-d).Unix(), 10), 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 "", err // was probably an RFC3339 like timestamp but the parser failed with an error
+ }
+ if _, _, err := parseTimestamp(value); err != nil {
+ return "", fmt.Errorf("failed to parse value as time or duration: %q", value)
+ }
+ return value, nil // unix timestamp in and out case (meaning: the value passed at the command line is already in the right format for passing to the server)
+ }
+
+ return fmt.Sprintf("%d.%09d", t.Unix(), int64(t.Nanosecond())), nil
+}
+
+// ParseTimestamps returns seconds and nanoseconds from a timestamp that has the
+// format "%d.%09d", time.Unix(), int64(time.Nanosecond()))
+// if the incoming nanosecond portion is longer or shorter than 9 digits it is
+// converted to nanoseconds. The expectation is that the seconds and
+// seconds will be used to create a time variable. For example:
+// seconds, nanoseconds, err := ParseTimestamp("1136073600.000000001",0)
+// if err == nil since := time.Unix(seconds, nanoseconds)
+// returns seconds as def(aultSeconds) if value == ""
+func ParseTimestamps(value string, def int64) (int64, int64, error) {
+ if value == "" {
+ return def, 0, nil
+ }
+ return parseTimestamp(value)
+}
+
+func parseTimestamp(value string) (int64, int64, error) {
+ sa := strings.SplitN(value, ".", 2)
+ s, err := strconv.ParseInt(sa[0], 10, 64)
+ if err != nil {
+ return s, 0, err
+ }
+ if len(sa) != 2 {
+ return s, 0, nil
+ }
+ n, err := strconv.ParseInt(sa[1], 10, 64)
+ if err != nil {
+ return s, n, err
+ }
+ // should already be in nanoseconds but just in case convert n to nanoseconds
+ n = int64(float64(n) * math.Pow(float64(10), float64(9-len(sa[1]))))
+ return s, n, nil
+}
diff --git a/pkg/timetype/timestamp_test.go b/pkg/timetype/timestamp_test.go
new file mode 100644
index 000000000..0fffb85a9
--- /dev/null
+++ b/pkg/timetype/timestamp_test.go
@@ -0,0 +1,95 @@
+package timetype
+
+// code adapted from https://github.com/moby/moby/blob/master/api/types/time/timestamp.go
+
+import (
+ "fmt"
+ "testing"
+ "time"
+)
+
+func TestGetTimestamp(t *testing.T) {
+ now := time.Now().In(time.UTC)
+ cases := []struct {
+ in, expected string
+ expectedErr bool
+ }{
+ // Partial RFC3339 strings get parsed with second precision
+ {"2006-01-02T15:04:05.999999999+07:00", "1136189045.999999999", false},
+ {"2006-01-02T15:04:05.999999999Z", "1136214245.999999999", false},
+ {"2006-01-02T15:04:05.999999999", "1136214245.999999999", false},
+ {"2006-01-02T15:04:05Z", "1136214245.000000000", false},
+ {"2006-01-02T15:04:05", "1136214245.000000000", false},
+ {"2006-01-02T15:04:0Z", "", true},
+ {"2006-01-02T15:04:0", "", true},
+ {"2006-01-02T15:04Z", "1136214240.000000000", false},
+ {"2006-01-02T15:04+00:00", "1136214240.000000000", false},
+ {"2006-01-02T15:04-00:00", "1136214240.000000000", false},
+ {"2006-01-02T15:04", "1136214240.000000000", false},
+ {"2006-01-02T15:0Z", "", true},
+ {"2006-01-02T15:0", "", true},
+ {"2006-01-02T15Z", "1136214000.000000000", false},
+ {"2006-01-02T15+00:00", "1136214000.000000000", false},
+ {"2006-01-02T15-00:00", "1136214000.000000000", false},
+ {"2006-01-02T15", "1136214000.000000000", false},
+ {"2006-01-02T1Z", "1136163600.000000000", false},
+ {"2006-01-02T1", "1136163600.000000000", false},
+ {"2006-01-02TZ", "", true},
+ {"2006-01-02T", "", true},
+ {"2006-01-02+00:00", "1136160000.000000000", false},
+ {"2006-01-02-00:00", "1136160000.000000000", false},
+ {"2006-01-02-00:01", "1136160060.000000000", false},
+ {"2006-01-02Z", "1136160000.000000000", false},
+ {"2006-01-02", "1136160000.000000000", false},
+ {"2015-05-13T20:39:09Z", "1431549549.000000000", false},
+
+ // unix timestamps returned as is
+ {"1136073600", "1136073600", false},
+ {"1136073600.000000001", "1136073600.000000001", false},
+ // Durations
+ {"1m", fmt.Sprintf("%d", now.Add(-1*time.Minute).Unix()), false},
+ {"1.5h", fmt.Sprintf("%d", now.Add(-90*time.Minute).Unix()), false},
+ {"1h30m", fmt.Sprintf("%d", now.Add(-90*time.Minute).Unix()), false},
+
+ {"invalid", "", true},
+ {"", "", true},
+ }
+
+ for _, c := range cases {
+ o, err := GetTimestamp(c.in, now)
+ if o != c.expected ||
+ (err == nil && c.expectedErr) ||
+ (err != nil && !c.expectedErr) {
+ t.Errorf("wrong value for '%s'. expected:'%s' got:'%s' with error: `%s`", c.in, c.expected, o, err)
+ t.Fail()
+ }
+ }
+}
+
+func TestParseTimestamps(t *testing.T) {
+ cases := []struct {
+ in string
+ def, expectedS, expectedN int64
+ expectedErr bool
+ }{
+ // unix timestamps
+ {"1136073600", 0, 1136073600, 0, false},
+ {"1136073600.000000001", 0, 1136073600, 1, false},
+ {"1136073600.0000000010", 0, 1136073600, 1, false},
+ {"1136073600.00000001", 0, 1136073600, 10, false},
+ {"foo.bar", 0, 0, 0, true},
+ {"1136073600.bar", 0, 1136073600, 0, true},
+ {"", -1, -1, 0, false},
+ }
+
+ for _, c := range cases {
+ s, n, err := ParseTimestamps(c.in, c.def)
+ if s != c.expectedS ||
+ n != c.expectedN ||
+ (err == nil && c.expectedErr) ||
+ (err != nil && !c.expectedErr) {
+ t.Errorf("wrong values for input `%s` with default `%d` expected:'%d'seconds and `%d`nanosecond got:'%d'seconds and `%d`nanoseconds with error: `%s`", c.in, c.def, c.expectedS, c.expectedN, s, n, err)
+ t.Fail()
+ }
+ }
+}