diff --git a/docs/language/functions/strftime.md b/docs/language/functions/strftime.md new file mode 100644 index 0000000000..446eef4d17 --- /dev/null +++ b/docs/language/functions/strftime.md @@ -0,0 +1,67 @@ +### Function + +  **strftime** — format time values + +### Synopsis +``` +strftime(format: string, t: time) -> string +``` + +### Description +The _strftime_ function returns a string represenation of time `t` +as specified by the provided string `format`. `format` is a string +containing format directives that dictate how the time string is +formatted. + +These directives are supported: + +| Directive | Explanation | Example | +|-----------|-------------|---------| +| %A | Weekday as full name | Sunday, Monday, ..., Saturday | +| %a | Weekday as abbreviated name | Sun, Mon, ..., Sat | +| %B | Month as full name | January, February, ..., December | +| %b | Month as abbreviated name | Jan, Feb, ..., Dec | +| %C | Century number (year / 100) as a 2-digit integer | 20 | +| %c | Locale's appropriate date and time representation | Tue Jul 30 14:30:15 2024 | +| %D | Equivalent to `%m/%d/%y` | 7/30/24 | +| %d | Day of the month as a zero-padded decimal number | 01, 02, ..., 31 | +| %e | Day of the month as a decimal number (1-31); single digits are preceded by a blank | 1, 2, ..., 31 | +| %F | Equivalent to `%Y-%m-%d` | 2024-07-30 | +| %H | Hour (24-hour clock) as a zero-padded decimal number | 00, 01, ..., 23 | +| %I | Hour (12-hour clock) as a zero-padded decimal number | 00, 01, ..., 12 | +| %j | Day of the year as a zero-padded decimal number | 001, 002, ..., 366 | +| %k | Hour (24-hour clock) as a decimal number; single digits are preceded by a blank | 0, 1, ..., 23 | +| %l | Hour (12-hour clock) as a decimal number; single digits are preceded by a blank | 0, 1, ..., 12 | +| %M | Minute as a zero-padded decimal number | 00, 01, ..., 59 | +| %m | Month as a zero-padded decimal number | 01, 02, ..., 12 | +| %n | Newline character | \n | +| %p | "ante meridiem" (a.m.) or "post meridiem" (p.m.) | AM, PM | +| %R | Equivalent to `%H:%M` | 18:49 | +| %r | Equivalent to `%I:%M:%S %p` | 06:50:58 PM | +| %S | Second as a zero-padded decimal number | 00, 01, ..., 59 | +| %T | Equivalent to `%H:%M:%S` | 18:50:58 | +| %t | Tab character | \t | +| %U | Week number of the year (Sunday as the first day of the week) | 00, 01, ..., 53 | +| %u | Weekday as a decimal number, range 1 to 7, with Monday being 1 | 1, 2, ..., 7 | +| %V | Week number of the year (Monday as the first day of the week) as a decimal number (01-53) | 01, 02, ..., 53 | +| %v | Equivalent to `%e-%b-%Y` | 31-Jul-2024 | +| %W | Week number of the year (Monday as the first day of the week) | 00, 01, ..., 53 | +| %w | Weekday as a decimal number, range 0 to 6, with Sunday being 0 | 0, 1, ..., 6 | +| %X | Locale's appropriate time representation | 14:30:15 | +| %x | Locale's appropriate date representation | 07/30/24 | +| %Y | Year with century as a decimal number | 2024 | +| %y | Year without century as a decimal number | 24, 23 | +| %Z | Timezone name | UTC | +| %z | +hhmm or -hhmm numeric timezone (that is, the hour and minute offset from UTC) | +0000 | +| %% | A literal '%' character | % | + +### Examples + +Print the year number as a string +```mdtest-command +echo 2024-07-30T20:05:15.118252Z | zq -z 'strftime("%Y", this)' - +``` +=> +```mdtest-output +"2024" +``` diff --git a/go.mod b/go.mod index 2098930d8f..fb6e3fcf29 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/gosuri/uilive v0.0.4 github.com/hashicorp/golang-lru/v2 v2.0.1 github.com/kr/text v0.2.0 + github.com/lestrrat-go/strftime v1.0.6 github.com/paulbellamy/ratecounter v0.2.0 github.com/pbnjay/memory v0.0.0-20190104145345-974d429e7ae4 github.com/peterh/liner v1.1.0 @@ -61,6 +62,7 @@ require ( github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 // indirect github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect github.com/rivo/uniseg v0.1.0 // indirect diff --git a/go.sum b/go.sum index a77f515470..a7b4ecb9b8 100644 --- a/go.sum +++ b/go.sum @@ -221,6 +221,10 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= +github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8= +github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is= +github.com/lestrrat-go/strftime v1.0.6 h1:CFGsDEt1pOpFNU+TJB0nhz9jl+K0hZSLE205AhTIGQQ= +github.com/lestrrat-go/strftime v1.0.6/go.mod h1:f7jQKgV5nnJpYgdEasS+/y7EsTb8ykN2z68n3TtcTaw= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= diff --git a/runtime/sam/expr/function/function.go b/runtime/sam/expr/function/function.go index b7912cb1b2..884890557e 100644 --- a/runtime/sam/expr/function/function.go +++ b/runtime/sam/expr/function/function.go @@ -161,6 +161,9 @@ func New(zctx *zed.Context, name string, narg int) (expr.Function, field.Path, e case "regexp_replace": argmin, argmax = 3, 3 f = &RegexpReplace{zctx: zctx} + case "strftime": + argmin, argmax = 2, 2 + f = &Strftime{zctx: zctx} case "under": f = &Under{zctx: zctx} case "unflatten": diff --git a/runtime/sam/expr/function/time.go b/runtime/sam/expr/function/time.go index f752955322..c2736baf92 100644 --- a/runtime/sam/expr/function/time.go +++ b/runtime/sam/expr/function/time.go @@ -1,10 +1,13 @@ package function import ( + "fmt" + "github.com/brimdata/zed" "github.com/brimdata/zed/pkg/nano" "github.com/brimdata/zed/runtime/sam/expr" "github.com/brimdata/zed/runtime/sam/expr/coerce" + "github.com/lestrrat-go/strftime" ) // https://github.com/brimdata/zed/blob/main/docs/language/functions.md#now @@ -46,3 +49,34 @@ func (b *Bucket) Call(ectx expr.Context, args []zed.Value) zed.Value { } return zed.NewTime(nano.Ts(v).Trunc(bin)) } + +// https://github.com/brimdata/zed/blob/main/docs/language/functions.md#strftime +type Strftime struct { + zctx *zed.Context + format string + formatter *strftime.Strftime +} + +func (s *Strftime) Call(ectx expr.Context, args []zed.Value) zed.Value { + formatArg, timeArg := args[0], args[1] + if !formatArg.IsString() { + return s.zctx.WrapError(ectx.Arena(), "strftime: string value required for format arg", formatArg) + } + if formatArg.IsNull() { + return zed.NullString + } + format := formatArg.AsString() + if zed.TypeUnder(timeArg.Type()) != zed.TypeTime { + return s.zctx.WrapError(ectx.Arena(), "strftime: time value required for time arg", args[1]) + } + t := timeArg.AsTime() + if s.format != format { + var err error + if s.formatter, err = strftime.New(format); err != nil { + return s.zctx.WrapError(ectx.Arena(), fmt.Sprintf("strftime: %s", err), formatArg) + } + s.format = format + } + out := s.formatter.FormatString(t.Time()) + return ectx.Arena().NewString(out) +} diff --git a/runtime/sam/expr/function/ztests/strftime.yaml b/runtime/sam/expr/function/ztests/strftime.yaml new file mode 100644 index 0000000000..7a6937821e --- /dev/null +++ b/runtime/sam/expr/function/ztests/strftime.yaml @@ -0,0 +1,11 @@ +zed: 'strftime(f, v)' + +input: | + {f:"%Y-%a",v:2024-07-30T06:15:01.062681Z} + {f:1,v:2024-07-30T06:15:01.062681Z} + {f:"%H",v:"foo"} + +output: | + "2024-Tue" + error({message:"strftime: string value required for format arg",on:1}) + error({message:"strftime: time value required for time arg",on:"foo"})