Skip to content

Commit

Permalink
Add support for UNIX time with nanoseconds to template functions (#300)
Browse files Browse the repository at this point in the history
  • Loading branch information
tksm committed May 2, 2024
1 parent ac36420 commit 0d580ff
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 4 deletions.
7 changes: 3 additions & 4 deletions cmd/cmd.go
Expand Up @@ -30,7 +30,6 @@ import (
"github.com/fatih/color"
"github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
"github.com/spf13/cast"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/stern/stern/stern"
Expand Down Expand Up @@ -549,13 +548,13 @@ func (o *options) generateTemplate() (*template.Template, error) {
return strings.TrimSuffix(string(b), "\n"), nil
},
"toRFC3339Nano": func(ts any) string {
return cast.ToTime(ts).Format(time.RFC3339Nano)
return toTime(ts).Format(time.RFC3339Nano)
},
"toUTC": func(ts any) time.Time {
return cast.ToTime(ts).UTC()
return toTime(ts).UTC()
},
"toTimestamp": func(ts any, layout string, optionalTZ ...string) (string, error) {
t, parseErr := cast.ToTimeE(ts)
t, parseErr := toTimeE(ts)
if parseErr != nil {
return "", parseErr
}
Expand Down
58 changes: 58 additions & 0 deletions cmd/helper.go
@@ -0,0 +1,58 @@
package cmd

import (
"encoding/json"
"strconv"
"strings"
"time"

"github.com/spf13/cast"
)

func toTime(a any) time.Time {
t, _ := toTimeE(a)
return t
}

func toTimeE(a any) (time.Time, error) {
switch v := a.(type) {
case string:
if t, ok := parseUnixTimeNanoString(v); ok {
return t, nil
}
case json.Number:
if t, ok := parseUnixTimeNanoString(v.String()); ok {
return t, nil
}
}
return cast.ToTimeE(a)
}

func parseUnixTimeNanoString(num string) (time.Time, bool) {
parts := strings.Split(num, ".")
if len(parts) > 2 {
return time.Time{}, false
}

sec, err := strconv.ParseInt(parts[0], 10, 64)
if err != nil {
return time.Time{}, false
}

var nsec int64
if len(parts) == 2 {
// convert fraction part to nanoseconds
const digits = 9
frac := parts[1]
if len(frac) > digits {
frac = frac[:digits]
} else if len(frac) < digits {
frac = frac + strings.Repeat("0", digits-len(frac))
}
nsec, err = strconv.ParseInt(frac, 10, 64)
if err != nil {
return time.Time{}, false
}
}
return time.Unix(sec, nsec), true
}
55 changes: 55 additions & 0 deletions cmd/helper_test.go
@@ -0,0 +1,55 @@
package cmd

import (
"encoding/json"
"fmt"
"testing"
"time"
)

func TestToTimeE(t *testing.T) {
base := time.Date(2006, 1, 2, 3, 4, 5, 0, time.UTC)
tests := []struct {
arg any
expected time.Time
wantError bool
}{
// nanoseconds
{"1136171045", base, false},
{"1136171045.0", base, false},
{"1136171045.1", base.Add(1e8 * time.Nanosecond), false},
{json.Number("1136171045.1"), base.Add(1e8 * time.Nanosecond), false},
{"1136171056.02", base.Add(11*time.Second + 2e7*time.Nanosecond), false},
{"1136171045.000000001", base.Add(1 * time.Nanosecond), false},
{"1136171045.123456789", base.Add(123456789 * time.Nanosecond), false},
{"1136171045.12345678912345", base.Add(123456789 * time.Nanosecond), false},
// cast.ToTimeE
{1136171045, base, false},
{"2006-01-02T03:04:05.123456789", base.Add(123456789 * time.Nanosecond), false},
// error
{"", time.Time{}, true},
{".", time.Time{}, true},
{"a.b", time.Time{}, true},
{"1.a", time.Time{}, true},
{"abc", time.Time{}, true},
}

for _, tt := range tests {
t.Run(fmt.Sprintf("%v", tt.arg), func(t *testing.T) {
tm, err := toTimeE(tt.arg)
if tt.wantError {
if err == nil {
t.Errorf("expected error, but got no error")
}
return
}
if err != nil {
t.Errorf("unexpected error: %+v", err)
return
}
if !tt.expected.Equal(tm) {
t.Errorf("expected %v, but actual %v", tt.expected, tm.UTC())
}
})
}
}

0 comments on commit 0d580ff

Please sign in to comment.