From 0855ba859069969e6f7ec100dbf9f3ee50159688 Mon Sep 17 00:00:00 2001 From: Matthew Nibecker Date: Tue, 30 Jul 2024 13:07:22 -0700 Subject: [PATCH] Add strftime function --- docs/language/functions/strftime.md | 59 +++++++++++++++++++ go.mod | 2 + go.sum | 3 + runtime/sam/expr/function/function.go | 3 + runtime/sam/expr/function/time.go | 49 +++++++++++++++ .../sam/expr/function/ztests/strftime.yaml | 6 ++ 6 files changed, 122 insertions(+) create mode 100644 docs/language/functions/strftime.md create mode 100644 runtime/sam/expr/function/ztests/strftime.yaml diff --git a/docs/language/functions/strftime.md b/docs/language/functions/strftime.md new file mode 100644 index 0000000000..bbf18c3ff1 --- /dev/null +++ b/docs/language/functions/strftime.md @@ -0,0 +1,59 @@ +### 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. + +The directives can be used: + +| 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 | +| %p | "ante meridiem" (a.m.) or "post meridiem" (p.m.) | AM, PM | +| %S | Second as a zero-padded decimal number | 00, 01, ..., 59 | +| %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 | +| %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 | +| %% | A literal '%' character | % | + +### Examples + +The logarithm of a various numbers: +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..3787a0c9a3 100644 --- a/go.mod +++ b/go.mod @@ -56,11 +56,13 @@ require ( github.com/klauspost/asmfmt v1.3.2 // indirect github.com/klauspost/compress v1.16.7 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect + github.com/lestrrat-go/strftime v1.0.6 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-runewidth v0.0.10 // indirect 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..844014728b 100644 --- a/go.sum +++ b/go.sum @@ -221,6 +221,9 @@ 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/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..d46b4be3c1 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,49 @@ func (b *Bucket) Call(ectx expr.Context, args []zed.Value) zed.Value { } return zed.NewTime(nano.Ts(v).Trunc(bin)) } + +var strftimeSet strftime.SpecificationSet + +func init() { + strftimeSet = strftime.NewSpecificationSet() + strftimeSet.Delete('n') // newline + strftimeSet.Delete('R') // equivalent to %H:%M + strftimeSet.Delete('r') // equivalent to %I:%M:%S %p + strftimeSet.Delete('t') // tab + strftimeSet.Delete('T') // equivalent to %H:%M:%S + strftimeSet.Delete('v') // equivalent to %e-%b-%Y + strftimeSet.Delete('V') // the week number of the year (Monday as the first day of the week) as a decimal number (01-53) + strftimeSet.Delete('z') // timezone + strftimeSet.Delete('Z') // timezone +} + +// 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 := args[0] + 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() + v, ok := coerce.ToInt(args[1]) + if !ok { + return s.zctx.WrapError(ectx.Arena(), "strftime: time value required for time arg", args[1]) + } + if s.format != format { + var err error + if s.formatter, err = strftime.New(format, strftime.WithSpecificationSet(strftimeSet)); err != nil { + return s.zctx.WrapError(ectx.Arena(), fmt.Sprintf("strftime: %s", err), formatArg) + } + s.format = format + } + out := s.formatter.FormatString(nano.Ts(v).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..dfb0147b99 --- /dev/null +++ b/runtime/sam/expr/function/ztests/strftime.yaml @@ -0,0 +1,6 @@ +zed: 'strftime("%Y-%a", this)' + +input: '2024-07-30T06:15:01.062681Z' + +output: | + "2024-Tue"