From 1276c31fe96a5681332907fa6dc505e6095342a1 Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Thu, 11 Jan 2018 20:26:27 +0000 Subject: [PATCH] add mtime package --- README.md | 5 ++ mtime/dur.go | 42 +++++++++++++++++ mtime/dur_test.go | 30 ++++++++++++ mtime/mtime.go | 5 ++ mtime/ts.go | 118 ++++++++++++++++++++++++++++++++++++++++++++++ mtime/ts_test.go | 118 ++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 318 insertions(+) create mode 100644 README.md create mode 100644 mtime/dur.go create mode 100644 mtime/dur_test.go create mode 100644 mtime/mtime.go create mode 100644 mtime/ts.go create mode 100644 mtime/ts_test.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..06c29d9 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# mediocre-go-lib + +This is a collection of packages which I use across many of my personal +projects. All packages intended to be used start with an `m`, packages not +starting with `m` are for internal use within this set of packages. diff --git a/mtime/dur.go b/mtime/dur.go new file mode 100644 index 0000000..edad9ac --- /dev/null +++ b/mtime/dur.go @@ -0,0 +1,42 @@ +package mtime + +import ( + "encoding/json" + "time" +) + +// Duration wraps time.Duration to implement marshaling and unmarshaling methods +type Duration struct { + time.Duration +} + +// MarshalText implements the text.Marshaler interface +func (d Duration) MarshalText() ([]byte, error) { + return []byte(d.Duration.String()), nil +} + +// UnmarshalText implements the text.Unmarshaler interface +func (d *Duration) UnmarshalText(b []byte) error { + var err error + d.Duration, err = time.ParseDuration(string(b)) + return err +} + +// MarshalJSON implements the json.Marshaler interface, marshaling the Duration +// as a json string via Duration's String method +func (d Duration) MarshalJSON() ([]byte, error) { + return json.Marshal(d.String()) +} + +// UnmarshalJSON implements the json.Unmarshaler interface, unmarshaling the +// Duration as a JSON string and using the time.ParseDuration function on that +func (d *Duration) UnmarshalJSON(b []byte) error { + var s string + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + d.Duration, err = time.ParseDuration(s) + return err +} diff --git a/mtime/dur_test.go b/mtime/dur_test.go new file mode 100644 index 0000000..6973842 --- /dev/null +++ b/mtime/dur_test.go @@ -0,0 +1,30 @@ +package mtime + +import ( + . "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestDuration(t *T) { + { + b, err := Duration{5 * time.Second}.MarshalText() + assert.NoError(t, err) + assert.Equal(t, []byte("5s"), b) + + var d Duration + assert.NoError(t, d.UnmarshalText(b)) + assert.Equal(t, 5*time.Second, d.Duration) + } + + { + b, err := Duration{5 * time.Second}.MarshalJSON() + assert.NoError(t, err) + assert.Equal(t, []byte(`"5s"`), b) + + var d Duration + assert.NoError(t, d.UnmarshalJSON(b)) + assert.Equal(t, 5*time.Second, d.Duration) + } +} diff --git a/mtime/mtime.go b/mtime/mtime.go new file mode 100644 index 0000000..1c705ac --- /dev/null +++ b/mtime/mtime.go @@ -0,0 +1,5 @@ +// Package mtime provides a wrapper around time.Time which marshals and unmarshals +// it as a floating point unix timestamp. + +// Package mtime extends the standard time package with extra functionality +package mtime diff --git a/mtime/ts.go b/mtime/ts.go new file mode 100644 index 0000000..f009a5e --- /dev/null +++ b/mtime/ts.go @@ -0,0 +1,118 @@ +package mtime + +// Code based off the timeutil package in github.com/levenlabs/golib +// Changes performed: +// - Renamed Timestamp to TS for brevity +// - Added NewTS function +// - Moved Float64 method +// - Moved initialization methods to top +// - Made MarshalJSON use String method +// - TSNow -> NowTS, make it use NewTS + +import ( + "bytes" + "strconv" + "time" +) + +var unixZero = time.Unix(0, 0) + +func timeToFloat(t time.Time) float64 { + // If time.Time is the empty value, UnixNano will return the farthest back + // timestamp a float can represent, which is some large negative value. We + // compromise and call it zero + if t.IsZero() { + return 0 + } + return float64(t.UnixNano()) / 1e9 +} + +// TS is a wrapper around time.Time which adds methods to marshal and +// unmarshal the value as a unix timestamp instead of a formatted string +type TS struct { + time.Time +} + +// NewTS returns a new TS instance wrapping the given time.Time, which will +// possibly be truncated a certain amount to account for floating point +// precision. +func NewTS(t time.Time) TS { + return TSFromFloat64(timeToFloat(t)) +} + +// NowTS is a wrapper around time.Now which returns a TS. +func NowTS() TS { + return NewTS(time.Now()) +} + +// TSFromInt64 returns a TS equal to the given int64, assuming it too is a unix +// timestamp +func TSFromInt64(ts int64) TS { + return TS{time.Unix(ts, 0)} +} + +// TSFromFloat64 returns a TS equal to the given float64, assuming it too is a +// unix timestamp. The float64 is interpreted as number of seconds, with +// everything after the decimal indicating milliseconds, microseconds, and +// nanoseconds +func TSFromFloat64(ts float64) TS { + secs := int64(ts) + nsecs := int64((ts - float64(secs)) * 1e9) + return TS{time.Unix(secs, nsecs)} +} + +// TSFromString attempts to parse the string as a float64, and then passes that +// into TSFromFloat64, returning the result +func TSFromString(ts string) (TS, error) { + f, err := strconv.ParseFloat(ts, 64) + if err != nil { + return TS{}, err + } + return TSFromFloat64(f), nil +} + +// String returns the string representation of the TS, in the form of a floating +// point form of the time as a unix timestamp +func (t TS) String() string { + ts := timeToFloat(t.Time) + return strconv.FormatFloat(ts, 'f', -1, 64) +} + +// Float64 returns the float representation of the timestamp in seconds. +func (t TS) Float64() float64 { + return timeToFloat(t.Time) +} + +var jsonNull = []byte("null") + +// MarshalJSON returns the JSON representation of the TS as an integer. It +// never returns an error +func (t TS) MarshalJSON() ([]byte, error) { + if t.IsZero() { + return jsonNull, nil + } + + return []byte(t.String()), nil +} + +// UnmarshalJSON takes a JSON integer and converts it into a TS, or +// returns an error if this can't be done +func (t *TS) UnmarshalJSON(b []byte) error { + // since 0 is a valid timestamp we can't use that to mean "unset", so we + // take null to mean unset instead + if bytes.Equal(b, jsonNull) { + t.Time = time.Time{} + return nil + } + + var err error + *t, err = TSFromString(string(b)) + return err +} + +// IsUnixZero returns true if the timestamp is equal to the unix zero timestamp, +// representing 1/1/1970. This is different than checking if the timestamp is +// the empty value (which should be done with IsZero) +func (t TS) IsUnixZero() bool { + return t.Equal(unixZero) +} diff --git a/mtime/ts_test.go b/mtime/ts_test.go new file mode 100644 index 0000000..39ea677 --- /dev/null +++ b/mtime/ts_test.go @@ -0,0 +1,118 @@ +package mtime + +import ( + "encoding/json" + "strconv" + . "testing" + "time" + + "gopkg.in/mgo.v2/bson" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTS(t *T) { + ts := NowTS() + + tsJ, err := json.Marshal(&ts) + require.Nil(t, err) + + // tsJ should basically be an integer + tsF, err := strconv.ParseFloat(string(tsJ), 64) + require.Nil(t, err) + assert.True(t, tsF > 0) + + ts2 := TSFromFloat64(tsF) + assert.Equal(t, ts, ts2) + + var ts3 TS + err = json.Unmarshal(tsJ, &ts3) + require.Nil(t, err) + assert.Equal(t, ts, ts3) +} + +// Make sure that we can take in a non-float from json +func TestTSMarshalInt(t *T) { + now := time.Now() + tsJ := []byte(strconv.FormatInt(now.Unix(), 10)) + var ts TS + err := json.Unmarshal(tsJ, &ts) + require.Nil(t, err) + assert.Equal(t, ts.Float64(), float64(now.Unix())) +} + +type Foo struct { + T TS `json:"timestamp" bson:"t"` +} + +func TestTSJSON(t *T) { + now := NowTS() + in := Foo{now} + b, err := json.Marshal(in) + require.Nil(t, err) + assert.NotEmpty(t, b) + + var out Foo + err = json.Unmarshal(b, &out) + require.Nil(t, err) + assert.Equal(t, in, out) +} + +func TestTSJSONNull(t *T) { + { + var foo Foo + timestampNull := []byte(`{"timestamp":null}`) + fooJSON, err := json.Marshal(foo) + require.Nil(t, err) + assert.Equal(t, timestampNull, fooJSON) + + require.Nil(t, json.Unmarshal(timestampNull, &foo)) + assert.True(t, foo.T.IsZero()) + assert.False(t, foo.T.IsUnixZero()) + } + + { + var foo Foo + foo.T = TS{Time: unixZero} + timestampZero := []byte(`{"timestamp":0}`) + fooJSON, err := json.Marshal(foo) + require.Nil(t, err) + assert.Equal(t, timestampZero, fooJSON) + + require.Nil(t, json.Unmarshal(timestampZero, &foo)) + assert.False(t, foo.T.IsZero()) + assert.True(t, foo.T.IsUnixZero()) + } +} + +func TestTSZero(t *T) { + var ts TS + assert.True(t, ts.IsZero()) + assert.False(t, ts.IsUnixZero()) + tsf := timeToFloat(ts.Time) + assert.Zero(t, tsf) + + ts = TSFromFloat64(0) + assert.False(t, ts.IsZero()) + assert.True(t, ts.IsUnixZero()) + tsf = timeToFloat(ts.Time) + assert.Zero(t, tsf) +} + +func TestTSBSON(t *T) { + // BSON only supports up to millisecond precision, but even if we keep that + // many it kinda gets messed up due to rounding errors. So we just give it + // one with second precision + now := TSFromInt64(time.Now().Unix()) + + in := Foo{now} + b, err := bson.Marshal(in) + require.Nil(t, err) + assert.NotEmpty(t, b) + + var out Foo + err = bson.Unmarshal(b, &out) + require.Nil(t, err) + assert.Equal(t, in, out) +}