From 593644c20f011bc6e1b3924770f2df6295ed1683 Mon Sep 17 00:00:00 2001 From: Ryan O'Hara-Reid Date: Mon, 19 May 2025 22:50:25 +1000 Subject: [PATCH] types/time: Update UnmarshalJSON method to handle all timestamp permutations (#1912) * types/time: handle decimal conversion to whole expected number * Add padding on all pathways * ch variable name * update comment * Update types/time_test.go Co-authored-by: Adrian Gallagher * linter: fix * Update types/time.go Co-authored-by: Gareth Kirwan * gk: nits * improve; old code is a duplication of strconv.ParseInt * Update types/time.go Co-authored-by: Gareth Kirwan * Update types/time.go Co-authored-by: Gareth Kirwan * rm extra back ticked back ticks --------- Co-authored-by: Ryan O'Hara-Reid Co-authored-by: Adrian Gallagher Co-authored-by: Gareth Kirwan --- types/time.go | 61 +++++++++++++++------------------------------- types/time_test.go | 12 ++++++--- 2 files changed, 28 insertions(+), 45 deletions(-) diff --git a/types/time.go b/types/time.go index 867a15bb..29a503fa 100644 --- a/types/time.go +++ b/types/time.go @@ -2,7 +2,6 @@ package types import ( "fmt" - "math" "strconv" "strings" "time" @@ -18,63 +17,43 @@ type Time time.Time func (t *Time) UnmarshalJSON(data []byte) error { s := string(data) - switch s { - case "null", "0", `""`, `"0"`: - *t = Time(time.Time{}) - return nil - } - if s[0] == '"' { s = s[1 : len(s)-1] } - badSyntax := false - target := strings.IndexFunc(s, func(r rune) bool { - if r == '.' { - return true - } - // types.Time may only parse numbers. The below check ensures an error is thrown. time.Time should be used to - // parse RFC3339 strings instead. - badSyntax = r < '0' || r > '9' - return badSyntax - }) + switch s { + case "null", "0", "": + return nil + } - if target != -1 { - if badSyntax { - return fmt.Errorf("%w for `%v`", strconv.ErrSyntax, string(data)) - } + if target := strings.Index(s, "."); target != -1 { s = s[:target] + s[target+1:] } - standard, err := strconv.ParseInt(s, 10, 64) + // Expects a string of length 10 (seconds), 13 (milliseconds), 16 (microseconds), or 19 (nanoseconds) representing a Unix timestamp + switch len(s) { + case 12, 15, 18: + s += "0" + case 11, 14, 17: + s += "00" + } + + unixTS, err := strconv.ParseInt(s, 10, 64) if err != nil { - return err + return fmt.Errorf("error parsing unix timestamp: %w", err) } switch len(s) { case 10: - // Seconds - *t = Time(time.Unix(standard, 0)) - case 11, 12: - // Milliseconds: 1726104395.5 && 1726104395.56 - *t = Time(time.UnixMilli(standard * int64(math.Pow10(13-len(s))))) + *t = Time(time.Unix(unixTS, 0)) case 13: - // Milliseconds - *t = Time(time.UnixMilli(standard)) - case 14: - // MicroSeconds: 1726106210903.0 - *t = Time(time.UnixMicro(standard * 100)) + *t = Time(time.UnixMilli(unixTS)) case 16: - // MicroSeconds - *t = Time(time.UnixMicro(standard)) - case 17: - // NanoSeconds: 1606292218213.4578 - *t = Time(time.Unix(0, standard*100)) + *t = Time(time.UnixMicro(unixTS)) case 19: - // NanoSeconds - *t = Time(time.Unix(0, standard)) + *t = Time(time.Unix(0, unixTS)) default: - return fmt.Errorf("cannot unmarshal %s into Time", string(data)) + return fmt.Errorf("cannot unmarshal %s into Time", data) } return nil } diff --git a/types/time_test.go b/types/time_test.go index 72a53c7e..b60e6f6c 100644 --- a/types/time_test.go +++ b/types/time_test.go @@ -47,6 +47,9 @@ func TestUnmarshalJSON(t *testing.T) { require.NoError(t, json.Unmarshal([]byte(`"1726106210903.0"`), &testTime)) assert.Equal(t, time.UnixMicro(1726106210903000), testTime.Time()) + require.NoError(t, json.Unmarshal([]byte(`"1747278712.09328"`), &testTime)) + assert.Equal(t, time.UnixMicro(1747278712093280), testTime.Time()) + // nanoseconds require.NoError(t, json.Unmarshal([]byte(`"1606292218213.4578"`), &testTime)) assert.Equal(t, time.Unix(0, 1606292218213457800), testTime.Time()) @@ -63,13 +66,14 @@ func TestUnmarshalJSON(t *testing.T) { require.ErrorIs(t, json.Unmarshal([]byte(`"1606292218213.45.8"`), &testTime), strconv.ErrSyntax) } -// 5030734 240.1 ns/op 168 B/op 2 allocs/op (current) -// 2716176 441.9 ns/op 352 B/op 6 allocs/op (previous) +// 6152384 195.5 ns/op 168 B/op 2 allocs/op (current) +// 5030734 240.1 ns/op 168 B/op 2 allocs/op (previous) func BenchmarkUnmarshalJSON(b *testing.B) { var testTime Time for b.Loop() { - err := json.Unmarshal([]byte(`"1691122380942.173000"`), &testTime) - require.NoError(b, err) + if err := json.Unmarshal([]byte(`"1691122380942.173000"`), &testTime); err != nil { + b.Fatal(err) + } } }