diff --git a/.github/workflows/workflow.yaml b/.github/workflows/workflow.yaml index 72b6333..f15cbaf 100644 --- a/.github/workflows/workflow.yaml +++ b/.github/workflows/workflow.yaml @@ -19,19 +19,10 @@ jobs: - uses: actions/checkout@v3 - name: Install Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} - - uses: actions/cache@v3 - with: - path: | - ~/.cache/go-build - ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- - - name: Download deps run: go mod download diff --git a/README.md b/README.md index 96c0f04..c7ef8c2 100644 --- a/README.md +++ b/README.md @@ -68,11 +68,18 @@ Same as epoch.Milliseconds, but for strings, e.g.: {"timestamp":"1136239445999"} ``` -## Additional terms of use for users from Russia and Belarus +## FloatMS +Integer part of timestamp represents seconds and fractional - milliseconds since the Epoch(Unix time), +e.g.: +```json + {"timestamp":1136239445.999} +``` + +## Additional terms of use for users from russia and Belarus By using the code provided in these repositories you agree with the following: -* Russia has [illegally annexed Crimea in 2014](https://en.wikipedia.org/wiki/Annexation_of_Crimea_by_the_Russian_Federation) and [brought the war in Donbas](https://en.wikipedia.org/wiki/War_in_Donbas) followed by [full-scale invasion of Ukraine in 2022](https://en.wikipedia.org/wiki/2022_Russian_invasion_of_Ukraine). -* Russia has brought sorrow and devastations to millions of Ukrainians, killed hundreds of innocent people, damaged thousands of buildings, and forced several million people to flee. +* russia has [illegally annexed Crimea in 2014](https://en.wikipedia.org/wiki/Annexation_of_Crimea_by_the_Russian_Federation) and [brought the war in Donbas](https://en.wikipedia.org/wiki/War_in_Donbas) followed by [full-scale invasion of Ukraine in 2022](https://en.wikipedia.org/wiki/2022_Russian_invasion_of_Ukraine). +* russia has brought sorrow and devastations to millions of Ukrainians, killed hundreds of innocent people, damaged thousands of buildings, and forced several million people to flee. * [Putin khuylo!](https://en.wikipedia.org/wiki/Putin_khuylo!) Glory to Ukraine! πŸ‡ΊπŸ‡¦ diff --git a/epoch.go b/epoch.go index 327e519..3de65b4 100644 --- a/epoch.go +++ b/epoch.go @@ -1,7 +1,19 @@ package epoch -import "strconv" +import ( + "strconv" + "time" +) + +const ( + msPerS = int64(time.Second / time.Millisecond) + nsPerMs = int64(time.Millisecond) +) func parseInt64(s string) (int64, error) { return strconv.ParseInt(s, 10, 64) } + +func parseFloat64(s string) (float64, error) { + return strconv.ParseFloat(s, 64) +} diff --git a/epoch_test.go b/epoch_test.go new file mode 100644 index 0000000..31b81d5 --- /dev/null +++ b/epoch_test.go @@ -0,0 +1,7 @@ +package epoch + +import ( + "time" +) + +var tmsTime = time.Unix(1136239445, 999*nsPerMs) diff --git a/float_ms.go b/float_ms.go new file mode 100644 index 0000000..279f9c7 --- /dev/null +++ b/float_ms.go @@ -0,0 +1,43 @@ +package epoch + +import ( + "encoding/json" + "fmt" + "math" + "time" +) + +// FloatMS - integer part of timestamp represents seconds and fractional - milliseconds since +// the Epoch(Unix time), e.g. +// 1136239445.999 +// +// Inherits built-in time.Time type, thus has all it methods, but has custom serializer and +// deserializer(converts float timestamp into built-in time.Time and vice versa). +type FloatMS struct { + time.Time +} + +// NewFloatMS - returns FloatMS +func NewFloatMS(t time.Time) FloatMS { + return FloatMS{Time: t} +} + +// MarshalJSON - implements JSON marshaling interface +func (s FloatMS) MarshalJSON() ([]byte, error) { + milli := s.Time.UnixMilli() + f := float64(milli) / float64(msPerS) + + return json.Marshal(f) +} + +func (s *FloatMS) UnmarshalJSON(data []byte) error { + f, err := parseFloat64(string(data)) + if err != nil { + return fmt.Errorf("failed to parse epoch.FloatMS: %w", err) + } + + i, frac := math.Modf(f) + s.Time = time.Unix(int64(i), int64(frac*1_000)*nsPerMs) + + return nil +} diff --git a/float_ms_test.go b/float_ms_test.go new file mode 100644 index 0000000..7109351 --- /dev/null +++ b/float_ms_test.go @@ -0,0 +1,147 @@ +package epoch + +import ( + "encoding/json" + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type testFloatMSValueStruct struct { + Timestamp FloatMS `json:"timestamp"` +} + +type testFloatMSPointerStruct struct { + Timestamp *FloatMS `json:"timestamp"` +} + +func TestFloatMS_Marshal(t *testing.T) { + const js = `{"timestamp":1136239445.999}` + + t.Run("value", func(t *testing.T) { + tests := map[string]struct { + v testFloatMSValueStruct + want string + wantErr error + }{ + "positive": { + v: testFloatMSValueStruct{ + Timestamp: FloatMS{Time: tmsTime}, + }, + want: js, + }, + "rounding": { + v: testFloatMSValueStruct{ + Timestamp: FloatMS{Time: time.Unix(1136239445, 500999000)}, + }, + want: `{"timestamp":1136239445.5}`, + }, + } + + for name, tc := range tests { + tc := tc + t.Run(name, func(t *testing.T) { + got, err := json.Marshal(tc.v) + require.NoError(t, err) + assert.Equal(t, tc.want, string(got)) + }) + } + }) + + t.Run("pointer", func(t *testing.T) { + tests := map[string]struct { + v testFloatMSPointerStruct + want string + wantErr error + }{ + "positive": { + v: testFloatMSPointerStruct{ + Timestamp: &FloatMS{Time: tmsTime}, + }, + want: js, + }, + "nil": { + v: testFloatMSPointerStruct{ + Timestamp: nil, + }, + want: `{"timestamp":null}`, + }, + } + + for name, tc := range tests { + tc := tc + t.Run(name, func(t *testing.T) { + got, err := json.Marshal(tc.v) + require.NoError(t, err) + assert.Equal(t, tc.want, string(got)) + }) + } + }) +} + +func TestFloatMS_Unmarshal(t *testing.T) { + const js = `{"timestamp":1136239445.999}` + + t.Run("value", func(t *testing.T) { + tests := map[string]struct { + v string + want FloatMS + wantErr error + }{ + "positive": { + v: js, + want: FloatMS{Time: tmsTime}, + }, + "not_int": { + v: `{"timestamp":"text"}`, + wantErr: errors.New("failed to parse epoch.FloatMS: strconv.ParseFloat: parsing \"\\\"text\\\"\": invalid syntax"), + }, + } + + for name, tc := range tests { + tc := tc + t.Run(name, func(t *testing.T) { + var got testFloatMSValueStruct + err := json.Unmarshal([]byte(tc.v), &got) + if tc.wantErr == nil { + require.NoError(t, err) + assert.Equal(t, tc.want, got.Timestamp) + + return + } + + require.EqualError(t, err, tc.wantErr.Error()) + }) + } + }) + + t.Run("pointer", func(t *testing.T) { + tests := map[string]struct { + v string + want *FloatMS + wantErr error + }{ + "positive": { + v: js, + want: &FloatMS{Time: tmsTime}, + }, + "nil": { + v: `{"timestamp":null}`, + want: nil, + }, + } + + for name, tc := range tests { + tc := tc + t.Run(name, func(t *testing.T) { + var got testFloatMSPointerStruct + err := json.Unmarshal([]byte(tc.v), &got) + require.NoError(t, err) + assert.Equal(t, tc.want, got.Timestamp) + }) + } + }) +} diff --git a/go.mod b/go.mod index 3bd0d7c..1e4f68b 100644 --- a/go.mod +++ b/go.mod @@ -2,10 +2,10 @@ module github.com/vtopc/epoch go 1.17 -require github.com/stretchr/testify v1.6.1 +require github.com/stretchr/testify v1.8.4 require ( - github.com/davecgh/go-spew v1.1.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index afe7890..479781e 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,17 @@ -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/milliseconds.go b/milliseconds.go index 0efb79e..95c0a4d 100644 --- a/milliseconds.go +++ b/milliseconds.go @@ -35,11 +35,6 @@ func (m *Milliseconds) UnmarshalJSON(data []byte) error { return nil } -const ( - msPerS = int64(time.Second / time.Millisecond) - nsPerMs = int64(time.Millisecond) -) - func msToTime(ms int64) time.Time { s := ms / msPerS ns := (ms % msPerS) * nsPerMs diff --git a/milliseconds_test.go b/milliseconds_test.go index 4d3139e..2ca7c2f 100644 --- a/milliseconds_test.go +++ b/milliseconds_test.go @@ -21,8 +21,6 @@ type testMillisecondsPointerStruct struct { const tms = int64(1136239445999) -var tmsTime = time.Unix(1136239445, 999*nsPerMs) - func TestNewMilliseconds(t *testing.T) { const ns = 123 * nsPerMs t.Run("seconds", func(t *testing.T) {