Commit f63877f2 by Agnès Toulet Committed by GitHub

Core: Add go-datemath library (#22978)

parent 85dc4e56
......@@ -60,6 +60,7 @@ require (
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337
github.com/stretchr/testify v1.4.0
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf
github.com/timberio/go-datemath v0.1.1-0.20200323150745-74ddef604fff
github.com/ua-parser/uap-go v0.0.0-20190826212731-daf92ba38329
github.com/uber/jaeger-client-go v2.20.1+incompatible
github.com/uber/jaeger-lib v2.2.0+incompatible // indirect
......
......@@ -286,6 +286,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf h1:Z2X3Os7oRzpdJ75iPqWZc0HeJWFYNCvKsfpQwFpRNTA=
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0=
github.com/timberio/go-datemath v0.1.1-0.20200323150745-74ddef604fff h1:QCdUBuN+iKWAB9HqPTkBwyKPPUHDobJ2AuELSNZwd4o=
github.com/timberio/go-datemath v0.1.1-0.20200323150745-74ddef604fff/go.mod h1:m7kjsbCuO4QKP3KLfnxiUZWiOiFXmxj30HeexjL3lc0=
github.com/ua-parser/uap-go v0.0.0-20190826212731-daf92ba38329 h1:VBsKFh4W1JEMz3eLCmM9zOJKZdDkP5W4b3Y4hc7SbZc=
github.com/ua-parser/uap-go v0.0.0-20190826212731-daf92ba38329/go.mod h1:OBcG9bn7sHtXgarhUEb3OfCnNsgtGnkVf41ilSZ3K3E=
github.com/uber/jaeger-client-go v2.20.1+incompatible h1:HgqpYBng0n7tLJIlyT4kPCIv5XgCsF+kai1NnnrJzEU=
......
......@@ -14,6 +14,7 @@ import (
_ "github.com/robfig/cron"
_ "github.com/robfig/cron/v3"
_ "github.com/stretchr/testify/require"
_ "github.com/timberio/go-datemath"
_ "gopkg.in/square/go-jose.v2"
)
......
# Contributing
Bug fixes and other contributions via pull request greatly welcomed!
This library relies on [goyacc](https://godoc.org/golang.org/x/tools/cmd/goyacc) and
[golex](https://godoc.org/modernc.org/golex) for parsing and evaluating datemath grammar.
To install, run:
* `go get golang.org/x/tools/cmd/goyacc modernc.org/golex`
After modifying either the `datemath.l` or `datemath.y` you can rerun `go generate`.
When in doubt on semantics of the library, [Elasticsearch's
implementation](https://www.elastic.co/guide/en/elasticsearch/reference/7.3/common-options.html#date-math) should be
considered the canonical specification.
Copyright (c) 2019, Timber Technologies, Inc.
Permission to use, copy, modify, and/or distribute this software for any purpose
with or without fee is hereby granted, provided that the above copyright notice
and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.
# go-datemath
[![GoDoc](https://godoc.org/github.com/timberio/go-datemath?status.svg)](http://godoc.org/github.com/timberio/go-datemath)
[![Circle CI](https://circleci.com/gh/timberio/go-datemath.svg?style=svg)](https://circleci.com/gh/timberio/go-datemath)
[![Go Report Card](https://goreportcard.com/badge/github.com/timberio/go-datemath)](https://goreportcard.com/report/github.com/timberio/go-datemath)
[![coverage](https://gocover.io/_badge/github.com/timberio/go-datemath?0 "coverage")](http://gocover.io/github.com/timberio/go-datemath)
This library provides support for parsing datemath expressions compatibly with [Elasticsearch datemath
expressions](https://www.elastic.co/guide/en/elasticsearch/reference/7.3/common-options.html#date-math). These are
useful for allowing users to specify, and for encoding, relative dates. Examples:
* `now+15m`: 15 minutes from now
* `now-1w+1d`: one day after on week ago
* `2015-05-05T00:00:00||+1M`: one month after 2019-05-05
These expressions will seem familiar if you have used Grafana or Kibana.
Example usage:
```go
expr, _ := datemath.Parse("now-15m")
fmt.Println(t.Time(datemath.WithNow(now)))
```
See [package documentation](http://godoc.org/github.com/timberio/go-datemath) for usage and more examples.
## Development / Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md).
// Requires golang.org/x/tools/cmd/goyacc and modernc.org/golex
//
//go:generate goyacc -o datemath.y.go datemath.y
//go:generate golex -o datemath.l.go datemath.l
/*
Package datemath provides an expression language for relative dates based on Elasticsearch's date math.
This package is useful for letting end-users describe dates in a simple format similar to Grafana and Kibana and for
persisting them as relative dates.
The expression starts with an anchor date, which can either be "now", or an ISO8601 date string ending with ||. This
anchor date can optionally be followed by one or more date math expressions, for example:
now+1h Add one hour
now-1d Subtract one day
now/d Round down to the nearest day
The supported time units are:
y Years
M Months
w Weeks
d Days
b Business Days (excludes Saturday and Sunday by default, use WithBusinessDayFunc to override)
h Hours
H Hours
m Minutes
s Seconds
Compatibility with Elasticsearch datemath
This package aims to be a superset of Elasticsearch's expressions. That is, any datemath expression that is valid for
Elasticsearch should evaluate in the same way here.
Currently the package does not support expressions outside of those also considered valid by Elasticsearch, but this may
change in the future to include additional functionality.
*/
package datemath
import (
"fmt"
"strconv"
"strings"
"time"
)
func init() {
// have goyacc parser return more verbose syntax error messages
yyErrorVerbose = true
}
var missingTimeZone = time.FixedZone("MISSING", 0)
type timeUnit rune
const (
timeUnitYear = timeUnit('y')
timeUnitMonth = timeUnit('M')
timeUnitWeek = timeUnit('w')
timeUnitDay = timeUnit('d')
timeUnitBusinessDay = timeUnit('b')
timeUnitHour = timeUnit('h')
timeUnitMinute = timeUnit('m')
timeUnitSecond = timeUnit('s')
)
func (u timeUnit) String() string {
return string(u)
}
// Expression represents a parsed datemath expression
type Expression struct {
input string
mathExpression
}
type mathExpression struct {
anchorDateExpression anchorDateExpression
adjustments []timeAdjuster
}
func newMathExpression(anchorDateExpression anchorDateExpression, adjustments []timeAdjuster) mathExpression {
return mathExpression{
anchorDateExpression: anchorDateExpression,
adjustments: adjustments,
}
}
// MarshalJSON implements the json.Marshaler interface
//
// It serializes as the string expression the Expression was created with
func (e Expression) MarshalJSON() ([]byte, error) {
return []byte(strconv.Quote(e.String())), nil
}
// UnmarshalJSON implements the json.Unmarshaler interface
//
// Parses the datemath expression from a JSON string
func (e *Expression) UnmarshalJSON(data []byte) error {
s, err := strconv.Unquote(string(data))
if err != nil {
return err
}
expression, err := Parse(s)
if err != nil {
return nil
}
*e = expression
return nil
}
// String returns a the string used to create the expression
func (e Expression) String() string {
return e.input
}
// Options represesent configurable behavior for interpreting the datemath expression
type Options struct {
// Use this this time as "now"
// Default is `time.Now()`
Now time.Time
// Use this location if there is no timezone in the expression
// Defaults to time.UTC
Location *time.Location
// Use this weekday as the start of the week
// Defaults to time.Monday
StartOfWeek time.Weekday
// Rounding to period should be done to the end of the period
// Defaults to false
RoundUp bool
BusinessDayFunc func(time.Time) bool
}
// WithNow use the given time as "now"
func WithNow(now time.Time) func(*Options) {
return func(o *Options) {
o.Now = now
}
}
// WithStartOfWeek uses the given weekday as the start of the week
func WithStartOfWeek(day time.Weekday) func(*Options) {
return func(o *Options) {
o.StartOfWeek = day
}
}
// WithLocation uses the given location as the timezone of the date if unspecified
func WithLocation(l *time.Location) func(*Options) {
return func(o *Options) {
o.Location = l
}
}
// WithRoundUp sets the rounding of time to the end of the period instead of the beginning
func WithRoundUp(b bool) func(*Options) {
return func(o *Options) {
o.RoundUp = b
}
}
// WithBusinessDayFunc use the given fn to check if a day is a business day
func WithBusinessDayFunc(fn func(time.Time) bool) func(*Options) {
return func(o *Options) {
o.BusinessDayFunc = fn
}
}
func isNotWeekend(t time.Time) bool {
return t.Weekday() != time.Saturday && t.Weekday() != time.Sunday
}
// Time evaluate the expression with the given options to get the time it represents
func (e Expression) Time(opts ...func(*Options)) time.Time {
options := Options{
Now: time.Now(),
Location: time.UTC,
StartOfWeek: time.Monday,
}
for _, opt := range opts {
opt(&options)
}
t := e.anchorDateExpression(options)
for _, adjustment := range e.adjustments {
t = adjustment(t, options)
}
return t
}
// Parse parses the datemath expression which can later be evaluated
func Parse(s string) (Expression, error) {
lex := newLexer([]byte(s))
lexWrapper := newLexerWrapper(lex)
yyParse(lexWrapper)
if len(lex.errors) > 0 {
return Expression{}, fmt.Errorf(strings.Join(lex.errors, "\n"))
}
return Expression{input: s, mathExpression: lexWrapper.expression}, nil
}
// MustParse is the same as Parse() but panic's on error
func MustParse(s string) Expression {
e, err := Parse(s)
if err != nil {
panic(err)
}
return e
}
// ParseAndEvaluate is a convience wrapper to parse and return the time that the expression represents
func ParseAndEvaluate(s string, opts ...func(*Options)) (time.Time, error) {
expression, err := Parse(s)
if err != nil {
return time.Time{}, err
}
return expression.Time(opts...), nil
}
type anchorDateExpression func(opts Options) time.Time
func anchorDateNow(opts Options) time.Time {
return opts.Now.In(opts.Location)
}
func anchorDate(t time.Time) func(opts Options) time.Time {
return func(opts Options) time.Time {
location := t.Location()
if location == missingTimeZone {
location = opts.Location
}
return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), location)
}
}
type timeAdjuster func(time.Time, Options) time.Time
func addUnits(factor int, u timeUnit) func(time.Time, Options) time.Time {
return func(t time.Time, options Options) time.Time {
switch u {
case timeUnitYear:
return t.AddDate(factor, 0, 0)
case timeUnitMonth:
return t.AddDate(0, factor, 0)
case timeUnitWeek:
return t.AddDate(0, 0, 7*factor)
case timeUnitDay:
return t.AddDate(0, 0, factor)
case timeUnitBusinessDay:
fn := options.BusinessDayFunc
if fn == nil {
fn = isNotWeekend
}
increment := 1
if factor < 0 {
increment = -1
}
for i := factor; i != 0; i -= increment {
t = t.AddDate(0, 0, increment)
for !fn(t) {
t = t.AddDate(0, 0, increment)
}
}
return t
case timeUnitHour:
return t.Add(time.Duration(factor) * time.Hour)
case timeUnitMinute:
return t.Add(time.Duration(factor) * time.Minute)
case timeUnitSecond:
return t.Add(time.Duration(factor) * time.Second)
default:
panic(fmt.Sprintf("unknown time unit: %s", u))
}
}
}
func truncateUnits(u timeUnit) func(time.Time, Options) time.Time {
var roundDown = func(t time.Time, options Options) time.Time {
switch u {
case timeUnitYear:
return time.Date(t.Year(), 1, 1, 0, 0, 0, 0, t.Location())
case timeUnitMonth:
return time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location())
case timeUnitWeek:
diff := int(t.Weekday() - options.StartOfWeek)
if diff < 0 {
return time.Date(t.Year(), t.Month(), t.Day()+diff-1, 0, 0, 0, 0, t.Location())
}
return time.Date(t.Year(), t.Month(), t.Day()-diff, 0, 0, 0, 0, t.Location())
case timeUnitDay:
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
case timeUnitHour:
return t.Truncate(time.Hour)
case timeUnitMinute:
return t.Truncate(time.Minute)
case timeUnitSecond:
return t.Truncate(time.Second)
default:
panic(fmt.Sprintf("unknown time unit: %s", u))
}
}
return func(t time.Time, options Options) time.Time {
if options.RoundUp {
return addUnits(1, u)(roundDown(t, options), options).Add(-time.Millisecond)
}
return roundDown(t, options)
}
}
func daysIn(m time.Month, year int) int {
return time.Date(year, m+1, 0, 0, 0, 0, 0, time.UTC).Day()
}
// lexerWrapper wraps the golex generated wrapper to store the parsed expression for later and provide needed data to
// the parser
type lexerWrapper struct {
lex yyLexer
expression mathExpression
}
func newLexerWrapper(lex yyLexer) *lexerWrapper {
return &lexerWrapper{
lex: lex,
}
}
func (l *lexerWrapper) Lex(lval *yySymType) int {
return l.lex.Lex(lval)
}
func (l *lexerWrapper) Error(s string) {
l.lex.Error(s)
}
/*
This file is used with golex to generate a lexer that has a signature compatible with goyacc.
Many constants referred to below are defined by goyacc when creating template.y.go
See https://godoc.org/modernc.org/golex for more about golex
*/
%{
package datemath
import (
"bytes"
"fmt"
"strconv"
)
const (
// 0 is expected by the goyacc generated parser to indicate EOF
eofCode = 0
)
// lexer holds the state of the lexer
type lexer struct {
src *bytes.Reader
buf []byte
current byte
pos int
errors []string
}
func newLexer(b []byte) *lexer {
l := &lexer{
src: bytes.NewReader(b),
}
// queue up a byte
l.next()
return l
}
func (l *lexer) Error(s string) {
l.errors = append(l.errors, fmt.Sprintf("%s at character %d starting with %q", s, l.pos, string(l.buf)))
}
func (l *lexer) next() {
if l.current != 0 {
l.buf = append(l.buf, l.current)
}
l.current = 0
if b, err := l.src.ReadByte(); err == nil {
l.current = b
}
l.pos++
}
func (l *lexer) Lex(lval *yySymType) int {
%}
/* give some regular expressions more semantic names for use below */
eof \0
/* tell golex how to determine the current start condition */
%yyt l.startCondition
/* tell golex how to determine the current byte */
%yyc l.current
/* tell golex how to advance to the next byte */
%yyn l.next()
%%
// runs before each token is parsed
l.buf = l.buf[:0]
[0-9]
i, err := strconv.ParseInt(string(l.buf), 10, 0)
if err != nil {
panic(fmt.Sprintf("could not parse digit as number: %s", err))
}
lval.i = int(i)
return tDIGIT
"now"
return tNOW
"+"
return tPLUS
"-"
return tMINUS
":"
return tCOLON
"||"
return tPIPES
"/"
return tBACKSLASH
[yMwdbhHms]
switch l.buf[0] {
case 'y':
lval.unit = timeUnitYear
case 'M':
lval.unit = timeUnitMonth
case 'w':
lval.unit = timeUnitWeek
case 'b':
lval.unit = timeUnitBusinessDay
case 'd':
lval.unit = timeUnitDay
case 'h', 'H':
lval.unit = timeUnitHour
case 'm':
lval.unit = timeUnitMinute
case 's':
lval.unit = timeUnitSecond
default:
panic(fmt.Sprintf("unknown time unit: %q", l.buf[0]))
}
return tUNIT
\.
return tDOT
"T"
return tTIME_DELIMITER
"Z"
return tUTC
{eof}
return eofCode
.
return tINVALID_TOKEN
%%
// should never get here
panic("scanner internal error")
}
// Code generated by golex. DO NOT EDIT.
/*
This file is used with golex to generate a lexer that has a signature compatible with goyacc.
Many constants referred to below are defined by goyacc when creating template.y.go
See https://godoc.org/modernc.org/golex for more about golex
*/
package datemath
import (
"bytes"
"fmt"
"strconv"
)
const (
// 0 is expected by the goyacc generated parser to indicate EOF
eofCode = 0
)
// lexer holds the state of the lexer
type lexer struct {
src *bytes.Reader
buf []byte
current byte
pos int
errors []string
}
func newLexer(b []byte) *lexer {
l := &lexer{
src: bytes.NewReader(b),
}
// queue up a byte
l.next()
return l
}
func (l *lexer) Error(s string) {
l.errors = append(l.errors, fmt.Sprintf("%s at character %d starting with %q", s, l.pos, string(l.buf)))
}
func (l *lexer) next() {
if l.current != 0 {
l.buf = append(l.buf, l.current)
}
l.current = 0
if b, err := l.src.ReadByte(); err == nil {
l.current = b
}
l.pos++
}
func (l *lexer) Lex(lval *yySymType) int {
/* give some regular expressions more semantic names for use below */
/* tell golex how to determine the current start condition */
/* tell golex how to determine the current byte */
/* tell golex how to advance to the next byte */
yystate0:
// runs before each token is parsed
l.buf = l.buf[:0]
goto yystart1
yystate1:
l.next()
yystart1:
switch {
default:
goto yyabort
case l.current == '+':
goto yystate4
case l.current == '-':
goto yystate5
case l.current == '.':
goto yystate6
case l.current == '/':
goto yystate7
case l.current == ':':
goto yystate9
case l.current == 'H' || l.current == 'M' || l.current == 'b' || l.current == 'd' || l.current == 'h' || l.current == 'm' || l.current == 's' || l.current == 'w' || l.current == 'y':
goto yystate10
case l.current == 'T':
goto yystate11
case l.current == 'Z':
goto yystate12
case l.current == '\x00':
goto yystate2
case l.current == 'n':
goto yystate13
case l.current == '|':
goto yystate16
case l.current >= '0' && l.current <= '9':
goto yystate8
case l.current >= '\x01' && l.current <= '\t' || l.current >= '\v' && l.current <= '*' || l.current == ',' || l.current >= ';' && l.current <= 'G' || l.current >= 'I' && l.current <= 'L' || l.current >= 'N' && l.current <= 'S' || l.current >= 'U' && l.current <= 'Y' || l.current >= '[' && l.current <= 'a' || l.current == 'c' || l.current >= 'e' && l.current <= 'g' || l.current >= 'i' && l.current <= 'l' || l.current >= 'o' && l.current <= 'r' || l.current >= 't' && l.current <= 'v' || l.current == 'x' || l.current == 'z' || l.current == '{' || l.current >= '}' && l.current <= 'ÿ':
goto yystate3
}
yystate2:
l.next()
goto yyrule12
yystate3:
l.next()
goto yyrule13
yystate4:
l.next()
goto yyrule3
yystate5:
l.next()
goto yyrule4
yystate6:
l.next()
goto yyrule9
yystate7:
l.next()
goto yyrule7
yystate8:
l.next()
goto yyrule1
yystate9:
l.next()
goto yyrule5
yystate10:
l.next()
goto yyrule8
yystate11:
l.next()
goto yyrule10
yystate12:
l.next()
goto yyrule11
yystate13:
l.next()
switch {
default:
goto yyrule13
case l.current == 'o':
goto yystate14
}
yystate14:
l.next()
switch {
default:
goto yyabort
case l.current == 'w':
goto yystate15
}
yystate15:
l.next()
goto yyrule2
yystate16:
l.next()
switch {
default:
goto yyrule13
case l.current == '|':
goto yystate17
}
yystate17:
l.next()
goto yyrule6
yyrule1: // [0-9]
{
i, err := strconv.ParseInt(string(l.buf), 10, 0)
if err != nil {
panic(fmt.Sprintf("could not parse digit as number: %s", err))
}
lval.i = int(i)
return tDIGIT
}
yyrule2: // "now"
{
return tNOW
}
yyrule3: // "+"
{
return tPLUS
}
yyrule4: // "-"
{
return tMINUS
}
yyrule5: // ":"
{
return tCOLON
}
yyrule6: // "||"
{
return tPIPES
}
yyrule7: // "/"
{
return tBACKSLASH
}
yyrule8: // [yMwdbhHms]
{
switch l.buf[0] {
case 'y':
lval.unit = timeUnitYear
case 'M':
lval.unit = timeUnitMonth
case 'w':
lval.unit = timeUnitWeek
case 'b':
lval.unit = timeUnitBusinessDay
case 'd':
lval.unit = timeUnitDay
case 'h', 'H':
lval.unit = timeUnitHour
case 'm':
lval.unit = timeUnitMinute
case 's':
lval.unit = timeUnitSecond
default:
panic(fmt.Sprintf("unknown time unit: %q", l.buf[0]))
}
return tUNIT
}
yyrule9: // \.
{
return tDOT
}
yyrule10: // "T"
{
return tTIME_DELIMITER
}
yyrule11: // "Z"
{
return tUTC
}
yyrule12: // {eof}
{
return eofCode
}
yyrule13: // .
if true { // avoid go vet determining the below panic will not be reached
return tINVALID_TOKEN
}
panic("unreachable")
yyabort: // no lexem recognized
//
// silence unused label errors for build and satisfy go vet reachability analysis
//
{
if false {
goto yyabort
}
if false {
goto yystate0
}
if false {
goto yystate1
}
}
// should never get here
panic("scanner internal error")
}
/*
This file is used with goyacc to generate a parser.
See https://godoc.org/golang.org/x/tools/cmd/goyacc for more about goyacc.
*/
%{
package datemath
import (
"fmt"
"math"
"time"
)
var epoch = time.Unix(0, 0).In(time.UTC)
// convert a list of significant digits to an integer
// assumes most to least significant
// e.g. 5,2,3 -> 523
func digitsToInt(digits ...int) int {
n := 0
for i := range digits {
n += digits[i] * int(math.Pow10(len(digits)-i-1))
}
return n
}
%}
/* set of valid tokens; generated constants used by lexer */
%token tNOW tPLUS tMINUS tPIPES tBACKSLASH tTIME_DELIMITER tCOLON tDOT tUNIT tUTC tDIGIT tINVALID_TOKEN
/* Go variables to hold the corresponding token values */
%union {
i64 int64
i int
unit timeUnit
month time.Month
expression mathExpression
anchorDateExpression anchorDateExpression
timeAdjuster timeAdjuster
timeAdjusters []timeAdjuster
location *time.Location
time time.Time
}
/* associate tokens with Go types */
%type <unit> tUNIT
%type <i> sign factor number year day hour minute second nanoseconds tDIGIT
%type <month> month
%type <expression> expression
%type <time> date time absolute_date_expression
%type <i64> millitimestamp
%type <timeAdjusters> date_math_expressions
%type <timeAdjuster> date_math_expression
%type <location> timezone
%error start tINVALID_TOKEN :
"invalid token"
%%
start : expression { // last rule; assign the evaluated time so we can use use it later
yylex.(*lexerWrapper).expression = $1
}
/*
* an expression can be either a:
* * A ISO8601 timestamp (can be truncated)
* * the string "now"
*
* followed by list of date math expressions
*/
expression :
absolute_date_expression {
$$ = newMathExpression(anchorDate($1), nil)
}
| absolute_date_expression tPIPES {
$$ = newMathExpression(anchorDate($1), nil)
}
| absolute_date_expression tPIPES date_math_expressions {
$$ = newMathExpression(anchorDate($1), $3)
}
| tNOW {
$$ = newMathExpression(anchorDateNow, nil)
}
| tNOW date_math_expressions {
$$ = newMathExpression(anchorDateNow, $2)
}
;
/*
* An absolute date expression can be:
* * a unix timestamp in milliseconds
* * a date
* * a time
* * a datetime
* Dates and times can be truncated by leaving off smaller units. For example, 2006 would map to 2006-01-01T00:00:00
*/
absolute_date_expression :
date {
$$ = $1
}
|
time {
$$ = $1
}
|
date tTIME_DELIMITER time timezone {
$$ = time.Date($1.Year(), $1.Month(), $1.Day(), $3.Hour(), $3.Minute(), $3.Second(), $3.Nanosecond(), $4)
}
|
millitimestamp {
$$ = time.Unix($1 / 1000, $1%1000 * 1000000)
}
;
timezone :
/* empty */ {
$$ = missingTimeZone
}
|
sign tDIGIT tDIGIT tCOLON tDIGIT tDIGIT { /* support +/-09:00 style timezone specifiers */
$$ = time.FixedZone("$1$2$3:$5$6", $1 * ((($2 * 10 + $3) * 60 * 60) + (($5 * 10 + $6) * 60)))
}
|
tUTC { /* Z */
$$ = time.UTC
}
;
date :
year {
$$ = time.Date($1, 1, 1, 0, 0, 0, 0, missingTimeZone)
}
|
year tMINUS month {
$$ = time.Date($1, $3, 1, 0, 0, 0, 0, missingTimeZone)
}
|
year tMINUS month tMINUS day {
if $5 > daysIn($3, $1) {
yylex.Error(fmt.Sprintf("day %d out of bounds for month %d", $5, $3))
}
$$ = time.Date($1, $3, $5, 0, 0, 0, 0, missingTimeZone)
}
;
/* store in a time.Time struct using the epoch for the year/month/day */
time :
hour {
$$ = time.Date(epoch.Year(), epoch.Month(), epoch.Day(), $1, 0, 0, 0, missingTimeZone)
}
|
hour tCOLON minute {
$$ = time.Date(epoch.Year(), epoch.Month(), epoch.Day(), $1, $3, 0, 0, missingTimeZone)
}
|
hour tCOLON minute tCOLON second {
$$ = time.Date(epoch.Year(), epoch.Month(), epoch.Day(), $1, $3, $5, 0, missingTimeZone)
}
|
hour tCOLON minute tCOLON second tDOT nanoseconds {
$$ = time.Date(epoch.Year(), epoch.Month(), epoch.Day(), $1, $3, $5, $7, missingTimeZone)
}
;
year :
tDIGIT tDIGIT tDIGIT tDIGIT {
$$ = digitsToInt($1, $2, $3, $4)
}
;
month:
tDIGIT tDIGIT {
$$ = time.Month(digitsToInt($1, $2))
if $$ > 12 {
yylex.Error(fmt.Sprintf("month out of bounds %d", $$))
}
}
;
day:
tDIGIT tDIGIT {
// range validated in `date`
$$ = digitsToInt($1, $2)
}
;
hour:
tDIGIT tDIGIT {
$$ = digitsToInt($1, $2)
if $$ > 23 {
yylex.Error(fmt.Sprintf("hours out of bounds %d", $$))
}
}
;
minute:
tDIGIT tDIGIT {
$$ = digitsToInt($1, $2)
if $$ > 59 {
yylex.Error(fmt.Sprintf("minutes out of bounds %d", $$))
}
}
;
second:
tDIGIT tDIGIT {
$$ = digitsToInt($1, $2)
if $$ > 59 {
yylex.Error(fmt.Sprintf("seconds out of bounds %d", $$))
}
}
;
/* only supports 3 digits for fractional seconds for now */
nanoseconds:
tDIGIT {
$$ = $1 * 100000000
}
|
tDIGIT tDIGIT {
$$ = digitsToInt($1, $2) * 10000000
}
|
tDIGIT tDIGIT tDIGIT {
$$ = digitsToInt($1, $2, $3) * 1000000
}
;
/* allow for list of time adjustments; evaluated from left to right */
date_math_expressions :
date_math_expression date_math_expressions {
$$ = append([]timeAdjuster{$1}, $2...)
/*f, g := $1, $2 // bind to local scope*/
/*$$ = func(t time.Time) time.Time {*/
/*return g(f(t))*/
/*}*/
}
|
date_math_expression {
$$ = []timeAdjuster{$1}
}
;
date_math_expression :
sign factor tUNIT { /* add units; e.g. +15m */
$$ = addUnits($1 * $2, $3)
}
|
tBACKSLASH tUNIT { /* truncate to specified unit: e.g. /d */
$$ = truncateUnits($2)
}
;
sign :
tMINUS {
$$ = -1
}
|
tPLUS {
$$ = 1
}
;
factor :
/* empty */ { /* default to 1 if no integer specified */
$$ = 1
}
|
number {
$$ = $1
}
;
number :
tDIGIT {
$$ = $1
}
|
number tDIGIT {
$$ = $1 * 10 + $2
}
;
/* 5 digits or longer is considered a timestamp */
millitimestamp:
tDIGIT tDIGIT tDIGIT tDIGIT tDIGIT {
$$ = int64(digitsToInt($1, $2, $3, $4, $5))
}
|
millitimestamp tDIGIT {
$$ = $1 * 10 + int64($2)
}
;
%%
module github.com/timberio/go-datemath
go 1.13
......@@ -262,6 +262,8 @@ github.com/stretchr/testify/assert
github.com/stretchr/testify/require
# github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf
github.com/teris-io/shortid
# github.com/timberio/go-datemath v0.1.1-0.20200323150745-74ddef604fff
github.com/timberio/go-datemath
# github.com/ua-parser/uap-go v0.0.0-20190826212731-daf92ba38329
github.com/ua-parser/uap-go/uaparser
# github.com/uber/jaeger-client-go v2.20.1+incompatible
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment