diff --git a/pbtime/README.md b/pbtime/README.md new file mode 100644 index 000000000000..1c6e22a65fca --- /dev/null +++ b/pbtime/README.md @@ -0,0 +1,3 @@ +# pbtime + +pbtime is a Go package, part of the core Cosmos SDK module with helper methods for [protobuf timestamp](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#timestamp). diff --git a/pbtime/cmp.go b/pbtime/cmp.go new file mode 100644 index 000000000000..b8dfd658a43f --- /dev/null +++ b/pbtime/cmp.go @@ -0,0 +1,90 @@ +package pbtime + +import ( + "fmt" + "time" + + durpb "google.golang.org/protobuf/types/known/durationpb" + tspb "google.golang.org/protobuf/types/known/timestamppb" +) + +// IsZero returns true when t is nil or is zero unix timestamp (1970-01-01) +func IsZero(t *tspb.Timestamp) bool { + return t == nil || t.Nanos == 0 && t.Seconds == 0 +} + +// Commpare t1 and t2 and returns -1 when t1 < t2, 0 when t1 == t2 and 1 otherwise. +// Returns false if t1 or t2 is nil +func Compare(t1, t2 *tspb.Timestamp) int { + if t1 == nil || t2 == nil { + panic(fmt.Sprint("Can't compare nil time, t1=", t1, "t2=", t2)) + } + if t1.Seconds == t2.Seconds && t1.Nanos == t2.Nanos { + return 0 + } + if t1.Seconds < t2.Seconds || t1.Seconds == t2.Seconds && t1.Nanos < t2.Nanos { + return -1 + } + return 1 +} + +func DurationIsNegative(d durpb.Duration) bool { + return d.Seconds < 0 || d.Seconds == 0 && d.Nanos < 0 +} + +// AddStd returns a new timestamp with value t + d, where d is stdlib Duration +// If t is nil then nil is returned +// Panics on overflow +func AddStd(t *tspb.Timestamp, d time.Duration) *tspb.Timestamp { + if t == nil { + return nil + } + if d == 0 { + t2 := *t + return &t2 + } + t2 := tspb.New(t.AsTime().Add(d)) + overflowPanic(t, t2, d < 0) + return t2 +} + +func overflowPanic(t1, t2 *tspb.Timestamp, negative bool) { + cmp := Compare(t1, t2) + if negative { + if cmp < 0 { + panic("time overflow") + } + } else { + if cmp > 0 { + panic("time overflow") + } + } +} + +const second = int32(time.Second) + +// Add returns a new timestamp with value t + d, where d is protobuf Duration +// If t is nil then nil is returned. Panics on overflow. +// Note: d must be a valid PB Duration. +func Add(t *tspb.Timestamp, d durpb.Duration) *tspb.Timestamp { + if t == nil { + return nil + } + if d.Seconds == 0 && d.Nanos == 0 { + t2 := *t + return &t2 + } + t2 := tspb.Timestamp{ + Seconds: t.Seconds + d.Seconds, + Nanos: t.Nanos + d.Nanos, + } + if t2.Nanos >= second { + t2.Nanos -= second + t2.Seconds++ + } else if t2.Nanos <= -second { + t2.Nanos += second + t2.Seconds-- + } + overflowPanic(t, &t2, DurationIsNegative(d)) + return &t2 +} diff --git a/pbtime/cmp_test.go b/pbtime/cmp_test.go new file mode 100644 index 000000000000..9c4b8ea572af --- /dev/null +++ b/pbtime/cmp_test.go @@ -0,0 +1,132 @@ +package pbtime + +import ( + "math" + "math/rand" + "testing" + "time" + + "github.com/stretchr/testify/require" + durpb "google.golang.org/protobuf/types/known/durationpb" + tspb "google.golang.org/protobuf/types/known/timestamppb" +) + +func new(s int64, n int32) *tspb.Timestamp { + return &tspb.Timestamp{Seconds: s, Nanos: n} +} + +func TestIsZero(t *testing.T) { + tcs := []struct { + t *tspb.Timestamp + expected bool + }{ + {nil, true}, + {&tspb.Timestamp{}, true}, + {new(0, 0), true}, + + {new(1, 0), false}, + {new(0, 1), false}, + {tspb.New(time.Time{}), false}, + } + + for i, tc := range tcs { + require.Equal(t, tc.expected, IsZero(tc.t), "test_id %d", i) + } +} + +func TestCompare(t *testing.T) { + tcs := []struct { + t1 *tspb.Timestamp + t2 *tspb.Timestamp + expected int + }{ + {&tspb.Timestamp{}, &tspb.Timestamp{}, 0}, + {new(1, 1), new(1, 1), 0}, + {new(-1, 1), new(-1, 1), 0}, + {new(231, -5), new(231, -5), 0}, + + {new(1, -1), new(1, 0), -1}, + {new(1, -1), new(12, -1), -1}, + {new(-11, -1), new(-1, -1), -1}, + + {new(1, -1), new(0, -1), 1}, + {new(1, -1), new(1, -2), 1}, + } + for i, tc := range tcs { + r := Compare(tc.t1, tc.t2) + require.Equal(t, tc.expected, r, "test %d", i) + } + + // test panics + tcs2 := []struct { + t1 *tspb.Timestamp + t2 *tspb.Timestamp + }{ + {nil, new(1, 1)}, + {new(1, 1), nil}, + {nil, nil}, + } + for i, tc := range tcs2 { + require.Panics(t, func() { + Compare(tc.t1, tc.t2) + }, "test-panics %d", i) + } +} + +func TestAddFuzzy(t *testing.T) { + requier := require.New(t) + check := func(s, n int64, d time.Duration) { + t := time.Unix(s, n) + t_expected := tspb.New(t.Add(d)) + tb := tspb.New(t) + tbPb := Add(tb, *durpb.New(d)) + tbStd := AddStd(tb, d) + requier.Equal(*t_expected, *tbStd, "checking pb add") + requier.Equal(*t_expected, *tbPb, "checking stdlib add") + } + rInt := func() int64 { return rand.Int63() / 2 } + + for i := 0; i < 2000; i++ { + s, n, d := rInt(), rand.Int63n(1e9), time.Duration(rInt()) + check(s, n, d) + } + check(0, 0, 0) + check(1, 2, 0) + check(-1, -1, 1) + + requier.Nil(Add(nil, durpb.Duration{Seconds: 1}), "Pb works with nil values") + requier.Nil(AddStd(nil, time.Second), "Std works with nil values") +} + +func TestAddOverflow(t *testing.T) { + require := require.New(t) + tb := tspb.Timestamp{ + Seconds: math.MaxInt64, + Nanos: 1000, + } + require.Panics(func() { + AddStd(&tb, time.Second) + }, "AddStd should panic on overflow") + + require.Panics(func() { + Add(&tb, durpb.Duration{Nanos: second - 1}) + }, "Add should panic on overflow") + + // should panic on underflow + + tb = tspb.Timestamp{ + Seconds: -math.MaxInt64 - 1, + Nanos: -1000, + } + require.True(tb.Seconds < 0, "sanity check") + require.Panics(func() { + tt := AddStd(&tb, -time.Second) + t.Log(tt) + }, "AddStd should panic on underflow") + + require.Panics(func() { + tt := Add(&tb, durpb.Duration{Nanos: -second + 1}) + t.Log(tt) + }, "Add should panic on underflow") + +}