├── .circleci └── config.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── datemath.go ├── datemath.l ├── datemath.l.go ├── datemath.y ├── datemath.y.go ├── datemath_test.go ├── go.mod └── go.sum /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: /go/src/github.com/jszwedko/go-circleci 5 | environment: 6 | TEST_RESULTS: /tmp/test-results 7 | docker: 8 | - image: circleci/golang:1.13 9 | steps: 10 | - checkout 11 | - run: go test ./... -bench . 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | y.output 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Bug fixes and other contributions via pull request greatly welcomed! 4 | 5 | This library relies on [goyacc](https://godoc.org/golang.org/x/tools/cmd/goyacc) and 6 | [golex](https://godoc.org/modernc.org/golex) for parsing and evaluating datemath grammar. 7 | 8 | To install, run: 9 | 10 | * `go install golang.org/x/tools/cmd/goyacc@latest` 11 | * `go install modernc.org/golex@latest` 12 | 13 | After modifying either the `datemath.l` or `datemath.y` you can rerun `go generate`. 14 | 15 | When in doubt on semantics of the library, [Elasticsearch's 16 | implementation](https://www.elastic.co/guide/en/elasticsearch/reference/7.3/common-options.html#date-math) should be 17 | considered the canonical specification. 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, Timber Technologies, Inc. 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose 4 | with or without fee is hereby granted, provided that the above copyright notice 5 | and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 8 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 9 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 11 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 12 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 13 | THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-datemath 2 | 3 | [![GoDoc](https://godoc.org/github.com/timberio/go-datemath?status.svg)](http://godoc.org/github.com/timberio/go-datemath) 4 | [![Circle CI](https://circleci.com/gh/timberio/go-datemath.svg?style=svg)](https://circleci.com/gh/timberio/go-datemath) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/timberio/go-datemath)](https://goreportcard.com/report/github.com/timberio/go-datemath) 6 | [![coverage](https://gocover.io/_badge/github.com/timberio/go-datemath?0 "coverage")](http://gocover.io/github.com/timberio/go-datemath) 7 | 8 | This library provides support for parsing datemath expressions compatibly with [Elasticsearch datemath 9 | expressions](https://www.elastic.co/guide/en/elasticsearch/reference/7.3/common-options.html#date-math). These are 10 | useful for allowing users to specify, and for encoding, relative dates. Examples: 11 | 12 | * `now+15m`: 15 minutes from now 13 | * `now-1w+1d`: one day after on week ago 14 | * `2015-05-05T00:00:00||+1M`: one month after 2019-05-05 15 | 16 | These expressions will seem familiar if you have used Grafana or Kibana. 17 | 18 | Example usage: 19 | 20 | ```go 21 | expr, _ := datemath.Parse("now-15m") 22 | fmt.Println(t.Time(datemath.WithNow(now))) 23 | ``` 24 | 25 | See [package documentation](http://godoc.org/github.com/timberio/go-datemath) for usage and more examples. 26 | 27 | ## Development / Contributing 28 | 29 | See [CONTRIBUTING.md](CONTRIBUTING.md). 30 | -------------------------------------------------------------------------------- /datemath.go: -------------------------------------------------------------------------------- 1 | // Requires golang.org/x/tools/cmd/goyacc and modernc.org/golex 2 | // 3 | //go:generate goyacc -o datemath.y.go datemath.y 4 | //go:generate golex -o datemath.l.go datemath.l 5 | 6 | /* 7 | Package datemath provides an expression language for relative dates based on Elasticsearch's date math. 8 | 9 | This package is useful for letting end-users describe dates in a simple format similar to Grafana and Kibana and for 10 | persisting them as relative dates. 11 | 12 | The expression starts with an anchor date, which can either be "now", or an ISO8601 date string ending with ||. This 13 | anchor date can optionally be followed by one or more date math expressions, for example: 14 | 15 | now+1h Add one hour 16 | now-1d Subtract one day 17 | now/d Round down to the nearest day 18 | 19 | The supported time units are: 20 | y Years 21 | fy Fiscal years (by default same as a regular year, use WithStartOfFiscalYear to override) 22 | Q Annual quarters 23 | fQ Fiscal quarters (by default same as a regular annual quarter, use WithStartOfFiscalYear to override) 24 | M Months 25 | w Weeks 26 | d Days 27 | b Business Days (excludes Saturday and Sunday by default, use WithBusinessDayFunc to override) 28 | h Hours 29 | H Hours 30 | m Minutes 31 | s Seconds 32 | 33 | Compatibility with Elasticsearch datemath 34 | 35 | This package aims to be a superset of Elasticsearch's expressions. That is, any datemath expression that is valid for 36 | Elasticsearch should evaluate in the same way here. 37 | 38 | In addition to the expressions supported by Elasticsearch, this package also supports business days, annual quarters, 39 | and fiscal years/quarters. 40 | */ 41 | package datemath 42 | 43 | import ( 44 | "fmt" 45 | "strconv" 46 | "strings" 47 | "time" 48 | ) 49 | 50 | func init() { 51 | // have goyacc parser return more verbose syntax error messages 52 | yyErrorVerbose = true 53 | } 54 | 55 | var missingTimeZone = time.FixedZone("MISSING", 0) 56 | 57 | type timeUnit string 58 | 59 | const ( 60 | timeUnitYear = timeUnit('y') 61 | timeUnitFiscalYear = timeUnit("fy") 62 | timeUnitQuarter = timeUnit('Q') 63 | timeUnitFiscalQuarter = timeUnit("fQ") 64 | timeUnitMonth = timeUnit('M') 65 | timeUnitWeek = timeUnit('w') 66 | timeUnitDay = timeUnit('d') 67 | timeUnitBusinessDay = timeUnit('b') 68 | timeUnitHour = timeUnit('h') 69 | timeUnitMinute = timeUnit('m') 70 | timeUnitSecond = timeUnit('s') 71 | ) 72 | 73 | func (u timeUnit) String() string { 74 | return string(u) 75 | } 76 | 77 | // Expression represents a parsed datemath expression 78 | type Expression struct { 79 | input string 80 | 81 | mathExpression 82 | } 83 | 84 | type mathExpression struct { 85 | anchorDateExpression anchorDateExpression 86 | adjustments []timeAdjuster 87 | } 88 | 89 | func newMathExpression(anchorDateExpression anchorDateExpression, adjustments []timeAdjuster) mathExpression { 90 | return mathExpression{ 91 | anchorDateExpression: anchorDateExpression, 92 | adjustments: adjustments, 93 | } 94 | } 95 | 96 | // MarshalJSON implements the json.Marshaler interface 97 | // 98 | // It serializes as the string expression the Expression was created with 99 | func (e Expression) MarshalJSON() ([]byte, error) { 100 | return []byte(strconv.Quote(e.String())), nil 101 | } 102 | 103 | // UnmarshalJSON implements the json.Unmarshaler interface 104 | // 105 | // Parses the datemath expression from a JSON string 106 | func (e *Expression) UnmarshalJSON(data []byte) error { 107 | s, err := strconv.Unquote(string(data)) 108 | if err != nil { 109 | return err 110 | } 111 | 112 | expression, err := Parse(s) 113 | if err != nil { 114 | return nil 115 | } 116 | 117 | *e = expression 118 | return nil 119 | } 120 | 121 | // String returns a the string used to create the expression 122 | func (e Expression) String() string { 123 | return e.input 124 | } 125 | 126 | // Options represesent configurable behavior for interpreting the datemath expression 127 | type Options struct { 128 | // Use this this time as "now" 129 | // Default is `time.Now()` 130 | Now time.Time 131 | 132 | // Use this location if there is no timezone in the expression 133 | // Defaults to time.UTC 134 | Location *time.Location 135 | 136 | // Use this weekday as the start of the week 137 | // Defaults to time.Monday 138 | StartOfWeek time.Weekday 139 | 140 | // Rounding to period should be done to the end of the period 141 | // Defaults to false 142 | RoundUp bool 143 | 144 | StartOfFiscalYear time.Time 145 | 146 | BusinessDayFunc func(time.Time) bool 147 | } 148 | 149 | // WithNow use the given time as "now" 150 | func WithNow(now time.Time) func(*Options) { 151 | return func(o *Options) { 152 | o.Now = now 153 | } 154 | } 155 | 156 | // WithStartOfWeek uses the given weekday as the start of the week 157 | func WithStartOfWeek(day time.Weekday) func(*Options) { 158 | return func(o *Options) { 159 | o.StartOfWeek = day 160 | } 161 | } 162 | 163 | // WithLocation uses the given location as the timezone of the date if unspecified 164 | func WithLocation(l *time.Location) func(*Options) { 165 | return func(o *Options) { 166 | o.Location = l 167 | } 168 | } 169 | 170 | // WithRoundUp sets the rounding of time to the end of the period instead of the beginning 171 | func WithRoundUp(b bool) func(*Options) { 172 | return func(o *Options) { 173 | o.RoundUp = b 174 | } 175 | } 176 | 177 | // WithStartOfFiscalYear sets the beginning of the fiscal year. 178 | // The year is ignored. 179 | func WithStartOfFiscalYear(t time.Time) func(*Options) { 180 | return func(o *Options) { 181 | o.StartOfFiscalYear = t 182 | } 183 | } 184 | 185 | // WithBusinessDayFunc use the given fn to check if a day is a business day 186 | func WithBusinessDayFunc(fn func(time.Time) bool) func(*Options) { 187 | return func(o *Options) { 188 | o.BusinessDayFunc = fn 189 | } 190 | } 191 | 192 | func isNotWeekend(t time.Time) bool { 193 | return t.Weekday() != time.Saturday && t.Weekday() != time.Sunday 194 | } 195 | 196 | // Time evaluate the expression with the given options to get the time it represents 197 | func (e Expression) Time(opts ...func(*Options)) time.Time { 198 | options := Options{ 199 | Now: time.Now(), 200 | Location: time.UTC, 201 | StartOfWeek: time.Monday, 202 | } 203 | for _, opt := range opts { 204 | opt(&options) 205 | } 206 | 207 | t := e.anchorDateExpression(options) 208 | for _, adjustment := range e.adjustments { 209 | t = adjustment(t, options) 210 | } 211 | return t 212 | } 213 | 214 | // Parse parses the datemath expression which can later be evaluated 215 | func Parse(s string) (Expression, error) { 216 | lex := newLexer([]byte(s)) 217 | lexWrapper := newLexerWrapper(lex) 218 | 219 | yyParse(lexWrapper) 220 | 221 | if len(lex.errors) > 0 { 222 | return Expression{}, fmt.Errorf(strings.Join(lex.errors, "\n")) 223 | } 224 | 225 | return Expression{input: s, mathExpression: lexWrapper.expression}, nil 226 | } 227 | 228 | // MustParse is the same as Parse() but panic's on error 229 | func MustParse(s string) Expression { 230 | e, err := Parse(s) 231 | if err != nil { 232 | panic(err) 233 | } 234 | return e 235 | } 236 | 237 | // ParseAndEvaluate is a convience wrapper to parse and return the time that the expression represents 238 | func ParseAndEvaluate(s string, opts ...func(*Options)) (time.Time, error) { 239 | expression, err := Parse(s) 240 | if err != nil { 241 | return time.Time{}, err 242 | } 243 | 244 | return expression.Time(opts...), nil 245 | } 246 | 247 | type anchorDateExpression func(opts Options) time.Time 248 | 249 | func anchorDateNow(opts Options) time.Time { 250 | return opts.Now.In(opts.Location) 251 | } 252 | 253 | func anchorDate(t time.Time) func(opts Options) time.Time { 254 | return func(opts Options) time.Time { 255 | location := t.Location() 256 | if location == missingTimeZone { 257 | location = opts.Location 258 | } 259 | 260 | return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), location) 261 | } 262 | } 263 | 264 | type timeAdjuster func(time.Time, Options) time.Time 265 | 266 | func addUnits(factor int, u timeUnit) func(time.Time, Options) time.Time { 267 | return func(t time.Time, options Options) time.Time { 268 | switch u { 269 | case timeUnitYear, timeUnitFiscalYear: 270 | return t.AddDate(factor, 0, 0) 271 | case timeUnitQuarter, timeUnitFiscalQuarter: 272 | return t.AddDate(0, 3*factor, 0) 273 | case timeUnitMonth: 274 | return t.AddDate(0, factor, 0) 275 | case timeUnitWeek: 276 | return t.AddDate(0, 0, 7*factor) 277 | case timeUnitDay: 278 | return t.AddDate(0, 0, factor) 279 | case timeUnitBusinessDay: 280 | 281 | fn := options.BusinessDayFunc 282 | if fn == nil { 283 | fn = isNotWeekend 284 | } 285 | 286 | increment := 1 287 | if factor < 0 { 288 | increment = -1 289 | } 290 | 291 | for i := factor; i != 0; i -= increment { 292 | t = t.AddDate(0, 0, increment) 293 | for !fn(t) { 294 | t = t.AddDate(0, 0, increment) 295 | } 296 | } 297 | 298 | return t 299 | 300 | case timeUnitHour: 301 | return t.Add(time.Duration(factor) * time.Hour) 302 | case timeUnitMinute: 303 | return t.Add(time.Duration(factor) * time.Minute) 304 | case timeUnitSecond: 305 | return t.Add(time.Duration(factor) * time.Second) 306 | default: 307 | panic(fmt.Sprintf("unknown time unit: %s", u)) 308 | } 309 | } 310 | } 311 | 312 | func truncateUnits(u timeUnit) func(time.Time, Options) time.Time { 313 | var roundDown = func(t time.Time, options Options) time.Time { 314 | switch u { 315 | case timeUnitYear: 316 | return time.Date(t.Year(), 1, 1, 0, 0, 0, 0, t.Location()) 317 | case timeUnitFiscalYear: 318 | return firstDayOfFiscalYear(t, options.StartOfFiscalYear) 319 | case timeUnitQuarter: 320 | firstOfQuarter := (t.Month()-1)/3*3 + 1 321 | return time.Date(t.Year(), firstOfQuarter, 1, 0, 0, 0, 0, t.Location()) 322 | case timeUnitFiscalQuarter: 323 | firstDay := firstDayOfFiscalYear(t, options.StartOfFiscalYear) 324 | var mDelta int 325 | if t.Month() >= firstDay.Month() { 326 | mDelta = int(t.Month() - firstDay.Month()) 327 | } else { 328 | mDelta = int(t.Month() + 12 - firstDay.Month()) 329 | } 330 | mDelta = mDelta / 3 * 3 331 | 332 | return firstDay.AddDate(0, mDelta, 0) 333 | case timeUnitMonth: 334 | return time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location()) 335 | case timeUnitWeek: 336 | diff := int(t.Weekday() - options.StartOfWeek) 337 | today := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()) 338 | if diff < 0 { 339 | today = today.AddDate(0, 0, -7) 340 | } 341 | return today.AddDate(0, 0, -diff) 342 | case timeUnitDay: 343 | return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()) 344 | case timeUnitHour: 345 | return t.Truncate(time.Hour) 346 | case timeUnitMinute: 347 | return t.Truncate(time.Minute) 348 | case timeUnitSecond: 349 | return t.Truncate(time.Second) 350 | default: 351 | panic(fmt.Sprintf("unknown time unit: %s", u)) 352 | } 353 | } 354 | 355 | return func(t time.Time, options Options) time.Time { 356 | if options.RoundUp { 357 | return addUnits(1, u)(roundDown(t, options), options).Add(-time.Millisecond) 358 | } 359 | return roundDown(t, options) 360 | } 361 | } 362 | 363 | func daysIn(m time.Month, year int) int { 364 | return time.Date(year, m+1, 0, 0, 0, 0, 0, time.UTC).Day() 365 | } 366 | 367 | func firstDayOfFiscalYear(t time.Time, fy time.Time) time.Time { 368 | d := time.Date(t.Year(), fy.Month(), fy.Day(), fy.Hour(), fy.Minute(), fy.Second(), fy.Nanosecond(), t.Location()) 369 | if d.After(t) { 370 | d = time.Date(t.Year()-1, fy.Month(), fy.Day(), fy.Hour(), fy.Minute(), fy.Second(), fy.Nanosecond(), t.Location()) 371 | } 372 | return d 373 | } 374 | 375 | // lexerWrapper wraps the golex generated wrapper to store the parsed expression for later and provide needed data to 376 | // the parser 377 | type lexerWrapper struct { 378 | lex yyLexer 379 | 380 | expression mathExpression 381 | } 382 | 383 | func newLexerWrapper(lex yyLexer) *lexerWrapper { 384 | return &lexerWrapper{ 385 | lex: lex, 386 | } 387 | } 388 | 389 | func (l *lexerWrapper) Lex(lval *yySymType) int { 390 | return l.lex.Lex(lval) 391 | } 392 | 393 | func (l *lexerWrapper) Error(s string) { 394 | l.lex.Error(s) 395 | } 396 | -------------------------------------------------------------------------------- /datemath.l: -------------------------------------------------------------------------------- 1 | /* 2 | This file is used with golex to generate a lexer that has a signature compatible with goyacc. 3 | 4 | Many constants referred to below are defined by goyacc when creating template.y.go 5 | 6 | See https://godoc.org/modernc.org/golex for more about golex 7 | */ 8 | 9 | %{ 10 | 11 | package datemath 12 | 13 | import ( 14 | "bytes" 15 | "fmt" 16 | "strconv" 17 | ) 18 | 19 | const ( 20 | // 0 is expected by the goyacc generated parser to indicate EOF 21 | eofCode = 0 22 | ) 23 | 24 | // lexer holds the state of the lexer 25 | type lexer struct { 26 | src *bytes.Reader 27 | 28 | buf []byte 29 | current byte 30 | 31 | pos int 32 | 33 | errors []string 34 | } 35 | 36 | func newLexer(b []byte) *lexer { 37 | l := &lexer{ 38 | src: bytes.NewReader(b), 39 | } 40 | // queue up a byte 41 | l.next() 42 | return l 43 | } 44 | 45 | func (l *lexer) Error(s string) { 46 | l.errors = append(l.errors, fmt.Sprintf("%s at character %d starting with %q", s, l.pos, string(l.buf))) 47 | } 48 | 49 | func (l *lexer) next() { 50 | if l.current != 0 { 51 | l.buf = append(l.buf, l.current) 52 | } 53 | l.current = 0 54 | if b, err := l.src.ReadByte(); err == nil { 55 | l.current = b 56 | } 57 | l.pos++ 58 | } 59 | func (l *lexer) Lex(lval *yySymType) int { 60 | %} 61 | 62 | /* give some regular expressions more semantic names for use below */ 63 | eof \0 64 | 65 | /* tell golex how to determine the current start condition */ 66 | %yyt l.startCondition 67 | /* tell golex how to determine the current byte */ 68 | %yyc l.current 69 | /* tell golex how to advance to the next byte */ 70 | %yyn l.next() 71 | 72 | %% 73 | // runs before each token is parsed 74 | l.buf = l.buf[:0] 75 | 76 | [0-9] 77 | i, err := strconv.ParseInt(string(l.buf), 10, 0) 78 | if err != nil { 79 | panic(fmt.Sprintf("could not parse digit as number: %s", err)) 80 | } 81 | lval.i = int(i) 82 | return tDIGIT 83 | 84 | "now" 85 | return tNOW 86 | 87 | "+" 88 | return tPLUS 89 | 90 | "-" 91 | return tMINUS 92 | 93 | ":" 94 | return tCOLON 95 | 96 | "||" 97 | return tPIPES 98 | 99 | "/" 100 | return tBACKSLASH 101 | 102 | [yMwdbhHmsQ] 103 | switch l.buf[0] { 104 | case 'y': 105 | lval.unit = timeUnitYear 106 | case 'Q': 107 | lval.unit = timeUnitQuarter 108 | case 'M': 109 | lval.unit = timeUnitMonth 110 | case 'w': 111 | lval.unit = timeUnitWeek 112 | case 'b': 113 | lval.unit = timeUnitBusinessDay 114 | case 'd': 115 | lval.unit = timeUnitDay 116 | case 'h', 'H': 117 | lval.unit = timeUnitHour 118 | case 'm': 119 | lval.unit = timeUnitMinute 120 | case 's': 121 | lval.unit = timeUnitSecond 122 | default: 123 | panic(fmt.Sprintf("unknown time unit: %q", l.buf[0])) 124 | } 125 | return tUNIT 126 | 127 | "fy" 128 | lval.unit = timeUnitFiscalYear 129 | return tUNIT 130 | 131 | "fQ" 132 | lval.unit = timeUnitFiscalQuarter 133 | return tUNIT 134 | 135 | \. 136 | return tDOT 137 | 138 | "T" 139 | return tTIME_DELIMITER 140 | 141 | "Z" 142 | return tUTC 143 | 144 | {eof} 145 | return eofCode 146 | 147 | no. 148 | return tINVALID_TOKEN 149 | 150 | . 151 | return tINVALID_TOKEN 152 | 153 | %% 154 | 155 | // should never get here 156 | panic("scanner internal error") 157 | } 158 | -------------------------------------------------------------------------------- /datemath.l.go: -------------------------------------------------------------------------------- 1 | // Code generated by golex. DO NOT EDIT. 2 | 3 | /* 4 | This file is used with golex to generate a lexer that has a signature compatible with goyacc. 5 | 6 | Many constants referred to below are defined by goyacc when creating template.y.go 7 | 8 | See https://godoc.org/modernc.org/golex for more about golex 9 | */ 10 | 11 | package datemath 12 | 13 | import ( 14 | "bytes" 15 | "fmt" 16 | "strconv" 17 | ) 18 | 19 | const ( 20 | // 0 is expected by the goyacc generated parser to indicate EOF 21 | eofCode = 0 22 | ) 23 | 24 | // lexer holds the state of the lexer 25 | type lexer struct { 26 | src *bytes.Reader 27 | 28 | buf []byte 29 | current byte 30 | 31 | pos int 32 | 33 | errors []string 34 | } 35 | 36 | func newLexer(b []byte) *lexer { 37 | l := &lexer{ 38 | src: bytes.NewReader(b), 39 | } 40 | // queue up a byte 41 | l.next() 42 | return l 43 | } 44 | 45 | func (l *lexer) Error(s string) { 46 | l.errors = append(l.errors, fmt.Sprintf("%s at character %d starting with %q", s, l.pos, string(l.buf))) 47 | } 48 | 49 | func (l *lexer) next() { 50 | if l.current != 0 { 51 | l.buf = append(l.buf, l.current) 52 | } 53 | l.current = 0 54 | if b, err := l.src.ReadByte(); err == nil { 55 | l.current = b 56 | } 57 | l.pos++ 58 | } 59 | func (l *lexer) Lex(lval *yySymType) int { 60 | 61 | /* give some regular expressions more semantic names for use below */ 62 | /* tell golex how to determine the current start condition */ 63 | /* tell golex how to determine the current byte */ 64 | /* tell golex how to advance to the next byte */ 65 | 66 | yystate0: 67 | 68 | // runs before each token is parsed 69 | l.buf = l.buf[:0] 70 | 71 | goto yystart1 72 | 73 | yystate1: 74 | l.next() 75 | yystart1: 76 | switch { 77 | default: 78 | goto yyabort 79 | case l.current == '+': 80 | goto yystate4 81 | case l.current == '-': 82 | goto yystate5 83 | case l.current == '.': 84 | goto yystate6 85 | case l.current == '/': 86 | goto yystate7 87 | case l.current == ':': 88 | goto yystate9 89 | case l.current == 'H' || l.current == 'M' || l.current == 'Q' || l.current == 'b' || l.current == 'd' || l.current == 'h' || l.current == 'm' || l.current == 's' || l.current == 'w' || l.current == 'y': 90 | goto yystate10 91 | case l.current == 'T': 92 | goto yystate11 93 | case l.current == 'Z': 94 | goto yystate12 95 | case l.current == '\x00': 96 | goto yystate2 97 | case l.current == 'f': 98 | goto yystate13 99 | case l.current == 'n': 100 | goto yystate16 101 | case l.current == '|': 102 | goto yystate20 103 | case l.current >= '0' && l.current <= '9': 104 | goto yystate8 105 | 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 <= 'P' || l.current == 'R' || 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 <= 'ÿ': 106 | goto yystate3 107 | } 108 | 109 | yystate2: 110 | l.next() 111 | goto yyrule14 112 | 113 | yystate3: 114 | l.next() 115 | goto yyrule16 116 | 117 | yystate4: 118 | l.next() 119 | goto yyrule3 120 | 121 | yystate5: 122 | l.next() 123 | goto yyrule4 124 | 125 | yystate6: 126 | l.next() 127 | goto yyrule11 128 | 129 | yystate7: 130 | l.next() 131 | goto yyrule7 132 | 133 | yystate8: 134 | l.next() 135 | goto yyrule1 136 | 137 | yystate9: 138 | l.next() 139 | goto yyrule5 140 | 141 | yystate10: 142 | l.next() 143 | goto yyrule8 144 | 145 | yystate11: 146 | l.next() 147 | goto yyrule12 148 | 149 | yystate12: 150 | l.next() 151 | goto yyrule13 152 | 153 | yystate13: 154 | l.next() 155 | switch { 156 | default: 157 | goto yyrule16 158 | case l.current == 'Q': 159 | goto yystate14 160 | case l.current == 'y': 161 | goto yystate15 162 | } 163 | 164 | yystate14: 165 | l.next() 166 | goto yyrule10 167 | 168 | yystate15: 169 | l.next() 170 | goto yyrule9 171 | 172 | yystate16: 173 | l.next() 174 | switch { 175 | default: 176 | goto yyrule16 177 | case l.current == 'o': 178 | goto yystate17 179 | } 180 | 181 | yystate17: 182 | l.next() 183 | switch { 184 | default: 185 | goto yyabort 186 | case l.current == 'w': 187 | goto yystate19 188 | case l.current >= '\x01' && l.current <= '\t' || l.current >= '\v' && l.current <= 'v' || l.current >= 'x' && l.current <= 'ÿ': 189 | goto yystate18 190 | } 191 | 192 | yystate18: 193 | l.next() 194 | goto yyrule15 195 | 196 | yystate19: 197 | l.next() 198 | goto yyrule2 199 | 200 | yystate20: 201 | l.next() 202 | switch { 203 | default: 204 | goto yyrule16 205 | case l.current == '|': 206 | goto yystate21 207 | } 208 | 209 | yystate21: 210 | l.next() 211 | goto yyrule6 212 | 213 | yyrule1: // [0-9] 214 | { 215 | 216 | i, err := strconv.ParseInt(string(l.buf), 10, 0) 217 | if err != nil { 218 | panic(fmt.Sprintf("could not parse digit as number: %s", err)) 219 | } 220 | lval.i = int(i) 221 | return tDIGIT 222 | } 223 | yyrule2: // "now" 224 | { 225 | 226 | return tNOW 227 | } 228 | yyrule3: // "+" 229 | { 230 | 231 | return tPLUS 232 | } 233 | yyrule4: // "-" 234 | { 235 | 236 | return tMINUS 237 | } 238 | yyrule5: // ":" 239 | { 240 | 241 | return tCOLON 242 | } 243 | yyrule6: // "||" 244 | { 245 | 246 | return tPIPES 247 | } 248 | yyrule7: // "/" 249 | { 250 | 251 | return tBACKSLASH 252 | } 253 | yyrule8: // [yMwdbhHmsQ] 254 | { 255 | 256 | switch l.buf[0] { 257 | case 'y': 258 | lval.unit = timeUnitYear 259 | case 'Q': 260 | lval.unit = timeUnitQuarter 261 | case 'M': 262 | lval.unit = timeUnitMonth 263 | case 'w': 264 | lval.unit = timeUnitWeek 265 | case 'b': 266 | lval.unit = timeUnitBusinessDay 267 | case 'd': 268 | lval.unit = timeUnitDay 269 | case 'h', 'H': 270 | lval.unit = timeUnitHour 271 | case 'm': 272 | lval.unit = timeUnitMinute 273 | case 's': 274 | lval.unit = timeUnitSecond 275 | default: 276 | panic(fmt.Sprintf("unknown time unit: %q", l.buf[0])) 277 | } 278 | return tUNIT 279 | } 280 | yyrule9: // "fy" 281 | { 282 | 283 | lval.unit = timeUnitFiscalYear 284 | return tUNIT 285 | } 286 | yyrule10: // "fQ" 287 | { 288 | 289 | lval.unit = timeUnitFiscalQuarter 290 | return tUNIT 291 | } 292 | yyrule11: // \. 293 | { 294 | 295 | return tDOT 296 | } 297 | yyrule12: // "T" 298 | { 299 | 300 | return tTIME_DELIMITER 301 | } 302 | yyrule13: // "Z" 303 | { 304 | 305 | return tUTC 306 | } 307 | yyrule14: // {eof} 308 | { 309 | 310 | return eofCode 311 | } 312 | yyrule15: // no. 313 | { 314 | 315 | return tINVALID_TOKEN 316 | } 317 | yyrule16: // . 318 | if true { // avoid go vet determining the below panic will not be reached 319 | 320 | return tINVALID_TOKEN 321 | } 322 | panic("unreachable") 323 | 324 | yyabort: // no lexem recognized 325 | // 326 | // silence unused label errors for build and satisfy go vet reachability analysis 327 | // 328 | { 329 | if false { 330 | goto yyabort 331 | } 332 | if false { 333 | goto yystate0 334 | } 335 | if false { 336 | goto yystate1 337 | } 338 | } 339 | 340 | // should never get here 341 | panic("scanner internal error") 342 | } 343 | -------------------------------------------------------------------------------- /datemath.y: -------------------------------------------------------------------------------- 1 | /* 2 | This file is used with goyacc to generate a parser. 3 | 4 | See https://godoc.org/golang.org/x/tools/cmd/goyacc for more about goyacc. 5 | */ 6 | 7 | %{ 8 | package datemath 9 | 10 | import ( 11 | "fmt" 12 | "math" 13 | "time" 14 | ) 15 | 16 | var epoch = time.Unix(0, 0).In(time.UTC) 17 | 18 | // convert a list of significant digits to an integer 19 | // assumes most to least significant 20 | // e.g. 5,2,3 -> 523 21 | func digitsToInt(digits ...int) int { 22 | n := 0 23 | for i := range digits { 24 | n += digits[i] * int(math.Pow10(len(digits)-i-1)) 25 | } 26 | return n 27 | } 28 | %} 29 | 30 | /* set of valid tokens; generated constants used by lexer */ 31 | %token tNOW tPLUS tMINUS tPIPES tBACKSLASH tTIME_DELIMITER tCOLON tDOT tUNIT tUTC tDIGIT tINVALID_TOKEN 32 | 33 | /* Go variables to hold the corresponding token values */ 34 | %union { 35 | i64 int64 36 | i int 37 | unit timeUnit 38 | month time.Month 39 | 40 | expression mathExpression 41 | anchorDateExpression anchorDateExpression 42 | timeAdjuster timeAdjuster 43 | timeAdjusters []timeAdjuster 44 | 45 | location *time.Location 46 | time time.Time 47 | } 48 | 49 | /* associate tokens with Go types */ 50 | %type tUNIT 51 | %type sign factor number year day hour minute second nanoseconds tDIGIT 52 | %type month 53 | %type expression 54 | %type