Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add timepb support library #60

Merged
merged 8 commits into from
Feb 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions support/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Support Libraries

This directory provides support libraries for known types.
23 changes: 23 additions & 0 deletions support/timepb/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# timepb
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a README or docs.go? Not sure what is best practice. I think the advantage of docs.go is that it shows up in pkg.go.dev like this for sub-packages:
image

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

README shows up in pkg.go.dev as well when you go to the package page. I think we need both. Will add doc.go


`timepb` is a Go package that provides functions to do time operations with
[protobuf timestamp](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#timestamp)
and [protobuf duration](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#duration)
structures.

### Example

``` go
t1 := &tspb.Timestamp{Seconds: 10, Nanos: 1}
d := &durpb.Duration{Seconds: 1, Nanos: 1e9 - 1}
t2 := Add(t1, d)

fmt.Println(Compare(&tspb.Timestamp{Seconds: 12, Nanos: 0}, t2) == 0)
fmt.Println(Compare(&tspb.Timestamp{Seconds: 10, Nanos: 1}, t1) == 0)
fmt.Println(Compare(t1, t2))
// Output:
// true
// true
// -1
```

92 changes: 92 additions & 0 deletions support/timepb/cmp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package timepb

import (
"fmt"
"time"

durpb "google.golang.org/protobuf/types/known/durationpb"
tspb "google.golang.org/protobuf/types/known/timestamppb"
)

// IsZero returns true only when t is nil
func IsZero(t *tspb.Timestamp) bool {
return t == nil
}

// 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
}

// DurationIsNegative returns true if the duration is negative. It assumes that d is valid
// (d..CheckValid() is nil).
func DurationIsNegative(d *durpb.Duration) bool {
return d.Seconds < 0 || d.Seconds == 0 && d.Nanos < 0
robert-zaremba marked this conversation as resolved.
Show resolved Hide resolved
}

// 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 {
robert-zaremba marked this conversation as resolved.
Show resolved Hide resolved
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 (d..CheckValid() is nil).
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
}
22 changes: 22 additions & 0 deletions support/timepb/cmp_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package timepb

import (
"fmt"

durpb "google.golang.org/protobuf/types/known/durationpb"
tspb "google.golang.org/protobuf/types/known/timestamppb"
)

func ExampleAdd() {
t1 := &tspb.Timestamp{Seconds: 10, Nanos: 1}
d := &durpb.Duration{Seconds: 1, Nanos: 1e9 - 1}
t2 := Add(t1, d)

fmt.Println(Compare(&tspb.Timestamp{Seconds: 12, Nanos: 0}, t2) == 0)
fmt.Println(Compare(&tspb.Timestamp{Seconds: 10, Nanos: 1}, t1) == 0)
fmt.Println(Compare(t1, t2))
// Output:
// true
// true
// -1
}
134 changes: 134 additions & 0 deletions support/timepb/cmp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package timepb

import (
"math"
"testing"
"time"

"github.com/stretchr/testify/require"
durpb "google.golang.org/protobuf/types/known/durationpb"
tspb "google.golang.org/protobuf/types/known/timestamppb"
"pgregory.net/rapid"
)

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{}, false},
{new(0, 0), false},
{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) {
check := func(t require.TestingT, s, n int64, d time.Duration) {
t_in := time.Unix(s, n)
t_expected := tspb.New(t_in.Add(d))
tb := tspb.New(t_in)
tbPb := Add(tb, durpb.New(d))
tbStd := AddStd(tb, d)
require.Equal(t, *t_expected, *tbStd, "checking pb add")
require.Equal(t, *t_expected, *tbPb, "checking stdlib add")
}
gen := rapid.Int64Range(0, 1<<62)
genNano := rapid.Int64Range(0, 1e9-1)
rInt := func(t *rapid.T, label string) int64 { return gen.Draw(t, label).(int64) }

rapid.Check(t, func(t *rapid.T) {
s, n, d := rInt(t, "sec"), genNano.Draw(t, "nanos").(int64), time.Duration(rInt(t, "dur"))
check(t, s, n, d)
})

check(t, 0, 0, 0)
check(t, 1, 2, 0)
check(t, -1, -1, 1)

require.Nil(t, Add(nil, &durpb.Duration{Seconds: 1}), "Pb works with nil values")
require.Nil(t, 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")

}
5 changes: 5 additions & 0 deletions support/timepb/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/*
Package timepb provides functions to do time operations with protobuf timestamp
and duration structures.
*/
package timepb