-
Notifications
You must be signed in to change notification settings - Fork 453
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[query] ParseTime function supports 95%+ of from / until
time formats
#2621
Changes from 25 commits
3fee32d
27f11a1
d98c5f1
3aaaee5
894d8f2
097164c
15f5f68
a8795c3
c405440
abaa0c0
858d337
55b55c1
6f007ed
b91f6cd
31c1948
a871c89
93fdbc4
c520419
5a49e80
6d16b08
eb6eaf7
caf1795
c2ae930
800b95e
d2b3adf
062814c
91cbaeb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,7 +30,14 @@ import ( | |
"github.com/m3db/m3/src/query/graphite/errors" | ||
) | ||
|
||
var reRelativeTime = regexp.MustCompile(`(?i)^\-([0-9]+)(s|min|h|d|w|mon|y)$`) | ||
var reRelativeTime = regexp.MustCompile(`(?i)^\-([0-9]+)(s|min|h|d|w|mon|y)$`) // allows -3min, -4d, etc. | ||
var reTimeOffset = regexp.MustCompile(`(?i)^(\-|\+)([0-9]+)(s|min|h|d|w|mon|y)$`) // -3min, +3min, -4d, +4d | ||
var reMonthAndDay = regexp.MustCompile(`(?i)^(january|february|march|april|may|june|july|august|september|october|november|december)([0-9]{1,2}?)$`) | ||
var reDayOfWeek = regexp.MustCompile(`(?i)^(sunday|monday|tuesday|wednesday|thursday|friday|saturday)$`) | ||
var reDayOfWeekOffset = regexp.MustCompile(`(?i)^(\-|\+)(sunday|monday|tuesday|wednesday|thursday|friday|saturday)$`) // +monday, +thursday, etc | ||
var rePM = regexp.MustCompile(`(?i)^(([0-1]?)([0-9])pm)([[:ascii:]])*$`) // 8pm, 12pm, 8pm monday | ||
var reAM = regexp.MustCompile(`(?i)^(([0-1]?)([0-9])am)([[:ascii:]])*$`) // 2am, 11am, 7am yesterday | ||
var reTimeOfDayWithColon = regexp.MustCompile(`(?i)^(([0-1]?)([0-9]):([0-5])([0-9])((am|pm)?))([[:ascii:]])*$`) // 8:12pm, 11:20am, 2:00am | ||
|
||
var periods = map[string]time.Duration{ | ||
"s": time.Second, | ||
|
@@ -42,6 +49,31 @@ var periods = map[string]time.Duration{ | |
"y": time.Hour * 24 * 365, | ||
} | ||
|
||
var weekdays = map[string]int{ | ||
"sunday": 0, | ||
"monday": 1, | ||
"tuesday": 2, | ||
"wednesday": 3, | ||
"thursday": 4, | ||
"friday": 5, | ||
"saturday": 6, | ||
} | ||
|
||
var months = map[string]int{ | ||
"january": 1, | ||
"february": 2, | ||
"march": 3, | ||
"april": 4, | ||
"may": 5, | ||
"june": 6, | ||
"july": 7, | ||
"august": 8, | ||
"september": 9, | ||
"october": 10, | ||
"november": 11, | ||
"december": 12, | ||
} | ||
|
||
// on Jan 2 15:04:05 -0700 MST 2006 | ||
var formats = []string{ | ||
"15:04_060102", | ||
|
@@ -50,6 +82,7 @@ var formats = []string{ | |
"15:04_02.01.06", | ||
"02.01.06", | ||
"01/02/06", | ||
"01/02/2006", | ||
"060102", | ||
"20060102", | ||
} | ||
|
@@ -74,6 +107,17 @@ func FormatTime(t time.Time) string { | |
return t.Format(formats[0]) | ||
} | ||
|
||
func getWeekDayOffset(weekday string, now time.Time) time.Duration { | ||
expectedDay := weekdays[weekday] | ||
today := int(now.Weekday()) | ||
dayOffset := today - expectedDay | ||
if dayOffset < 0 { | ||
dayOffset += 7 | ||
} | ||
|
||
return time.Duration(dayOffset) * time.Hour * -24 | ||
} | ||
|
||
// ParseTime translates a Graphite from/until string into a timestamp relative to the provide time | ||
func ParseTime(s string, now time.Time, absoluteOffset time.Duration) (time.Time, error) { | ||
if len(s) == 0 { | ||
|
@@ -108,9 +152,153 @@ func ParseTime(s string, now time.Time, absoluteOffset time.Duration) (time.Time | |
return time.Unix(n, 0).UTC(), nil | ||
} | ||
|
||
s = strings.Replace(strings.Replace(strings.ToLower(s), ",", "", -1), " ", "", -1) | ||
ref, offset := s, "" | ||
if strings.Contains(s, "+") { | ||
stringSlice := strings.Split(s, "+") | ||
if len(stringSlice) == 2 { | ||
ref, offset = stringSlice[0], stringSlice[1] | ||
offset = "+" + offset | ||
} | ||
} else if strings.Contains(s, "-") { | ||
stringSlice := strings.Split(s, "-") | ||
if len(stringSlice) == 2 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here |
||
ref, offset = stringSlice[0], stringSlice[1] | ||
offset = "-" + offset | ||
} | ||
} | ||
parsedReference, err := ParseTimeReference(ref, now) | ||
robskillington marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if err == nil { | ||
parsedOffset, err := ParseOffset(offset, now) | ||
if err == nil { | ||
return parsedReference.Add(parsedOffset), nil | ||
} | ||
} | ||
|
||
return now, err | ||
} | ||
|
||
// ParseTimeReference takes a Graphite time reference ("8am", "today", "monday") and returns a time.Time | ||
func ParseTimeReference(ref string, now time.Time) (time.Time, error) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need to add comment for exported symbols. i.e. |
||
var ( | ||
hour = now.Hour() | ||
minute = now.Minute() | ||
refDate time.Time | ||
) | ||
|
||
if ref == "" || ref == "now" { | ||
return now, nil | ||
} | ||
|
||
// check if the time ref fits an absolute time format | ||
for _, format := range formats { | ||
t, err := time.Parse(format, ref) | ||
if err == nil { | ||
return t, nil | ||
} | ||
} | ||
|
||
rawRef := ref | ||
|
||
// Time of Day Reference (8:12pm, 11:20am, 2:00am, etc.) | ||
if reTimeOfDayWithColon.MatchString(rawRef) { | ||
i := strings.Index(ref, ":") | ||
newHour, err := strconv.ParseInt(ref[:i], 10, 0) | ||
if err != nil { | ||
return time.Time{}, err | ||
} | ||
hour = int(newHour) | ||
if len(ref) >= i+3 { | ||
newMinute, err := strconv.ParseInt(ref[i+1:i+3], 10, 32) | ||
if err != nil { | ||
return time.Time{}, err | ||
} | ||
minute = int(newMinute) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this make sure it's not over 60 or is that allowed? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good call. I will throw an error and add a test case for this. |
||
if minute > 59 { | ||
return time.Time{}, errors.NewInvalidParamsError(fmt.Errorf("unknown time reference %s", rawRef)) | ||
} | ||
ref = ref[i+3:] | ||
} | ||
|
||
if len(ref) >= 2 { | ||
if ref[:2] == "am" { | ||
ref = ref[2:] | ||
} else if ref[:2] == "pm" { | ||
hour = (hour + 12) % 24 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this valid for e.g. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good call. I will throw an error and add a test case for this. |
||
ref = ref[2:] | ||
} | ||
} | ||
} | ||
|
||
// Xam or XXam | ||
if reAM.MatchString(rawRef) { | ||
i := strings.Index(ref, "am") | ||
newHour, err := strconv.ParseInt(ref[:i], 10, 32) | ||
if err != nil { | ||
return time.Time{}, err | ||
} | ||
hour = int(newHour) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wouldn't this parse something like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good call. Added a check and an error test case for this. |
||
minute = 0 | ||
ref = ref[i+2:] | ||
} | ||
|
||
// Xpm or XXpm | ||
if rePM.MatchString(rawRef) { | ||
i := strings.Index(ref, "pm") | ||
newHour, err := strconv.ParseInt(ref[:i], 10, 32) | ||
if err != nil { | ||
return time.Time{}, err | ||
} | ||
if newHour > 24 { | ||
return time.Time{}, errors.NewInvalidParamsError(fmt.Errorf("unknown time reference %s", rawRef)) | ||
} | ||
hour = int((newHour + 12) % 24) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it correct to parse There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good call. Added a check and an error test case for this. |
||
minute = 0 | ||
ref = ref[i+2:] | ||
} | ||
|
||
if strings.HasPrefix(ref, "noon") { | ||
hour, minute = 12, 0 | ||
ref = ref[4:] | ||
} else if strings.HasPrefix(ref, "midnight") { | ||
hour, minute = 0, 0 | ||
ref = ref[8:] | ||
} else if strings.HasPrefix(ref, "teatime") { | ||
hour, minute = 16, 0 | ||
ref = ref[7:] | ||
} | ||
|
||
refDate = time.Date(now.Year(), now.Month(), now.Day(), hour, minute, 0, 0, now.Location()) | ||
|
||
// Day reference | ||
if ref == "yesterday" { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should these match against the initial There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, because this function needs to be able to parse "noon yesterday" which will actually be "noonyesterday" when passed in because whitespace gets stripped. Parsing the unshortened ref could break this. |
||
refDate = refDate.Add(time.Hour * -24) | ||
} else if ref == "tomorrow" { | ||
refDate = refDate.Add(time.Hour * 24) | ||
} else if ref == "today" { | ||
return refDate, nil | ||
} else if reMonthAndDay.MatchString(ref) { // monthDay (january10, may6, may06 etc.) | ||
day := 0 | ||
monthString := "" | ||
if val, err := strconv.ParseInt(ref[len(ref)-2:], 10, 64); err == nil { | ||
day = int(val) | ||
monthString = ref[:len(ref)-2] | ||
} else if val, err := strconv.ParseInt(ref[len(ref)-1:], 10, 64); err == nil { | ||
day = int(val) | ||
monthString = ref[:len(ref)-1] | ||
} else { | ||
return refDate, errors.NewInvalidParamsError(fmt.Errorf("day of month required after month name")) | ||
} | ||
refDate = time.Date(refDate.Year(), time.Month(months[monthString]), day, hour, minute, 0, 0, refDate.Location()) | ||
} else if reDayOfWeek.MatchString(ref) { // DayOfWeek (Monday, etc) | ||
refDate = refDate.Add(getWeekDayOffset(ref, refDate)) | ||
} else if ref != "" { | ||
return time.Time{}, errors.NewInvalidParamsError(fmt.Errorf("unknown time reference %s", rawRef)) | ||
} | ||
|
||
return refDate, nil | ||
} | ||
|
||
// ParseDuration parses a duration | ||
func ParseDuration(s string) (time.Duration, error) { | ||
if m := reRelativeTime.FindStringSubmatch(s); len(m) != 0 { | ||
|
@@ -125,3 +313,25 @@ func ParseDuration(s string) (time.Duration, error) { | |
|
||
return 0, errors.NewInvalidParamsError(fmt.Errorf("invalid relative time %s", s)) | ||
} | ||
|
||
// ParseOffset parses a time offset (like a duration, but can be 0 or positive) | ||
func ParseOffset(s string, now time.Time) (time.Duration, error) { | ||
if s == "" { | ||
return time.Duration(0), nil | ||
} | ||
|
||
if m := reTimeOffset.FindStringSubmatch(s); len(m) != 0 { | ||
parity := 1 | ||
if m[1] == "-" { | ||
parity = -1 | ||
} | ||
timeInteger, err := strconv.ParseInt(m[2], 10, 32) | ||
if err != nil { | ||
return 0, errors.NewInvalidParamsError(fmt.Errorf("invalid time offset %v", err)) | ||
} | ||
period := periods[strings.ToLower(m[3])] | ||
return period * time.Duration(timeInteger) * time.Duration(parity), nil | ||
} | ||
|
||
return 0, errors.NewInvalidParamsError(fmt.Errorf("invalid time offset %s", s)) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this error if len != 2?