Skip to content
Closed
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 builtin/builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ var (
symbols.DurationFromHours: symbols.NewFunType(ast.DurationBound /* <= */, ast.Float64Bound),
symbols.DurationFromMinutes: symbols.NewFunType(ast.DurationBound /* <= */, ast.Float64Bound),
symbols.DurationFromSeconds: symbols.NewFunType(ast.DurationBound /* <= */, ast.Float64Bound),
symbols.DurationParse: symbols.NewFunType(ast.DurationBound /* <= */, ast.StringBound),
}

// ReducerFunctions has those built-in functions with are reducers.
Expand Down
16 changes: 16 additions & 0 deletions functional/functional.go
Original file line number Diff line number Diff line change
Expand Up @@ -839,6 +839,22 @@ func EvalApplyFn(applyFn ast.ApplyFn, subst ast.Subst) (ast.Constant, error) {
}
return ast.Duration(int64(s * float64(time.Second))), nil

case symbols.DurationParse.Symbol:
if l := len(evaluatedArgs); l != 1 {
return ast.Constant{}, fmt.Errorf("fn:duration:parse expected 1 argument, got %d", l)
}
str, err := evaluatedArgs[0].StringValue()
if err != nil {
return ast.Constant{}, fmt.Errorf("fn:duration:parse argument must be string: %w", err)
}
// Go's time.ParseDuration supports: h, m, s, ms, us (or µs), ns
// Examples: "1h30m", "500ms", "-2h45m30s", "1.5h"
d, err := time.ParseDuration(str)
if err != nil {
return ast.Constant{}, fmt.Errorf("fn:duration:parse failed: %w", err)
}
return ast.Duration(int64(d)), nil

default:
return EvalNumericApplyFn(applyFn, subst)
}
Expand Down
59 changes: 59 additions & 0 deletions functional/functional_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"math"
"testing"
"time"

"github.com/google/mangle/ast"
"github.com/google/mangle/parse"
Expand Down Expand Up @@ -1503,3 +1504,61 @@ func TestDurationFromUnits(t *testing.T) {
})
}
}

func TestDurationParse(t *testing.T) {
tests := []struct {
name string
input string
want ast.Constant
wantErr bool
}{
// Basic single-unit durations
{"hours", "2h", ast.Duration(2 * int64(time.Hour)), false},
{"minutes", "30m", ast.Duration(30 * int64(time.Minute)), false},
{"seconds", "45s", ast.Duration(45 * int64(time.Second)), false},
{"milliseconds", "500ms", ast.Duration(500 * int64(time.Millisecond)), false},
{"microseconds_us", "100us", ast.Duration(100 * int64(time.Microsecond)), false},
{"microseconds_µs", "100µs", ast.Duration(100 * int64(time.Microsecond)), false},
{"nanoseconds", "1000ns", ast.Duration(1000), false},

// Combined durations (Go-style)
{"hours_minutes", "1h30m", ast.Duration(int64(time.Hour) + 30*int64(time.Minute)), false},
{"hours_minutes_seconds", "2h45m30s", ast.Duration(2*int64(time.Hour) + 45*int64(time.Minute) + 30*int64(time.Second)), false},
{"minutes_seconds", "5m30s", ast.Duration(5*int64(time.Minute) + 30*int64(time.Second)), false},

// Decimal values
{"decimal_hours", "1.5h", ast.Duration(int64(1.5 * float64(time.Hour))), false},
{"decimal_seconds", "2.5s", ast.Duration(int64(2.5 * float64(time.Second))), false},

// Negative durations
{"negative_hours", "-2h", ast.Duration(-2 * int64(time.Hour)), false},
{"negative_combined", "-1h30m", ast.Duration(-int64(time.Hour) - 30*int64(time.Minute)), false},

// Zero
{"zero", "0s", ast.Duration(0), false},

// Error cases
{"invalid_unit", "5d", ast.Duration(0), true}, // 'd' (days) not supported by Go
{"invalid_format", "abc", ast.Duration(0), true},
{"empty_string", "", ast.Duration(0), true},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
expr := ast.ApplyFn{symbols.DurationParse, []ast.BaseTerm{ast.String(test.input)}}
got, err := EvalApplyFn(expr, ast.ConstSubstMap{})
if test.wantErr {
if err == nil {
t.Errorf("EvalApplyFn(%v) expected error, got %v", expr, got)
}
return
}
if err != nil {
t.Fatalf("EvalApplyFn(%v) failed with %v", expr, err)
}
if !got.Equals(test.want) {
t.Errorf("EvalApplyFn(%v) = %v, want %v", expr, got, test.want)
}
})
}
}
13 changes: 13 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,30 @@ bitbucket.org/creachadair/stringset v0.0.11 h1:6Sv4CCv14Wm+OipW4f3tWOb0SQVpBDLW0
bitbucket.org/creachadair/stringset v0.0.11/go.mod h1:wh0BHewFe+j0HrzWz7KcGbSNpFzWwnpmgPRlB57U5jU=
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/glog v1.2.4 h1:CNNw5U8lSiiBk7druxtSHHTsRWcxKoac6kZKm2peBBc=
github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/exp v0.0.0-20240707233637-46b078467d37 h1:uLDX+AfeFCct3a2C7uIWBKMJIR3CJMhcgfrUAqjRK6w=
golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4=
google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
4 changes: 4 additions & 0 deletions symbols/symbols.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,10 @@ var (
DurationFromMinutes = ast.FunctionSym{"fn:duration:from_minutes", 1}
// DurationFromSeconds creates a duration from seconds.
DurationFromSeconds = ast.FunctionSym{"fn:duration:from_seconds", 1}
// DurationParse parses a Go-style duration string like "1h30m", "500ms", "2h45m30s".
// Format: [+-]<value><unit>[<value><unit>...]
// Units: h (hours), m (minutes), s (seconds), ms (milliseconds), us/µs (microseconds), ns (nanoseconds)
DurationParse = ast.FunctionSym{"fn:duration:parse", 1}

// PairType is a constructor for a pair type.
PairType = ast.FunctionSym{"fn:Pair", 2}
Expand Down