Skip to content
Merged
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
1 change: 1 addition & 0 deletions funcs/cel_exports.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

60 changes: 60 additions & 0 deletions funcs/time.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,66 @@ func (TimeFuncs) Until(n gotime.Time) gotime.Duration {
return gotime.Until(n)
}

// InTimeRange reports whether the time of day of t falls within [start, end] (both inclusive).
// start and end are "HH:MM" or "HH:MM:SS" strings.
//
// Examples:
//
// InTimeRange(t, "09:00", "17:00") → true for 9:00:00–17:00:59
// InTimeRange(t, "09:30", "17:30") → true for 9:30:00–17:30:59
// InTimeRange(t, "09:00:00", "17:00:30") → true for 9:00:00–17:00:30
func (TimeFuncs) InTimeRange(t any, start, end string) (bool, error) {
ts, err := toTime(t)
if err != nil {
return false, err
}
startSecs, err := parseTimeOfDay(start)
if err != nil {
return false, err
}
endSecs, err := parseTimeOfDay(end)
if err != nil {
return false, err
}
tSecs := ts.Hour()*3600 + ts.Minute()*60 + ts.Second()
return tSecs >= startSecs && tSecs <= endSecs, nil
}

// parseTimeOfDay parses a "HH:MM" or "HH:MM:SS" string and returns
// the total number of seconds since midnight.
func parseTimeOfDay(s string) (int, error) {
for _, layout := range []string{"15:04:05", "15:04"} {
if t, err := gotime.Parse(layout, s); err == nil {
return t.Hour()*3600 + t.Minute()*60 + t.Second(), nil
}
}
return 0, fmt.Errorf("cannot parse %q as a time of day (expected HH:MM or HH:MM:SS)", s)
}

// toTime converts a timestamp value to a time.Time.
// Supported input types: time.Time, or a string in RFC3339Nano / RFC3339 /
// "2006-01-02T15:04:05" / "2006-01-02 15:04:05" format.
func toTime(v any) (gotime.Time, error) {
switch t := v.(type) {
case gotime.Time:
return t, nil
case string:
layouts := []string{
gotime.RFC3339Nano,
gotime.RFC3339,
"2006-01-02T15:04:05",
"2006-01-02 15:04:05",
}
for _, layout := range layouts {
if ts, err := gotime.Parse(layout, t); err == nil {
return ts, nil
}
}
return gotime.Time{}, fmt.Errorf("cannot parse %q as a timestamp", t)
}
return gotime.Time{}, fmt.Errorf("cannot convert %T to a timestamp", v)
}

// InBusinessHour returns nil when no business hours are configured.
func (TimeFuncs) InBusinessHour(value string) (any, error) {
in, err := inBusinessHour(value)
Expand Down
33 changes: 33 additions & 0 deletions funcs/time_cel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package funcs

import (
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
)

// timeInTimeRangeGen exposes time.InTimeRange(t, start, end) → bool.
// Returns true when the time of day of t falls within [start, end] inclusive.
// start and end are "HH:MM" or "HH:MM:SS" strings.
// Accepts a timestamp (time.Time) or an RFC3339 string as the first argument.
//
// Examples:
//
// time.InTimeRange(t, "09:00", "17:00")
// time.InTimeRange(t, "09:30:00", "17:30:00")
var timeInTimeRangeGen = cel.Function("time.InTimeRange",
cel.Overload("time.InTimeRange_any_string_string",
[]*cel.Type{cel.AnyType, cel.StringType, cel.StringType},
cel.BoolType,
cel.FunctionBinding(func(args ...ref.Val) ref.Val {
var x TimeFuncs
start := args[1].Value().(string)
end := args[2].Value().(string)
result, err := x.InTimeRange(args[0].Value(), start, end)
if err != nil {
return types.WrapErr(err)
}
return types.Bool(result)
}),
),
)
48 changes: 48 additions & 0 deletions tests/cel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,54 @@ func TestCelDates(t *testing.T) {
runTests(t, tests)
}

func TestCelTimestampGetHoursMinutes(t *testing.T) {
ts, _ := time.Parse(time.RFC3339, "2024-06-15T14:30:45Z")

runTests(t, []Test{
// getHours and getMinutes on a time.Time variable
{map[string]any{"t": ts}, `t.getHours()`, "14"},
{map[string]any{"t": ts}, `t.getMinutes()`, "30"},

// combined — check it's within 9–17 and past the half hour
{map[string]any{"t": ts}, `t.getHours() >= 9 && t.getHours() <= 17`, "true"},
{map[string]any{"t": ts}, `t.getHours() == 14 && t.getMinutes() == 30`, "true"},

// with a timezone offset (CEL built-in tz support)
{map[string]any{"t": ts}, `t.getHours("America/New_York")`, "10"}, // UTC-4 in June
{map[string]any{"t": ts}, `t.getMinutes("America/New_York")`, "30"},
})
}

func TestCelTimeInTimeRange(t *testing.T) {
// various timestamps to test boundary conditions
ts14_30_45, _ := time.Parse(time.RFC3339, "2024-06-15T14:30:45Z") // 14:30:45 — inside 9–17
ts09_00_00, _ := time.Parse(time.RFC3339, "2024-06-15T09:00:00Z") // 09:00:00 — start boundary
ts17_00_00, _ := time.Parse(time.RFC3339, "2024-06-15T17:00:00Z") // 17:00:00 — end boundary
ts17_00_01, _ := time.Parse(time.RFC3339, "2024-06-15T17:00:01Z") // 17:00:01 — one second past end
ts08_59_59, _ := time.Parse(time.RFC3339, "2024-06-15T08:59:59Z") // 08:59:59 — one second before start
ts09_30_00, _ := time.Parse(time.RFC3339, "2024-06-15T09:30:00Z") // 09:30:00 — inside with HH:MM:SS range

runTests(t, []Test{
// time.Time input, HH:MM boundaries
{map[string]any{"t": ts14_30_45}, `time.InTimeRange(t, "09:00", "17:00")`, "true"},
{map[string]any{"t": ts09_00_00}, `time.InTimeRange(t, "09:00", "17:00")`, "true"}, // start boundary
{map[string]any{"t": ts17_00_00}, `time.InTimeRange(t, "09:00", "17:00")`, "true"}, // end boundary
{map[string]any{"t": ts17_00_01}, `time.InTimeRange(t, "09:00", "17:00")`, "false"}, // one second past end
{map[string]any{"t": ts08_59_59}, `time.InTimeRange(t, "09:00", "17:00")`, "false"}, // one second before start

// RFC3339 string input, HH:MM boundaries
{nil, `time.InTimeRange("2024-06-15T14:30:00Z", "09:00", "17:00")`, "true"},
{nil, `time.InTimeRange("2024-06-15T08:59:59Z", "09:00", "17:00")`, "false"},
{nil, `time.InTimeRange("2024-06-15T17:00:01Z", "09:00", "17:00")`, "false"},

// HH:MM:SS boundaries (seconds precision)
{map[string]any{"t": ts09_30_00}, `time.InTimeRange(t, "09:30:00", "17:30:00")`, "true"},
{nil, `time.InTimeRange("2024-06-15T09:29:59Z", "09:30:00", "17:30:00")`, "false"}, // one second before HH:MM:SS start
{nil, `time.InTimeRange("2024-06-15T17:30:00Z", "09:30:00", "17:30:00")`, "true"}, // exact HH:MM:SS end boundary
{nil, `time.InTimeRange("2024-06-15T17:30:01Z", "09:30:00", "17:30:00")`, "false"}, // one second past HH:MM:SS end
})
}

func TestCelVariadic(t *testing.T) {
testData := []struct {
Input string
Expand Down
Loading