Skip to content

Commit

Permalink
built-ins: time.diff function (open-policy-agent#3358)
Browse files Browse the repository at this point in the history
This built-in function makes it possible to get the absolute
difference between to unix timestamps (nanoseconds since epoch)
on the format [year, month, day, hour, minute, second].

Fixes: open-policy-agent#3348

Signed-off-by: Andre Håland <[email protected]>
  • Loading branch information
andrehaland authored Apr 13, 2021
1 parent f7d7e0f commit 458d874
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 0 deletions.
19 changes: 19 additions & 0 deletions ast/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ var DefaultBuiltins = [...]*Builtin{
Clock,
Weekday,
AddDate,
Diff,

// Crypto
CryptoX509ParseCertificates,
Expand Down Expand Up @@ -1721,6 +1722,24 @@ var AddDate = &Builtin{
),
}

// Diff returns the difference [years, months, days, hours, minutes, seconds] between two unix timestamps in nanoseconds
var Diff = &Builtin{
Name: "time.diff",
Decl: types.NewFunction(
types.Args(
types.NewAny(
types.N,
types.NewArray([]types.Type{types.N, types.S}, nil),
),
types.NewAny(
types.N,
types.NewArray([]types.Type{types.N, types.S}, nil),
),
),
types.NewArray([]types.Type{types.N, types.N, types.N, types.N, types.N, types.N}, nil),
),
}

/**
* Crypto.
*/
Expand Down
69 changes: 69 additions & 0 deletions capabilities.json
Original file line number Diff line number Diff line change
Expand Up @@ -2910,6 +2910,75 @@
"type": "function"
}
},
{
"name": "time.diff",
"decl": {
"args": [
{
"of": [
{
"type": "number"
},
{
"static": [
{
"type": "number"
},
{
"type": "string"
}
],
"type": "array"
}
],
"type": "any"
},
{
"of": [
{
"type": "number"
},
{
"static": [
{
"type": "number"
},
{
"type": "string"
}
],
"type": "array"
}
],
"type": "any"
}
],
"result": {
"static": [
{
"type": "number"
},
{
"type": "number"
},
{
"type": "number"
},
{
"type": "number"
},
{
"type": "number"
},
{
"type": "number"
}
],
"type": "array"
},
"type": "function"
}
},
{
"name": "time.now_ns",
"decl": {
Expand Down
1 change: 1 addition & 0 deletions docs/content/policy-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,7 @@ result_valid_hs256 := io.jwt.verify_hs256(result_hs256, "foo")
| <span class="opa-keep-it-together">``output := time.clock(ns)``<br/>``output := time.clock([ns, tz])``</span> | ``output`` is of the form ``[hour, minute, second]``, which outputs the ``hour``, ``minute`` (0-59), and ``second`` (0-59) as ``number``s representing the time of day for the nanoseconds since epoch (``ns``) in the timezone (``tz``), if supplied, or as UTC. | ``SDK-dependent`` |
| <span class="opa-keep-it-together">``day := time.weekday(ns)``<br/>``day := time.weekday([ns, tz])``</span> | outputs the ``day`` as ``string`` representing the day of the week for the nanoseconds since epoch (``ns``) in the timezone (``tz``), if supplied, or as UTC. | ``SDK-dependent`` |
| <span class="opa-keep-it-together">``output := time.add_date(ns, years, months, days)``</span> | ``output`` is a ``number`` representing the time since epoch in nanoseconds after adding the ``years``, ``months`` and ``days`` to ``ns``. See the [Go `time` package documentation](https://golang.org/pkg/time/#Time.AddDate) for more details on ``add_date``. | ``SDK-dependent`` |
| <span class="opa-keep-it-together">``output := time.diff(ns1, ns2)``<br/>``output := time.diff([ns1, tz1], [ns2, tz2])``</span> | ``output`` is of the form ``[year(s), month(s), day(s), hour(s), minute(s), second(s)]``, which outputs ``year(s)``, ``month(s)`` (0-11), ``day(s)`` (0-30), ``hour(s)``(0-23), ``minute(s)``(0-59) and ``second(s)``(0-59) as ``number``s representing the difference between the the two timestamps in nanoseconds since epoch (``ns1`` and ``ns2``), in the timezones (``tz1`` and ``tz2``, respectively), if supplied, or as UTC. | ``SDK-dependent`` |

> Multiple calls to the `time.now_ns` built-in function within a single policy
evaluation query will always return the same value.
Expand Down
41 changes: 41 additions & 0 deletions test/cases/testdata/time/test-time-0970.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
cases:
- data:
modules:
- |
package test
layout := "2006-01-02"
a = minute_second {
minute_second := time.diff(time.now_ns()+61*1000*1000*1000, time.now_ns())
}
b = different_tz {
different_tz := time.diff([time.now_ns()+60*1000*1000*1000, "UTC"], [time.now_ns(), "Asia/Shanghai"])
}
c = leap_year {
leap_year := time.diff(time.parse_ns(layout, "2020-02-02"), time.parse_ns(layout, "2020-03-01"))
}
d = not_leap_year {
not_leap_year := time.diff(time.parse_ns(layout, "2021-02-02"), time.parse_ns(layout, "2021-03-01"))
}
e = leap_year_one_day {
leap_year_one_day := time.diff(time.parse_ns(layout, "2004-02-29"), time.parse_ns(layout, "2005-03-01"))
}
note: time/diff
query: >
data.test.a = minute_second;
data.test.b = different_tz;
data.test.c = leap_year;
data.test.d = not_leap_year;
data.test.e = leap_year_one_day
want_result:
- minute_second: [0,0,0,0,1,1]
different_tz: [0,0,0,0,1,0]
leap_year: [0,0,28,0,0,0]
not_leap_year: [0,0,27,0,0,0]
leap_year_one_day: [1,0,1,0,0,0]
67 changes: 67 additions & 0 deletions topdown/time.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,72 @@ func builtinAddDate(bctx BuiltinContext, operands []*ast.Term, iter func(*ast.Te
return iter(ast.NewTerm(ast.Number(int64ToJSONNumber(result.UnixNano()))))
}

func builtinDiff(bctx BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
t1, err := tzTime(operands[0].Value)
if err != nil {
return err
}
t2, err := tzTime(operands[1].Value)
if err != nil {
return err
}

// The following implementation of this function is taken
// from https://github.com/icza/gox licensed under Apache 2.0.
// The only modification made is to variable names.
//
// For details, see https://stackoverflow.com/a/36531443/1705598
//
// Copyright 2021 icza
// BEGIN REDISTRIBUTION FROM APACHE 2.0 LICENSED PROJECT
if t1.Location() != t2.Location() {
t2 = t2.In(t1.Location())
}
if t1.After(t2) {
t1, t2 = t2, t1
}
y1, M1, d1 := t1.Date()
y2, M2, d2 := t2.Date()

h1, m1, s1 := t1.Clock()
h2, m2, s2 := t2.Clock()

year := y2 - y1
month := int(M2 - M1)
day := d2 - d1
hour := h2 - h1
min := m2 - m1
sec := s2 - s1

// Normalize negative values
if sec < 0 {
sec += 60
min--
}
if min < 0 {
min += 60
hour--
}
if hour < 0 {
hour += 24
day--
}
if day < 0 {
// Days in month:
t := time.Date(y1, M1, 32, 0, 0, 0, 0, time.UTC)
day += 32 - t.Day()
month--
}
if month < 0 {
month += 12
year--
}
// END REDISTRIBUTION FROM APACHE 2.0 LICENSED PROJECT

return iter(ast.ArrayTerm(ast.IntNumberTerm(year), ast.IntNumberTerm(month), ast.IntNumberTerm(day),
ast.IntNumberTerm(hour), ast.IntNumberTerm(min), ast.IntNumberTerm(sec)))
}

func tzTime(a ast.Value) (t time.Time, err error) {
var nVal ast.Value
loc := time.UTC
Expand Down Expand Up @@ -208,6 +274,7 @@ func init() {
RegisterFunctionalBuiltin1(ast.Clock.Name, builtinClock)
RegisterFunctionalBuiltin1(ast.Weekday.Name, builtinWeekday)
RegisterBuiltinFunc(ast.AddDate.Name, builtinAddDate)
RegisterBuiltinFunc(ast.Diff.Name, builtinDiff)
tzCacheMutex = &sync.Mutex{}
tzCache = make(map[string]*time.Location)
}

0 comments on commit 458d874

Please sign in to comment.