Skip to content

Commit

Permalink
✨ parse.duration (#2956)
Browse files Browse the repository at this point in the history
This now supports parsing durations in seconds (s), minutes (m), hours (m), days (d), and years (y). This works with both the shorthand scalar as well as the long version, e.g.:

```coffee
parse.duration("3d")
parse.duration("3days")
```

The parser is very lenient, but we recommend using the above scalars as
so:

```coffee
1s = 1 second
2m = 2 minutes
3h = 3 hours
4d = 4 days
5y = 5 years
```

Signed-off-by: Dominik Richter <[email protected]>
  • Loading branch information
arlimus authored Jan 6, 2024
1 parent 309c066 commit 88dc0b4
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 3 deletions.
3 changes: 2 additions & 1 deletion llx/builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -694,7 +694,8 @@ func init() {
return e.runBlock(bind, chunk.Function.Args[0], chunk.Function.Args[1:], ref)
}},
// TODO: [#32] unique builtin fields that need a long-term support in LR
string(types.Resource("parse") + ".date"): {f: resourceDateV2},
string(types.Resource("parse") + ".date"): {f: resourceDateV2},
string(types.Resource("parse") + ".duration"): {f: resourceDuration},
},
}

Expand Down
41 changes: 41 additions & 0 deletions llx/builtin_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ package llx

import (
"errors"
"regexp"
"strconv"
"strings"
"time"

"go.mondoo.com/cnquery/v9/types"
Expand Down Expand Up @@ -282,3 +284,42 @@ func resourceDateV2(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (

return nil, 0, errors.New("failed to parse time")
}

var durationRegex = regexp.MustCompile(`^(\d+|[.])(\w*)$`)

func resourceDuration(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) {
args, rref, err := primitive2array(e, ref, chunk.Function.Args)
if err != nil || rref != 0 {
return nil, rref, err
}

// Note: Using the regex is slower than parsing it step by step, so this code can be improved
m := durationRegex.FindStringSubmatch(args[0].(string))
if m == nil {
return nil, 0, errors.New("failed to parse duration")
}

num, err := strconv.ParseFloat(m[1], 64)
if err != nil {
return nil, 0, errors.New("failed to parse duration numeric value")
}

var t time.Time
scalar := strings.ToLower(m[2])
switch scalar {
case "s", "", "sec", "second", "seconds":
t = DurationToTime(int64(num))
case "m", "min", "minute", "minutes":
t = DurationToTime(int64(num * 60))
case "h", "hour", "hours":
t = DurationToTime(int64(num * 60 * 60))
case "d", "day", "days":
t = DurationToTime(int64(num * 60 * 60 * 24))
case "y", "year", "years":
t = DurationToTime(int64(num * 60 * 60 * 24 * 365))
default:
return nil, 0, errors.New("failed to parsee duration (only supports: s/m/h/d/y)")
}

return TimeData(t), 0, nil
}
3 changes: 2 additions & 1 deletion mqlc/builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ func init() {
},
// TODO: [#32] unique builtin fields that need a long-term support in LR
types.Resource("parse"): {
"date": {compile: compileResourceParseDate, signature: FunctionSignature{Required: 1, Args: []types.Type{types.String, types.String}}},
"date": {compile: compileResourceParseDate, signature: FunctionSignature{Required: 1, Args: []types.Type{types.String, types.String}}},
"duration": {compile: compileResourceParseDuration, signature: FunctionSignature{Required: 1, Args: []types.Type{types.String}}},
},
}
}
Expand Down
38 changes: 38 additions & 0 deletions mqlc/builtin_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -505,3 +505,41 @@ func compileResourceParseDate(c *compiler, typ types.Type, ref uint64, id string
})
return types.Time, nil
}

func compileResourceParseDuration(c *compiler, typ types.Type, ref uint64, id string, call *parser.Call) (types.Type, error) {
if call == nil {
return types.Nil, errors.New("missing arguments to parse duration")
}

functionID := string(typ) + "." + id

init := &resources.Init{
Args: []*resources.TypedArg{
{Name: "value", Type: string(types.String)},
},
}
args, err := c.unnamedArgs("parse."+id, init, call.Function)
if err != nil {
return types.Nil, err
}

rawArgs := make([]*llx.Primitive, len(call.Function))
for i := range call.Function {
rawArgs[i] = args[i*2+1]
}

if len(rawArgs) == 0 {
return types.Nil, errors.New("missing arguments to parse duration")
}

c.addChunk(&llx.Chunk{
Call: llx.Chunk_FUNCTION,
Id: functionID,
Function: &llx.Function{
Type: string(types.Time),
Binding: ref,
Args: rawArgs,
},
})
return types.Time, nil
}
2 changes: 1 addition & 1 deletion mqlc/mqlc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1954,7 +1954,7 @@ func TestSuggestions(t *testing.T) {
{
// builtin calls
"parse.d",
[]string{"date"},
[]string{"date", "duration"},
errors.New("cannot find field 'd' in parse"),
nil,
},
Expand Down
1 change: 1 addition & 0 deletions providers/core/resources/core.lr
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ regex {
parse {
// Built-in functions:
// date(value, format) time
// duration(value) time
}

// UUIDs based on RFC 4122 and DCE 1.1
Expand Down
41 changes: 41 additions & 0 deletions providers/core/resources/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"testing"
"time"

"go.mondoo.com/cnquery/v9/llx"
"go.mondoo.com/cnquery/v9/providers-sdk/v1/testutils"
)

Expand Down Expand Up @@ -34,3 +35,43 @@ func TestParse_Date(t *testing.T) {
},
})
}

func TestParse_Duration(t *testing.T) {
twoSecs := llx.DurationToTime(2)
tenMin := llx.DurationToTime(10 * 60)
threeHours := llx.DurationToTime(3 * 60 * 60)
thirtyDays := llx.DurationToTime(30 * 60 * 60 * 24)
sevenYears := llx.DurationToTime(7 * 60 * 60 * 24 * 365)
x.TestSimple(t, []testutils.SimpleTest{
{
Code: "parse.duration('2')",
ResultIndex: 0,
Expectation: &twoSecs,
},
{
Code: "parse.duration('2seconds')",
ResultIndex: 0,
Expectation: &twoSecs,
},
{
Code: "parse.duration('10min')",
ResultIndex: 0,
Expectation: &tenMin,
},
{
Code: "parse.duration('3h')",
ResultIndex: 0,
Expectation: &threeHours,
},
{
Code: "parse.duration('30day')",
ResultIndex: 0,
Expectation: &thirtyDays,
},
{
Code: "parse.duration('7y')",
ResultIndex: 0,
Expectation: &sevenYears,
},
})
}

0 comments on commit 88dc0b4

Please sign in to comment.