diff --git a/pkg/components/gtime/gtime.go b/pkg/components/gtime/gtime.go index 12a66ff1d7f..e1e00d965c9 100644 --- a/pkg/components/gtime/gtime.go +++ b/pkg/components/gtime/gtime.go @@ -1,12 +1,13 @@ package gtime import ( + "fmt" "regexp" "strconv" "time" ) -var dateUnitPattern = regexp.MustCompile(`(\d+)([wdy])`) +var dateUnitPattern = regexp.MustCompile(`^(\d+)([dwMy])$`) // ParseInterval parses an interval with support for all units that Grafana uses. func ParseInterval(interval string) (time.Duration, error) { @@ -18,14 +19,18 @@ func ParseInterval(interval string) (time.Duration, error) { num, _ := strconv.Atoi(string(result[1])) period := string(result[2]) + now := time.Now() - if period == `d` { - return time.Hour * 24 * time.Duration(num), nil + switch period { + case "d": + return now.Sub(now.AddDate(0, 0, -num)), nil + case "w": + return now.Sub(now.AddDate(0, 0, -num*7)), nil + case "M": + return now.Sub(now.AddDate(0, -num, 0)), nil + case "y": + return now.Sub(now.AddDate(-num, 0, 0)), nil } - if period == `w` { - return time.Hour * 24 * 7 * time.Duration(num), nil - } - - return time.Hour * 24 * 7 * 365 * time.Duration(num), nil + return 0, fmt.Errorf("ParseInterval: invalid duration %q", interval) } diff --git a/pkg/components/gtime/gtime_test.go b/pkg/components/gtime/gtime_test.go index 65489826af2..4dab30fbf6a 100644 --- a/pkg/components/gtime/gtime_test.go +++ b/pkg/components/gtime/gtime_test.go @@ -1,35 +1,38 @@ package gtime import ( - "errors" "fmt" "testing" "time" + + "github.com/stretchr/testify/require" ) func TestParseInterval(t *testing.T) { + now := time.Now() + tcs := []struct { interval string duration time.Duration - err error + err string }{ - {interval: "1d", duration: time.Hour * 24}, - {interval: "1w", duration: time.Hour * 24 * 7}, - {interval: "2w", duration: time.Hour * 24 * 7 * 2}, - {interval: "1y", duration: time.Hour * 24 * 7 * 365}, - {interval: "5y", duration: time.Hour * 24 * 7 * 365 * 5}, - {interval: "1M", err: errors.New("time: unknown unit M in duration 1M")}, - {interval: "invalid-duration", err: errors.New("time: invalid duration invalid-duration")}, + {interval: "1d", duration: now.Sub(now.AddDate(0, 0, -1))}, + {interval: "1w", duration: now.Sub(now.AddDate(0, 0, -7))}, + {interval: "2w", duration: now.Sub(now.AddDate(0, 0, -14))}, + {interval: "1M", duration: now.Sub(now.AddDate(0, -1, 0))}, + {interval: "1y", duration: now.Sub(now.AddDate(-1, 0, 0))}, + {interval: "5y", duration: now.Sub(now.AddDate(-5, 0, 0))}, + {interval: "invalid-duration", err: "time: invalid duration invalid-duration"}, } for i, tc := range tcs { t.Run(fmt.Sprintf("testcase %d", i), func(t *testing.T) { res, err := ParseInterval(tc.interval) - if err != nil && err.Error() != tc.err.Error() { - t.Fatalf("expected '%v' got '%v'", tc.err, err) - } - if res != tc.duration { - t.Errorf("expected %v got %v", tc.duration, res) + if tc.err == "" { + require.NoError(t, err, "interval %q", tc.interval) + require.Equal(t, tc.duration, res, "interval %q", tc.interval) + } else { + require.EqualError(t, err, tc.err, "interval %q", tc.interval) } }) } diff --git a/pkg/tsdb/time_range.go b/pkg/tsdb/time_range.go index 18e389e5993..085604a44f5 100644 --- a/pkg/tsdb/time_range.go +++ b/pkg/tsdb/time_range.go @@ -1,10 +1,11 @@ package tsdb import ( - "fmt" "strconv" "strings" "time" + + "github.com/timberio/go-datemath" ) func NewTimeRange(from, to string) *TimeRange { @@ -79,38 +80,26 @@ func tryParseUnixMsEpoch(val string) (time.Time, bool) { } func (tr *TimeRange) ParseFrom() (time.Time, error) { - if res, ok := tryParseUnixMsEpoch(tr.From); ok { - return res, nil - } - - fromRaw := strings.Replace(tr.From, "now-", "", 1) - diff, err := time.ParseDuration("-" + fromRaw) - if err != nil { - return time.Time{}, err - } - - return tr.now.Add(diff), nil + return parse(tr.From, tr.now, false) } func (tr *TimeRange) ParseTo() (time.Time, error) { - if tr.To == "now" { - return tr.now, nil - } else if strings.HasPrefix(tr.To, "now-") { - withoutNow := strings.Replace(tr.To, "now-", "", 1) + return parse(tr.To, tr.now, true) +} - diff, err := time.ParseDuration("-" + withoutNow) - if err != nil { - return time.Time{}, nil - } - - return tr.now.Add(diff), nil - } - - if res, ok := tryParseUnixMsEpoch(tr.To); ok { +func parse(s string, now time.Time, withRoundUp bool) (time.Time, error) { + if res, ok := tryParseUnixMsEpoch(s); ok { return res, nil } - return time.Time{}, fmt.Errorf("cannot parse to value %s", tr.To) + withoutNow := strings.Replace(s, "now-", "", 1) + + diff, err := time.ParseDuration("-" + withoutNow) + if err != nil { + return datemath.ParseAndEvaluate(s, datemath.WithNow(now), datemath.WithRoundUp(withRoundUp)) + } + + return now.Add(diff), nil } // EpochPrecisionToMs converts epoch precision to millisecond, if needed. diff --git a/pkg/tsdb/time_range_test.go b/pkg/tsdb/time_range_test.go index 38b14768954..89de8e81463 100644 --- a/pkg/tsdb/time_range_test.go +++ b/pkg/tsdb/time_range_test.go @@ -1,7 +1,9 @@ package tsdb import ( + "strconv" "testing" + "time" . "github.com/smartystreets/goconvey/convey" @@ -20,7 +22,8 @@ func TestTimeRange(t *testing.T) { } Convey("5m ago ", func() { - fiveMinAgo, _ := time.ParseDuration("-5m") + fiveMinAgo, err := time.ParseDuration("-5m") + So(err, ShouldBeNil) expected := now.Add(fiveMinAgo) res, err := tr.ParseFrom() @@ -43,7 +46,8 @@ func TestTimeRange(t *testing.T) { } Convey("5h ago ", func() { - fiveHourAgo, _ := time.ParseDuration("-5h") + fiveHourAgo, err := time.ParseDuration("-5h") + So(err, ShouldBeNil) expected := now.Add(fiveHourAgo) res, err := tr.ParseFrom() @@ -52,7 +56,8 @@ func TestTimeRange(t *testing.T) { }) Convey("now-10m ", func() { - tenMinAgo, _ := time.ParseDuration("-10m") + tenMinAgo, err := time.ParseDuration("-10m") + So(err, ShouldBeNil) expected := now.Add(tenMinAgo) res, err := tr.ParseTo() So(err, ShouldBeNil) @@ -60,7 +65,101 @@ func TestTimeRange(t *testing.T) { }) }) - Convey("can parse unix epocs", func() { + now, err := time.Parse(time.RFC3339Nano, "2020-03-26T15:12:56.000Z") + So(err, ShouldBeNil) + Convey("Can parse now-1M/M, now-1M/M", func() { + tr := TimeRange{ + From: "now-1M/M", + To: "now-1M/M", + now: now, + } + + Convey("from now-1M/M ", func() { + expected, err := time.Parse(time.RFC3339Nano, "2020-02-01T00:00:00.000Z") + So(err, ShouldBeNil) + + res, err := tr.ParseFrom() + So(err, ShouldBeNil) + So(res, ShouldEqual, expected) + }) + + Convey("to now-1M/M ", func() { + expected, err := time.Parse(time.RFC3339Nano, "2020-02-29T23:59:59.999Z") + So(err, ShouldBeNil) + + res, err := tr.ParseTo() + So(err, ShouldBeNil) + So(res, ShouldEqual, expected) + }) + }) + + Convey("Can parse now-3d, now+3w", func() { + tr := TimeRange{ + From: "now-3d", + To: "now+3w", + now: now, + } + + Convey("now-3d ", func() { + expected, err := time.Parse(time.RFC3339Nano, "2020-03-23T15:12:56.000Z") + So(err, ShouldBeNil) + + res, err := tr.ParseFrom() + So(err, ShouldBeNil) + So(res, ShouldEqual, expected) + }) + + Convey("now+3w ", func() { + expected, err := time.Parse(time.RFC3339Nano, "2020-04-16T15:12:56.000Z") + So(err, ShouldBeNil) + + res, err := tr.ParseTo() + So(err, ShouldBeNil) + So(res, ShouldEqual, expected) + }) + }) + + Convey("Can parse 1960-02-01T07:00:00.000Z, 1965-02-03T08:00:00.000Z", func() { + tr := TimeRange{ + From: "1960-02-01T07:00:00.000Z", + To: "1965-02-03T08:00:00.000Z", + now: now, + } + + Convey("1960-02-01T07:00:00.000Z ", func() { + expected, err := time.Parse(time.RFC3339Nano, "1960-02-01T07:00:00.000Z") + So(err, ShouldBeNil) + + res, err := tr.ParseFrom() + So(err, ShouldBeNil) + So(res, ShouldEqual, expected) + }) + + Convey("1965-02-03T08:00:00.000Z ", func() { + expected, err := time.Parse(time.RFC3339Nano, "1965-02-03T08:00:00.000Z") + So(err, ShouldBeNil) + + res, err := tr.ParseTo() + So(err, ShouldBeNil) + So(res, ShouldEqual, expected) + }) + }) + + Convey("Can parse negative unix epochs", func() { + from := time.Date(1960, 2, 1, 7, 0, 0, 0, time.UTC) + to := time.Date(1965, 2, 3, 8, 0, 0, 0, time.UTC) + tr := NewTimeRange(strconv.FormatInt(from.UnixNano()/int64(time.Millisecond), 10), strconv.FormatInt(to.UnixNano()/int64(time.Millisecond), 10)) + + res, err := tr.ParseFrom() + So(err, ShouldBeNil) + So(res, ShouldEqual, from) + + res, err = tr.ParseTo() + So(err, ShouldBeNil) + So(res, ShouldEqual, to) + }) + + Convey("can parse unix epochs", func() { var err error tr := TimeRange{ From: "1474973725473",