Skip to content

Commit

Permalink
time.format: new builtin to get string timestamp for ns (open-policy-…
Browse files Browse the repository at this point in the history
…agent#5432)

Adds a builtin time.format to format time given in ns to a string timestamp for
the given timezone or UTC as default. The builtin takes in 3 types of arguments:

1. An integer value representing the time in nanoseconds since epoch
2. An array with the first value as integer representing the time in ns and second argument as string value representing the timezone. In the first case, when only an integer value is provided, UTC timezone is considered
    The function returns a string type value of the timestamp in the RFC3339Nano format. E.g. 2022-11-23T18:20:14Z
3. An array with the first value the integer ns, the second a string timezone, and third a string for the format, to use one different from RFC3339Nano.

Signed-off-by: burnerlee <[email protected]>
  • Loading branch information
burnerlee authored Dec 19, 2022
1 parent 94e3d55 commit 34f6939
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 16 deletions.
16 changes: 16 additions & 0 deletions ast/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ var DefaultBuiltins = [...]*Builtin{
ParseNanos,
ParseRFC3339Nanos,
ParseDurationNanos,
Format,
Date,
Clock,
Weekday,
Expand Down Expand Up @@ -2144,6 +2145,21 @@ var ParseDurationNanos = &Builtin{
),
}

var Format = &Builtin{
Name: "time.format",
Description: "Returns the formatted timestamp for the nanoseconds since epoch.",
Decl: types.NewFunction(
types.Args(
types.Named("x", types.NewAny(
types.N,
types.NewArray([]types.Type{types.N, types.S}, nil),
types.NewArray([]types.Type{types.N, types.S, types.S}, nil),
)).Description("a number representing the nanoseconds since the epoch (UTC); or a two-element array of the nanoseconds, and a timezone string; or a three-element array of ns, timezone string and a layout string (see golang supported time formats)"),
),
types.Named("formatted timestamp", types.S).Description("the formatted timestamp represented for the nanoseconds since the epoch in the supplied timezone (or UTC)"),
),
}

var Date = &Builtin{
Name: "time.date",
Description: "Returns the `[year, month, day]` for the nanoseconds since epoch.",
Expand Down
21 changes: 21 additions & 0 deletions builtin_metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@
"time.clock",
"time.date",
"time.diff",
"time.format",
"time.now_ns",
"time.parse_duration_ns",
"time.parse_ns",
Expand Down Expand Up @@ -13500,6 +13501,26 @@
},
"wasm": false
},
"time.format": {
"args": [
{
"description": "a number representing the nanoseconds since the epoch (UTC); or a two-element array of the nanoseconds, and a timezone string; or a three-element array of ns, timezone string and a layout string (see golang supported time formats)",
"name": "x",
"type": "any\u003cnumber, array\u003cnumber, string\u003e, array\u003cnumber, string, string\u003e\u003e"
}
],
"available": [
"edge"
],
"description": "Returns the formatted timestamp for the nanoseconds since epoch.",
"introduced": "edge",
"result": {
"description": "the formatted timestamp represented for the nanoseconds since the epoch in the supplied timezone (or UTC)",
"name": "formatted timestamp",
"type": "string"
},
"wasm": false
},
"time.now_ns": {
"args": [],
"available": [
Expand Down
44 changes: 44 additions & 0 deletions capabilities.json
Original file line number Diff line number Diff line change
Expand Up @@ -3919,6 +3919,50 @@
"type": "function"
}
},
{
"name": "time.format",
"decl": {
"args": [
{
"of": [
{
"type": "number"
},
{
"static": [
{
"type": "number"
},
{
"type": "string"
}
],
"type": "array"
},
{
"static": [
{
"type": "number"
},
{
"type": "string"
},
{
"type": "string"
}
],
"type": "array"
}
],
"type": "any"
}
],
"result": {
"type": "string"
},
"type": "function"
}
},
{
"name": "time.now_ns",
"decl": {
Expand Down
35 changes: 35 additions & 0 deletions test/cases/testdata/time/test-time-0971.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
cases:
- data:
modules:
- |
package test
time_ns := 1670006453141828752
a := time.format(time_ns)
b := time.format([time_ns, "Asia/Kolkata"])
c := time.format([time_ns,"Asia/Kolkata","Mon Jan 02 15:04:05 -0700 2006"])
note: time/format
query: >
data.test.a = no_timezone;
data.test.b = with_timezone;
data.test.c = with_layout
want_result:
- no_timezone: "2022-12-02T18:40:53.141828752Z"
with_timezone: "2022-12-03T00:10:53.141828752+05:30"
with_layout: "Sat Dec 03 00:10:53 +0530 2022"
- data:
modules:
- |
package generated
p := time.format(1582977600 * 10e12)
note: time/format too big
query: data.generated.p = x
want_error: timestamp too big
want_error_code: eval_builtin_error
strict_error: true
54 changes: 38 additions & 16 deletions topdown/time.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,21 @@ func builtinParseDurationNanos(_ BuiltinContext, operands []*ast.Term, iter func
return iter(ast.NumberTerm(int64ToJSONNumber(int64(value))))
}

func builtinFormat(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
t, layout, err := tzTime(operands[0].Value)
if err != nil {
return err
}
// Using RFC3339Nano time formatting as default
if layout == "" {
layout = time.RFC3339Nano
}
timestamp := t.Format(layout)
return iter(ast.StringTerm(timestamp))
}

func builtinDate(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
t, err := tzTime(operands[0].Value)
t, _, err := tzTime(operands[0].Value)
if err != nil {
return err
}
Expand All @@ -93,7 +106,7 @@ func builtinDate(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) er
}

func builtinClock(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
t, err := tzTime(operands[0].Value)
t, _, err := tzTime(operands[0].Value)
if err != nil {
return err
}
Expand All @@ -103,7 +116,7 @@ func builtinClock(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) e
}

func builtinWeekday(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
t, err := tzTime(operands[0].Value)
t, _, err := tzTime(operands[0].Value)
if err != nil {
return err
}
Expand All @@ -112,7 +125,7 @@ func builtinWeekday(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term)
}

func builtinAddDate(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
t, err := tzTime(operands[0].Value)
t, _, err := tzTime(operands[0].Value)
if err != nil {
return err
}
Expand All @@ -138,11 +151,11 @@ func builtinAddDate(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term)
}

func builtinDiff(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
t1, err := tzTime(operands[0].Value)
t1, _, err := tzTime(operands[0].Value)
if err != nil {
return err
}
t2, err := tzTime(operands[1].Value)
t2, _, err := tzTime(operands[1].Value)
if err != nil {
return err
}
Expand Down Expand Up @@ -203,25 +216,25 @@ func builtinDiff(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) er
ast.IntNumberTerm(hour), ast.IntNumberTerm(min), ast.IntNumberTerm(sec)))
}

func tzTime(a ast.Value) (t time.Time, err error) {
func tzTime(a ast.Value) (t time.Time, lay string, err error) {
var nVal ast.Value
loc := time.UTC

layout := ""
switch va := a.(type) {
case *ast.Array:
if va.Len() == 0 {
return time.Time{}, builtins.NewOperandTypeErr(1, a, "either number (ns) or [number (ns), string (tz)]")
return time.Time{}, layout, builtins.NewOperandTypeErr(1, a, "either number (ns) or [number (ns), string (tz)]")
}

nVal, err = builtins.NumberOperand(va.Elem(0).Value, 1)
if err != nil {
return time.Time{}, err
return time.Time{}, layout, err
}

if va.Len() > 1 {
tzVal, err := builtins.StringOperand(va.Elem(1).Value, 1)
if err != nil {
return time.Time{}, err
return time.Time{}, layout, err
}

tzName := string(tzVal)
Expand All @@ -243,35 +256,43 @@ func tzTime(a ast.Value) (t time.Time, err error) {
loc, err = time.LoadLocation(tzName)
if err != nil {
tzCacheMutex.Unlock()
return time.Time{}, err
return time.Time{}, layout, err
}
tzCache[tzName] = loc
}
tzCacheMutex.Unlock()
}
}

if va.Len() > 2 {
lay, err := builtins.StringOperand(va.Elem(2).Value, 1)
if err != nil {
return time.Time{}, layout, err
}
layout = string(lay)
}

case ast.Number:
nVal = a

default:
return time.Time{}, builtins.NewOperandTypeErr(1, a, "either number (ns) or [number (ns), string (tz)]")
return time.Time{}, layout, builtins.NewOperandTypeErr(1, a, "either number (ns) or [number (ns), string (tz)]")
}

value, err := builtins.NumberOperand(nVal, 1)
if err != nil {
return time.Time{}, err
return time.Time{}, layout, err
}

f := builtins.NumberToFloat(value)
i64, acc := f.Int64()
if acc != big.Exact {
return time.Time{}, fmt.Errorf("timestamp too big")
return time.Time{}, layout, fmt.Errorf("timestamp too big")
}

t = time.Unix(0, i64).In(loc)

return t, nil
return t, layout, nil
}

func int64ToJSONNumber(i int64) json.Number {
Expand All @@ -283,6 +304,7 @@ func init() {
RegisterBuiltinFunc(ast.ParseRFC3339Nanos.Name, builtinTimeParseRFC3339Nanos)
RegisterBuiltinFunc(ast.ParseNanos.Name, builtinTimeParseNanos)
RegisterBuiltinFunc(ast.ParseDurationNanos.Name, builtinParseDurationNanos)
RegisterBuiltinFunc(ast.Format.Name, builtinFormat)
RegisterBuiltinFunc(ast.Date.Name, builtinDate)
RegisterBuiltinFunc(ast.Clock.Name, builtinClock)
RegisterBuiltinFunc(ast.Weekday.Name, builtinWeekday)
Expand Down

0 comments on commit 34f6939

Please sign in to comment.