-
Notifications
You must be signed in to change notification settings - Fork 66
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Move gtime package from grafana/grafana (#406)
- Loading branch information
1 parent
6d0be6b
commit 03a2da2
Showing
2 changed files
with
189 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
package gtime | ||
|
||
import ( | ||
"fmt" | ||
"regexp" | ||
"strconv" | ||
"time" | ||
) | ||
|
||
var dateUnitPattern = regexp.MustCompile(`^(\d+)([dwMy])$`) | ||
|
||
// ParseInterval parses an interval with support for all units that Grafana uses. | ||
// An interval is relative to the current wall time. | ||
func ParseInterval(inp string) (time.Duration, error) { | ||
dur, period, err := parse(inp) | ||
if err != nil { | ||
return 0, err | ||
} | ||
if period == "" { | ||
return dur, nil | ||
} | ||
|
||
num := int(dur) | ||
|
||
// Use UTC to ensure that the interval is deterministic, and daylight saving | ||
// doesn't cause surprises | ||
now := time.Now().UTC() | ||
switch period { | ||
case "d": | ||
return now.AddDate(0, 0, num).Sub(now), nil | ||
case "w": | ||
return now.AddDate(0, 0, num*7).Sub(now), nil | ||
case "M": | ||
return now.AddDate(0, num, 0).Sub(now), nil | ||
case "y": | ||
return now.AddDate(num, 0, 0).Sub(now), nil | ||
} | ||
|
||
return 0, fmt.Errorf("invalid interval %q", inp) | ||
} | ||
|
||
// ParseDuration parses a duration with support for all units that Grafana uses. | ||
// Durations are independent of wall time. | ||
func ParseDuration(inp string) (time.Duration, error) { | ||
dur, period, err := parse(inp) | ||
if err != nil { | ||
return 0, err | ||
} | ||
if period == "" { | ||
return dur, nil | ||
} | ||
|
||
// The average number of days in a year, using the Julian calendar | ||
const daysInAYear = 365.25 | ||
const day = 24 * time.Hour | ||
const week = 7 * day | ||
const year = time.Duration(float64(day) * daysInAYear) | ||
const month = time.Duration(float64(year) / 12) | ||
|
||
switch period { | ||
case "d": | ||
return dur * day, nil | ||
case "w": | ||
return dur * week, nil | ||
case "M": | ||
return dur * month, nil | ||
case "y": | ||
return dur * year, nil | ||
} | ||
|
||
return 0, fmt.Errorf("invalid duration %q", inp) | ||
} | ||
|
||
func parse(inp string) (time.Duration, string, error) { | ||
result := dateUnitPattern.FindSubmatch([]byte(inp)) | ||
if len(result) != 3 { | ||
dur, err := time.ParseDuration(inp) | ||
return dur, "", err | ||
} | ||
|
||
num, err := strconv.Atoi(string(result[1])) | ||
if err != nil { | ||
return 0, "", err | ||
} | ||
|
||
return time.Duration(num), string(result[2]), nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
package gtime | ||
|
||
import ( | ||
"fmt" | ||
"regexp" | ||
"testing" | ||
"time" | ||
|
||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestParseInterval(t *testing.T) { | ||
daysInMonth, daysInYear := calculateDays() | ||
|
||
tcs := []struct { | ||
inp string | ||
duration time.Duration | ||
err *regexp.Regexp | ||
}{ | ||
{inp: "1d", duration: 24 * time.Hour}, | ||
{inp: "1w", duration: 168 * time.Hour}, | ||
{inp: "2w", duration: 2 * 168 * time.Hour}, | ||
{inp: "1M", duration: time.Duration(daysInMonth * 24 * int(time.Hour))}, | ||
{inp: "1y", duration: time.Duration(daysInYear * 24 * int(time.Hour))}, | ||
{inp: "5y", duration: time.Duration(calculateDays5y() * 24 * int(time.Hour))}, | ||
{inp: "invalid-duration", err: regexp.MustCompile(`^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.inp) | ||
if tc.err == nil { | ||
require.NoError(t, err, "input %q", tc.inp) | ||
require.Equal(t, tc.duration, res, "input %q", tc.inp) | ||
} else { | ||
require.Error(t, err, "input %q", tc.inp) | ||
require.Regexp(t, tc.err, err.Error()) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestParseDuration(t *testing.T) { | ||
tcs := []struct { | ||
inp string | ||
duration time.Duration | ||
err *regexp.Regexp | ||
}{ | ||
{inp: "1s", duration: time.Second}, | ||
{inp: "1m", duration: time.Minute}, | ||
{inp: "1h", duration: time.Hour}, | ||
{inp: "1d", duration: 24 * time.Hour}, | ||
{inp: "1w", duration: 7 * 24 * time.Hour}, | ||
{inp: "2w", duration: 2 * 7 * 24 * time.Hour}, | ||
{inp: "1M", duration: time.Duration(730.5 * float64(time.Hour))}, | ||
{inp: "1y", duration: 365.25 * 24 * time.Hour}, | ||
{inp: "5y", duration: 5 * 365.25 * 24 * time.Hour}, | ||
{inp: "invalid-duration", err: regexp.MustCompile(`^time: invalid duration "?invalid-duration"?$`)}, | ||
} | ||
for i, tc := range tcs { | ||
t.Run(fmt.Sprintf("testcase %d", i), func(t *testing.T) { | ||
res, err := ParseDuration(tc.inp) | ||
if tc.err == nil { | ||
require.NoError(t, err, "input %q", tc.inp) | ||
require.Equal(t, tc.duration, res, "input %q", tc.inp) | ||
} else { | ||
require.Error(t, err, "input %q", tc.inp) | ||
require.Regexp(t, tc.err, err.Error()) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func calculateDays() (int, int) { | ||
now := time.Now().UTC() | ||
currentYear, currentMonth, _ := now.Date() | ||
|
||
firstDayOfMonth := time.Date(currentYear, currentMonth, 1, 0, 0, 0, 0, time.UTC) | ||
daysInMonth := firstDayOfMonth.AddDate(0, 1, -1).Day() | ||
|
||
t1 := time.Date(currentYear, 1, 1, 0, 0, 0, 0, time.UTC) | ||
t2 := time.Date(currentYear+1, 1, 1, 0, 0, 0, 0, time.UTC) | ||
|
||
daysInYear := int(t2.Sub(t1).Hours() / 24) | ||
|
||
return daysInMonth, daysInYear | ||
} | ||
|
||
func calculateDays5y() int { | ||
now := time.Now().UTC() | ||
currentYear, _, _ := now.Date() | ||
|
||
var daysInYear int | ||
|
||
for i := 0; i < 5; i++ { | ||
t1 := time.Date(currentYear+i, 1, 1, 0, 0, 0, 0, time.UTC) | ||
t2 := time.Date(currentYear+i+1, 1, 1, 0, 0, 0, 0, time.UTC) | ||
|
||
daysInYear += int(t2.Sub(t1).Hours() / 24) | ||
} | ||
|
||
return daysInYear | ||
} |