Skip to content

Commit 5b538a7

Browse files
committed
Add default timezone option expr.Timezone()
1 parent 1f31cc5 commit 5b538a7

File tree

8 files changed

+141
-16
lines changed

8 files changed

+141
-16
lines changed

builtin/builtin.go

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -472,9 +472,27 @@ var Builtins = []*Function{
472472
{
473473
Name: "now",
474474
Func: func(args ...any) (any, error) {
475-
return time.Now(), nil
475+
if len(args) == 0 {
476+
return time.Now(), nil
477+
}
478+
if len(args) == 1 {
479+
if tz, ok := args[0].(*time.Location); ok {
480+
return time.Now().In(tz), nil
481+
}
482+
}
483+
return nil, fmt.Errorf("invalid number of arguments (expected 0, got %d)", len(args))
484+
},
485+
Validate: func(args []reflect.Type) (reflect.Type, error) {
486+
if len(args) == 0 {
487+
return timeType, nil
488+
}
489+
if len(args) == 1 {
490+
if args[0].AssignableTo(locationType) {
491+
return timeType, nil
492+
}
493+
}
494+
return anyType, fmt.Errorf("invalid number of arguments (expected 0, got %d)", len(args))
476495
},
477-
Types: types(new(func() time.Time)),
478496
},
479497
{
480498
Name: "duration",
@@ -486,9 +504,17 @@ var Builtins = []*Function{
486504
{
487505
Name: "date",
488506
Func: func(args ...any) (any, error) {
507+
tz, ok := args[0].(*time.Location)
508+
if ok {
509+
args = args[1:]
510+
}
511+
489512
date := args[0].(string)
490513
if len(args) == 2 {
491514
layout := args[1].(string)
515+
if tz != nil {
516+
return time.ParseInLocation(layout, date, tz)
517+
}
492518
return time.Parse(layout, date)
493519
}
494520
if len(args) == 3 {
@@ -516,18 +542,32 @@ var Builtins = []*Function{
516542
time.RFC1123,
517543
}
518544
for _, layout := range layouts {
519-
t, err := time.Parse(layout, date)
520-
if err == nil {
521-
return t, nil
545+
if tz == nil {
546+
t, err := time.Parse(layout, date)
547+
if err == nil {
548+
return t, nil
549+
}
550+
} else {
551+
t, err := time.ParseInLocation(layout, date, tz)
552+
if err == nil {
553+
return t, nil
554+
}
522555
}
523556
}
524557
return nil, fmt.Errorf("invalid date %s", date)
525558
},
526-
Types: types(
527-
new(func(string) time.Time),
528-
new(func(string, string) time.Time),
529-
new(func(string, string, string) time.Time),
530-
),
559+
Validate: func(args []reflect.Type) (reflect.Type, error) {
560+
if len(args) < 1 {
561+
return anyType, fmt.Errorf("invalid number of arguments (expected at least 1, got %d)", len(args))
562+
}
563+
if args[0].AssignableTo(locationType) {
564+
args = args[1:]
565+
}
566+
if len(args) > 3 {
567+
return anyType, fmt.Errorf("invalid number of arguments (expected at most 3, got %d)", len(args))
568+
}
569+
return timeType, nil
570+
},
531571
},
532572
{
533573
Name: "timezone",

builtin/builtin_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ func TestBuiltin_works_with_any(t *testing.T) {
170170
config := map[string]struct {
171171
arity int
172172
}{
173+
"now": {0},
173174
"get": {2},
174175
"take": {2},
175176
"sortBy": {2},

builtin/utils.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@ package builtin
33
import (
44
"fmt"
55
"reflect"
6+
"time"
67
)
78

89
var (
9-
anyType = reflect.TypeOf(new(any)).Elem()
10-
integerType = reflect.TypeOf(0)
11-
floatType = reflect.TypeOf(float64(0))
12-
arrayType = reflect.TypeOf([]any{})
13-
mapType = reflect.TypeOf(map[any]any{})
10+
anyType = reflect.TypeOf(new(any)).Elem()
11+
integerType = reflect.TypeOf(0)
12+
floatType = reflect.TypeOf(float64(0))
13+
arrayType = reflect.TypeOf([]any{})
14+
mapType = reflect.TypeOf(map[any]any{})
15+
timeType = reflect.TypeOf(new(time.Time)).Elem()
16+
locationType = reflect.TypeOf(new(time.Location))
1417
)
1518

1619
func kind(t reflect.Type) reflect.Kind {

expr.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"errors"
55
"fmt"
66
"reflect"
7+
"time"
78

89
"github.com/expr-lang/expr/ast"
910
"github.com/expr-lang/expr/builtin"
@@ -183,6 +184,17 @@ func WithContext(name string) Option {
183184
})
184185
}
185186

187+
// Timezone sets default timezone for date() and now() builtin functions.
188+
func Timezone(name string) Option {
189+
tz, err := time.LoadLocation(name)
190+
if err != nil {
191+
panic(err)
192+
}
193+
return Patch(patcher.WithTimezone{
194+
Location: tz,
195+
})
196+
}
197+
186198
// Compile parses and compiles given input expression to bytecode program.
187199
func Compile(input string, ops ...Option) (*vm.Program, error) {
188200
config := conf.CreateNew()

expr_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,23 @@ func ExampleWithContext() {
584584
// Output: 42
585585
}
586586

587+
func ExampleWithTimezone() {
588+
program, err := expr.Compile(`now().Location().String()`, expr.Timezone("Asia/Kamchatka"))
589+
if err != nil {
590+
fmt.Printf("%v", err)
591+
return
592+
}
593+
594+
output, err := expr.Run(program, nil)
595+
if err != nil {
596+
fmt.Printf("%v", err)
597+
return
598+
}
599+
600+
fmt.Printf("%v", output)
601+
// Output: Asia/Kamchatka
602+
}
603+
587604
func TestExpr_readme_example(t *testing.T) {
588605
env := map[string]any{
589606
"greet": "Hello, %v!",

patcher/with_timezone.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package patcher
2+
3+
import (
4+
"time"
5+
6+
"github.com/expr-lang/expr/ast"
7+
)
8+
9+
// WithTimezone passes Location to date() and now() functions.
10+
type WithTimezone struct {
11+
Location *time.Location
12+
}
13+
14+
func (t WithTimezone) Visit(node *ast.Node) {
15+
if btin, ok := (*node).(*ast.BuiltinNode); ok {
16+
switch btin.Name {
17+
case "date", "now":
18+
loc := &ast.ConstantNode{Value: t.Location}
19+
ast.Patch(node, &ast.BuiltinNode{
20+
Name: btin.Name,
21+
Arguments: append([]ast.Node{loc}, btin.Arguments...),
22+
})
23+
}
24+
}
25+
}

patcher/with_timezone_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package patcher_test
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/expr-lang/expr"
10+
)
11+
12+
func TestWithTimezone_date(t *testing.T) {
13+
program, err := expr.Compile(`date("2024-05-07 23:00:00")`, expr.Timezone("Europe/Zurich"))
14+
require.NoError(t, err)
15+
16+
out, err := expr.Run(program, nil)
17+
require.NoError(t, err)
18+
require.Equal(t, "2024-05-07T23:00:00+02:00", out.(time.Time).Format(time.RFC3339))
19+
}
20+
21+
func TestWithTimezone_now(t *testing.T) {
22+
program, err := expr.Compile(`now()`, expr.Timezone("Asia/Kamchatka"))
23+
require.NoError(t, err)
24+
25+
out, err := expr.Run(program, nil)
26+
require.NoError(t, err)
27+
require.Equal(t, "Asia/Kamchatka", out.(time.Time).Location().String())
28+
}

testdata/examples.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7231,7 +7231,6 @@ get(false ? f64 : 1, ok)
72317231
get(false ? f64 : score, add)
72327232
get(false ? false : f32, i)
72337233
get(false ? i32 : list, i64)
7234-
get(false ? i32 : ok, now(div, array))
72357234
get(false ? i64 : foo, Bar)
72367235
get(false ? i64 : true, f64)
72377236
get(false ? score : ok, trimSuffix("bar", "bar"))

0 commit comments

Comments
 (0)