Skip to content
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

Add duration literal support #499

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions ast/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package ast
import (
"reflect"
"regexp"
"time"

"github.com/expr-lang/expr/file"
)
Expand Down Expand Up @@ -93,6 +94,12 @@ type BoolNode struct {
Value bool // Value of the boolean.
}

// DurationNode represents a duration.
type DurationNode struct {
base
Value time.Duration // Value of the duration.
}

// StringNode represents a string.
type StringNode struct {
base
Expand Down
4 changes: 4 additions & 0 deletions ast/print.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ func (n *BoolNode) String() string {
return fmt.Sprintf("%t", n.Value)
}

func (n *DurationNode) String() string {
return fmt.Sprintf("%v", n.Value)
}

func (n *StringNode) String() string {
return fmt.Sprintf("%q", n.Value)
}
Expand Down
1 change: 1 addition & 0 deletions ast/visitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ func Walk(node *Node, v Visitor) {
case *IdentifierNode:
case *IntegerNode:
case *FloatNode:
case *DurationNode:
case *BoolNode:
case *StringNode:
case *ConstantNode:
Expand Down
49 changes: 35 additions & 14 deletions builtin/builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"math"
"reflect"
"sort"
"strings"
Expand Down Expand Up @@ -154,20 +155,6 @@ var Builtins = []*ast.Function{
return anyType, fmt.Errorf("invalid argument for floor (type %s)", args[0])
},
},
{
Name: "round",
Fast: Round,
Validate: func(args []reflect.Type) (reflect.Type, error) {
if len(args) != 1 {
return anyType, fmt.Errorf("invalid number of arguments (expected 1, got %d)", len(args))
}
switch kind(args[0]) {
case reflect.Float32, reflect.Float64, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Interface:
return floatType, nil
}
return anyType, fmt.Errorf("invalid argument for floor (type %s)", args[0])
},
},
{
Name: "int",
Fast: Int,
Expand Down Expand Up @@ -209,6 +196,40 @@ var Builtins = []*ast.Function{
Fast: String,
Types: types(new(func(any any) string)),
},
{
Name: "round",
Func: func(args ...any) (any, error) {
switch l := len(args); l {
case 1:
a := args[0]
switch a := a.(type) {
case float32:
return math.Round(float64(a)), nil
case float64:
return math.Round(a), nil
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
return Float(a), nil
}
return nil, fmt.Errorf("invalid argument for round (type %T)", a)
case 2:
a, ok := args[0].(time.Duration)
if !ok {
return nil, fmt.Errorf("invalid argument for round (type %T)", args[0])
}
b, ok := args[1].(time.Duration)
if !ok {
return nil, fmt.Errorf("invalid argument for round (type %T)", args[1])
}
return a.Round(b), nil
default:
return nil, fmt.Errorf("invalid number of arguments for round (expected 1 or 2, got %d)", l)
}
},
Types: types(
math.Round,
new(func(time.Duration, time.Duration) time.Duration),
),
},
{
Name: "trim",
Func: func(args ...any) (any, error) {
Expand Down
7 changes: 7 additions & 0 deletions builtin/builtin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,22 +36,27 @@ func TestBuiltin(t *testing.T) {
{`abs(-5)`, 5},
{`abs(.5)`, .5},
{`abs(-.5)`, .5},
{`abs(-24h)`, 24 * time.Hour},
{`ceil(5.5)`, 6.0},
{`ceil(5)`, 5.0},
{`floor(5.5)`, 5.0},
{`floor(5)`, 5.0},
{`round(5.5)`, 6.0},
{`round(5)`, 5.0},
{`round(5.49)`, 5.0},
{`round(24h2m, 1h)`, 24 * time.Hour},
{`int(5.5)`, 5},
{`int(5)`, 5},
{`int("5")`, 5},
{`int(2ns)`, 2},
{`float(5)`, 5.0},
{`float(5.5)`, 5.5},
{`float("5.5")`, 5.5},
{`float(2ns)`, 2.0},
{`string(5)`, "5"},
{`string(5.5)`, "5.5"},
{`string("5.5")`, "5.5"},
{`string(1m2s)`, "1m2s"},
{`trim(" foo ")`, "foo"},
{`trim("__foo___", "_")`, "foo"},
{`trimPrefix("prefix_foo", "prefix_")`, "foo"},
Expand All @@ -75,8 +80,10 @@ func TestBuiltin(t *testing.T) {
{`hasSuffix("foo,bar,baz", "baz")`, true},
{`max(1, 2, 3)`, 3},
{`max(1.5, 2.5, 3.5)`, 3.5},
{`max(3h, 2m, 1s)`, 3 * time.Hour},
{`min(1, 2, 3)`, 1},
{`min(1.5, 2.5, 3.5)`, 1.5},
{`min(3h, 2m, 1s)`, time.Second},
{`sum(1..9)`, 45},
{`sum([.5, 1.5, 2.5])`, 4.5},
{`sum([])`, 0},
Expand Down
24 changes: 12 additions & 12 deletions builtin/func.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"math"
"reflect"
"strconv"
"time"

"github.com/expr-lang/expr/vm/runtime"
)
Expand Down Expand Up @@ -35,6 +36,8 @@ func Type(arg any) any {
}
if v.Type().Name() != "" && v.Type().PkgPath() != "" {
return fmt.Sprintf("%s.%s", v.Type().PkgPath(), v.Type().Name())
} else if v.Type().String() == "time.Duration" {
return "duration"
}
switch v.Type().Kind() {
case reflect.Invalid:
Expand Down Expand Up @@ -135,6 +138,11 @@ func Abs(x any) any {
} else {
return x
}
case time.Duration:
if x.(time.Duration) < 0 {
return -x.(time.Duration)
}
return x
}
panic(fmt.Sprintf("invalid argument for abs (type %T)", x))
}
Expand Down Expand Up @@ -163,18 +171,6 @@ func Floor(x any) any {
panic(fmt.Sprintf("invalid argument for floor (type %T)", x))
}

func Round(x any) any {
switch x := x.(type) {
case float32:
return math.Round(float64(x))
case float64:
return math.Round(x)
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
return Float(x)
}
panic(fmt.Sprintf("invalid argument for round (type %T)", x))
}

func Int(x any) any {
switch x := x.(type) {
case float32:
Expand All @@ -201,6 +197,8 @@ func Int(x any) any {
return int(x)
case uint64:
return int(x)
case time.Duration:
return int(x)
case string:
i, err := strconv.Atoi(x)
if err != nil {
Expand Down Expand Up @@ -244,6 +242,8 @@ func Float(x any) any {
panic(fmt.Sprintf("invalid operation: float(%s)", x))
}
return f
case time.Duration:
return float64(x)
default:
panic(fmt.Sprintf("invalid operation: float(%T)", x))
}
Expand Down
9 changes: 9 additions & 0 deletions checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ func (v *checker) visit(node ast.Node) (reflect.Type, info) {
t, i = v.IntegerNode(n)
case *ast.FloatNode:
t, i = v.FloatNode(n)
case *ast.DurationNode:
t, i = v.DurationNode(n)
case *ast.BoolNode:
t, i = v.BoolNode(n)
case *ast.StringNode:
Expand Down Expand Up @@ -202,6 +204,10 @@ func (v *checker) FloatNode(*ast.FloatNode) (reflect.Type, info) {
return floatType, info{}
}

func (v *checker) DurationNode(*ast.DurationNode) (reflect.Type, info) {
return durationType, info{}
}

func (v *checker) BoolNode(*ast.BoolNode) (reflect.Type, info) {
return boolType, info{}
}
Expand Down Expand Up @@ -233,6 +239,9 @@ func (v *checker) UnaryNode(node *ast.UnaryNode) (reflect.Type, info) {
if isNumber(t) {
return t, info{}
}
if isDuration(t) {
return t, info{}
}
if isAny(t) {
return anyType, info{}
}
Expand Down
6 changes: 6 additions & 0 deletions compiler/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,8 @@ func (c *compiler) compile(node ast.Node) {
c.IntegerNode(n)
case *ast.FloatNode:
c.FloatNode(n)
case *ast.DurationNode:
c.DurationNode(n)
case *ast.BoolNode:
c.BoolNode(n)
case *ast.StringNode:
Expand Down Expand Up @@ -319,6 +321,10 @@ func (c *compiler) FloatNode(node *ast.FloatNode) {
}
}

func (c *compiler) DurationNode(node *ast.DurationNode) {
c.emitPush(node.Value)
}

func (c *compiler) BoolNode(node *ast.BoolNode) {
if node.Value {
c.emit(OpTrue)
Expand Down
30 changes: 30 additions & 0 deletions docs/Language-Definition.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@
<code>"foo"</code>, <code>'bar'</code>
</td>
</tr>
<tr>
<td>Duration</td>
<td>
<code>1h16m7ms</code>
</td>
</tr>
<tr>
<td>Array</td>
<td>
Expand Down Expand Up @@ -358,6 +364,30 @@ date("2023-08-14T00:00:00Z")
date("2023-08-14 00:00:00", "2006-01-02 15:04:05", "Europe/Zurich")
```

## Duration Functions

The following operators can be used to manipulate durations:

```expr
-1h == -1 * 1h
+1h == 1h
2 * 1h == 2h
1h + 1m == 1h1m
1h - 1m == 59m
1h / 10m == 6
1h / 2 == 30m
```

Some number functions (max, min and abs) are compatible with durations as well.

### round(d1, d2)

Returns the result of rounding d1 to the nearest multiple of d2.

```expr
round(24h2m, 1h) == 24h
```

## Number Functions

### max(n1, n2)
Expand Down
84 changes: 84 additions & 0 deletions expr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1031,6 +1031,90 @@ func TestExpr(t *testing.T) {
`duration("1s") * .5`,
5e8,
},
{
`1h == 1h`,
true,
},
{
`TimePlusDay - Time >= 24h`,
true,
},
{
`1h > 1m`,
true,
},
{
`1h < 1m`,
false,
},
{
`1h >= 1m`,
true,
},
{
`1h <= 1m`,
false,
},
{
`1h > 1m`,
true,
},
{
`1h + 1m`,
time.Hour + time.Minute,
},
{
`7 * 1h`,
7 * time.Hour,
},
{
`1h * 7`,
7 * time.Hour,
},
{
`1s * .5`,
5e8,
},
{
"-1h",
-time.Hour,
},
{
"+1h",
time.Hour,
},
{
"1h - 1m",
59 * time.Minute,
},
{
"1h - -1m",
time.Hour + time.Minute,
},
{
"1h / 2 * 2",
time.Hour,
},
{
"1h * 2 / 2",
time.Hour,
},
{
"1h5m / 10m",
6.5,
},
{
`date("2023-08-14") - 24h == date("2023-08-13")`,
true,
},
{
`date("2023-08-13") + 24h == date("2023-08-14")`,
true,
},
{
`let res = 24h; date("2023-08-14") - date("2023-08-13") == res`,
true,
},
{
`1 /* one */ + 2 // two`,
3,
Expand Down
Loading
Loading