Skip to content

Commit

Permalink
feat: add timepb support library (#60)
Browse files Browse the repository at this point in the history
* feat: add timepb support library

* use pointer for durpb, everywhere

* use rapid for fuzzy testing

* update IsZero

* add docs

* remove duplicated test

* Update support/timepb/doc.go

Co-authored-by: Aaron Craelius <aaron@regen.network>

Co-authored-by: Tyler <48813565+technicallyty@users.noreply.github.com>
Co-authored-by: Aaron Craelius <aaron@regen.network>
  • Loading branch information
3 people authored Feb 8, 2022
1 parent 3782f42 commit 213b768
Show file tree
Hide file tree
Showing 6 changed files with 279 additions and 0 deletions.
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

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

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

0 comments on commit 213b768

Please sign in to comment.