Skip to content

Commit

Permalink
Time exp2 (#71)
Browse files Browse the repository at this point in the history
At support for timezone override in all time functions (Now default to UTC if not specified, as opposed to mix). Add timeattr function (For things like quarter/week/etc)
  • Loading branch information
zix99 committed Jul 17, 2022
1 parent b524815 commit db66f53
Show file tree
Hide file tree
Showing 4 changed files with 236 additions and 52 deletions.
24 changes: 18 additions & 6 deletions docs/usage/expressions.md
Expand Up @@ -259,24 +259,36 @@ See: [json](json.md) for more information.

### Time

Syntax: `{time str "[format]"}` `{timeformat unixtime "[format]" "[utc]"}` `{duration dur}` `{buckettime str bucket "[format]"}`
Syntax:
`{time str "[format]" "[tz]"}`
`{timeformat unixtime "[format]" "[tz]"}`
`{duration dur}`
`{buckettime str bucket "[format]" "[tz]"}`
`{timeattr unixtime attr [utc/local]"}`

These three time functions provide you a way to parse and manipulate time.

* `time`: Parse a given time-string into a unix second time (default: RFC3339)
* `timeformat`: Takes a unix time, and formats it (default: auto-detection)
* `time`: Parse a given time-string into a unix second time (default: auto-detection)
* `timeformat`: Takes a unix time, and formats it (default: RFC3339)
* `duration`: Use a duration expressed in s,m,h and convert it to seconds eg `{duration 24h}`
* `buckettime`: Truncate the time to a given bucket (*n*ano, *s*econd, *m*inute, *h*our, *d*ay, *mo*nth, *y*ear)
* `timeattr`: Extracts an attribute about a given datetime (weekday, week, yearweek, quarter)

**Timezones:**

The following values are accepted for a `tz` (timezone): `utc`, `local`, or a valid *IANA Time Zone*

By default, all datetimes are processed as UTC, unless explicit in the datetime itself, or overridden via a parameter.

**Format Auto-Detection:**

If the format argument is ommitted or set to "auto", it will attempt to resolve the format of the time.

If the format is unable to be resolved, it must be specific manually with a format below, or a custom format.

If ommitted: The first seen date will determine the format for all dates going forward (faster)
If ommitted or "cache": The first seen date will determine the format for all dates going forward (faster)

If "auto": The date format will always be auto-detected. This can be used if the date could be in different formats (slower)
If "auto": The date format will always be auto-detected each time. This can be used if the date could be in different formats (slower)

**Special Values:**
The time `now` will return the current unix timestamp `{time now}`
Expand All @@ -287,7 +299,7 @@ The time `now` will return the current unix timestamp `{time now}`
ASNIC, UNIX, RUBY, RFC822, RFC822Z, RFC1123, RFC1123Z, RFC3339, RFC3339, RFC3339N, NGINX

**Additional formats for formatting:**
MONTH, DAY, YEAR, HOUR, MINUTE, SECOND, TIMEZONE, NTIMEZONE
MONTH, MONTHNAME, MNTH, DAY, WEEKDAY, WDAY, YEAR, HOUR, MINUTE, SECOND, TIMEZONE, NTIMEZONE

**Custom formats:**
You can provide a custom format using go's well-known date. Here's an exercept from their docs:
Expand Down
1 change: 1 addition & 0 deletions pkg/expressions/stdlib/funcs.go
Expand Up @@ -77,6 +77,7 @@ var StandardFunctions = map[string]KeyBuilderFunction{
"timeformat": KeyBuilderFunction(kfTimeFormat),
"buckettime": KeyBuilderFunction(kfBucketTime),
"duration": KeyBuilderFunction(kfDuration),
"timeattr": KeyBuilderFunction(kfTimeAttr),

// Color and drawing
"color": KeyBuilderFunction(kfColor),
Expand Down
137 changes: 112 additions & 25 deletions pkg/expressions/stdlib/funcsTime.go
Expand Up @@ -28,36 +28,42 @@ var timeFormats = map[string]string{
"NGINX": "_2/Jan/2006:15:04:05 -0700",
// Parts,
"MONTH": "01",
"DAY": "_2",
"MONTHNAME": "January",
"MNTH": "Jan",
"DAY": "02",
"YEAR": "2006",
"HOUR": "15",
"MINUTE": "04",
"SECOND": "05",
"TIMEZONE": "MST",
"NTIMEZONE": "-0700",
"NTZ": "-0700",
"WEEKDAY": "Monday",
"WDAY": "Mon",
}

// namedTimeFormatToFormat converts a string to a go-format. If not listed above, assumes the string is the format
func namedTimeFormatToFormat(f string) string {
if mapped, ok := timeFormats[strings.ToUpper(f)]; ok {
return mapped
}
return f
}

func smartDateParseWrapper(format string, dateStage KeyBuilderStage, f func(time time.Time) string) KeyBuilderStage {
// smartDateParseWrapper wraps different types of date parsing and manipulation into a stage
func smartDateParseWrapper(format string, tz *time.Location, dateStage KeyBuilderStage, f func(time time.Time) string) KeyBuilderStage {
switch strings.ToLower(format) {
case "auto": // Auto will attempt to parse every time
return KeyBuilderStage(func(context KeyBuilderContext) string {
strTime := dateStage(context)
val, err := dateparse.ParseAny(strTime)
val, err := dateparse.ParseIn(strTime, tz)
if err != nil {
return ErrorParsing
}
return f(val)
})

case "": // Empty format will auto-detect on first successful entry
case "", "cache": // Empty format will auto-detect on first successful entry
var atomicFormat atomic.Value
atomicFormat.Store("")

Expand All @@ -79,7 +85,7 @@ func smartDateParseWrapper(format string, dateStage KeyBuilderStage, f func(time
atomicFormat.Store(liveFormat)
}

val, err := time.Parse(liveFormat, strTime)
val, err := time.ParseInLocation(liveFormat, strTime, tz)
if err != nil {
return ErrorParsing
}
Expand All @@ -90,7 +96,7 @@ func smartDateParseWrapper(format string, dateStage KeyBuilderStage, f func(time
parseFormat := namedTimeFormatToFormat(format)
return KeyBuilderStage(func(context KeyBuilderContext) string {
strTime := dateStage(context)
val, err := time.Parse(parseFormat, strTime)
val, err := time.ParseInLocation(parseFormat, strTime, tz)
if err != nil {
return ErrorParsing
}
Expand All @@ -100,6 +106,8 @@ func smartDateParseWrapper(format string, dateStage KeyBuilderStage, f func(time
}

// Parse time into standard unix epoch time (easier to use)
// By default, will attempt to auto-detect and cache format
// {func <time> [format:cache] [tz:utc]}
func kfTimeParse(args []KeyBuilderStage) KeyBuilderStage {
if len(args) < 1 {
return stageError(ErrorArgCount)
Expand All @@ -118,32 +126,42 @@ func kfTimeParse(args []KeyBuilderStage) KeyBuilderStage {

// Specific format denoted
format := EvalStageIndexOrDefault(args, 1, "")
tz, tzOk := parseTimezoneLocation(EvalStageIndexOrDefault(args, 2, ""))
if !tzOk {
return stageError(ErrorParsing)
}

return smartDateParseWrapper(format, args[0], func(t time.Time) string {
return smartDateParseWrapper(format, tz, args[0], func(t time.Time) string {
return strconv.FormatInt(t.Unix(), 10)
})
}

// {func <unixtime> [format:RFC3339] [tz:utc]}
func kfTimeFormat(args []KeyBuilderStage) KeyBuilderStage {
if len(args) < 1 {
return stageError(ErrorArgCount)
}
format := namedTimeFormatToFormat(EvalStageIndexOrDefault(args, 1, defaultTimeFormat))
utc := Truthy(EvalStageIndexOrDefault(args, 2, ""))

tz, tzOk := parseTimezoneLocation(EvalStageIndexOrDefault(args, 2, ""))
if !tzOk {
return stageError(ErrorParsing)
}

return KeyBuilderStage(func(context KeyBuilderContext) string {
strUnixTime := args[0](context)
unixTime, err := strconv.ParseInt(strUnixTime, 10, 64)
if err != nil {
return ErrorType
}
t := time.Unix(unixTime, 0)
if utc {
t = t.UTC()
}

t := time.Unix(unixTime, 0).In(tz)

return t.Format(format)
})
}

// {func <duration_string>}
func kfDuration(args []KeyBuilderStage) KeyBuilderStage {
if len(args) != 1 {
return stageError(ErrorArgCount)
Expand All @@ -161,19 +179,6 @@ func kfDuration(args []KeyBuilderStage) KeyBuilderStage {
})
}

func kfBucketTime(args []KeyBuilderStage) KeyBuilderStage {
if len(args) < 2 {
return stageError(ErrorArgCount)
}

bucketFormat := timeBucketToFormat(EvalStageOrDefault(args[1], "day"))
parseFormat := EvalStageIndexOrDefault(args, 2, "")

return smartDateParseWrapper(parseFormat, args[0], func(t time.Time) string {
return t.Format(bucketFormat)
})
}

func timeBucketToFormat(name string) string {
name = strings.ToLower(name)

Expand All @@ -194,3 +199,85 @@ func timeBucketToFormat(name string) string {
}
return ErrorBucket
}

// {func <time> <bucket> [format:auto] [tz:utc]}
func kfBucketTime(args []KeyBuilderStage) KeyBuilderStage {
if len(args) < 2 {
return stageError(ErrorArgCount)
}

bucketFormat := timeBucketToFormat(EvalStageOrDefault(args[1], "day"))
parseFormat := EvalStageIndexOrDefault(args, 2, "")
tz, tzOk := parseTimezoneLocation(EvalStageIndexOrDefault(args, 3, ""))
if !tzOk {
return stageError(ErrorParsing)
}

return smartDateParseWrapper(parseFormat, tz, args[0], func(t time.Time) string {
return t.Format(bucketFormat)
})
}

// valid time attributes
var attrType = map[string](func(t time.Time) string){
"WEEKDAY": func(t time.Time) string { return strconv.Itoa(int(t.Weekday())) },
"WEEK": func(t time.Time) string {
_, week := t.ISOWeek()
return strconv.Itoa(week)
},
"YEARWEEK": func(t time.Time) string {
year, week := t.ISOWeek()
return strconv.Itoa(year) + "-" + strconv.Itoa(week)
},
"QUARTER": func(t time.Time) string {
month := int(t.Month())
return strconv.Itoa(month/3 + 1)
},
}

// {func <time> <attr> [tz:utc]}
func kfTimeAttr(args []KeyBuilderStage) KeyBuilderStage {
if len(args) < 2 || len(args) > 3 {
return stageError(ErrorArgCount)
}

attrName, hasAttrName := EvalStaticStage(args[1])
if !hasAttrName {
return stageError(ErrorType)
}
tz, tzOk := parseTimezoneLocation(EvalStageIndexOrDefault(args, 2, ""))
if !tzOk {
return stageError(ErrorParsing)
}

attrFunc, hasAttrFunc := attrType[strings.ToUpper(attrName)]
if !hasAttrFunc {
return stageError(ErrorBucket)
}

return KeyBuilderStage(func(context KeyBuilderContext) string {
unixTime, err := strconv.ParseInt(args[0](context), 10, 64)
if err != nil {
return ErrorType
}

t := time.Unix(unixTime, 0).In(tz)

return attrFunc(t)
})
}

// Pass in "", "local", "utc" or a valid unix timezone
func parseTimezoneLocation(tzf string) (loc *time.Location, ok bool) {
switch strings.ToUpper(tzf) {
case "", "UTC":
return time.UTC, true
case "LOCAL":
return time.Local, true
default:
if tz, err := time.LoadLocation(tzf); err == nil {
return tz, true
}
return time.UTC, false
}
}

0 comments on commit db66f53

Please sign in to comment.