feat(tyme/ts): add ts package

This commit is contained in:
2022-10-30 03:34:39 +00:00
parent 5d31aa6743
commit de61785051
20 changed files with 2326 additions and 0 deletions

View File

@@ -3,4 +3,5 @@ go 1.19
use ( use (
. .
./dur ./dur
./ts
) )

2
ts/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
bin/*
coverage.*

94
ts/.golangci.yml Normal file
View File

@@ -0,0 +1,94 @@
linters-settings:
funlen:
lines: 100
statements: 450
golint:
min-confidence: 0
govet:
enable-all: true
disable:
- fieldalignment
- shadow
lll:
line-length: 80
tab-width: 4
maligned:
suggest-new: true
misspell:
locale: US
paralleltest:
ignore-missing: true
linters:
disable-all: true
enable:
- asciicheck
- bodyclose
- depguard
- durationcheck
- errcheck
- errorlint
- exhaustive
- exportloopref
- funlen
- gochecknoinits
- goconst
- gocritic
- godot
- gofumpt
- goimports
- goprintffuncname
- gosec
- gosimple
- govet
- importas
- ineffassign
- lll
- misspell
- nakedret
- nilerr
- noctx
- nolintlint
- paralleltest
- prealloc
- predeclared
- revive
- rowserrcheck
- sqlclosecheck
- staticcheck
- typecheck
- unconvert
- unparam
- unused
- wastedassign
- whitespace
issues:
exclude:
- Using the variable on range scope `tt` in function literal
- Using the variable on range scope `tc` in function literal
exclude-rules:
- path: "_test\\.go"
linters:
- funlen
- dupl
- source: "^//go:generate "
linters:
- lll
- source: "`env:"
linters:
- lll
- source: "`json:"
linters:
- lll
- source: "`xml:"
linters:
- lll
- source: "`yaml:"
linters:
- lll
run:
timeout: 2m
allow-parallel-runners: true
modules-download-mode: readonly

191
ts/Makefile Normal file
View File

@@ -0,0 +1,191 @@
GOMODNAME := $(shell grep 'module' go.mod | sed -e 's/^module //')
SOURCES := $(shell find . -name "*.go" -or -name "go.mod" -or -name "go.sum" \
-or -name "Makefile" -or -name "*.golden")
# Verbose output
ifdef VERBOSE
V = -v
endif
#
# Environment
#
BINDIR := bin
TOOLDIR := $(BINDIR)/tools
# Global environment variables for all targets
SHELL ?= /bin/bash
SHELL := env \
GO111MODULE=on \
GOBIN=$(CURDIR)/$(TOOLDIR) \
CGO_ENABLED=1 \
PATH='$(CURDIR)/$(BINDIR):$(CURDIR)/$(TOOLDIR):$(PATH)' \
$(SHELL)
#
# Defaults
#
# Default target
.DEFAULT_GOAL := test
#
# Tools
#
# external tool
define tool # 1: binary-name, 2: go-import-path
TOOLS += $(TOOLDIR)/$(1)
$(TOOLDIR)/$(1): Makefile
GOBIN="$(CURDIR)/$(TOOLDIR)" go install "$(2)"
endef
$(eval $(call tool,godoc,golang.org/x/tools/cmd/godoc@latest))
$(eval $(call tool,gofumpt,mvdan.cc/gofumpt@latest))
$(eval $(call tool,goimports,golang.org/x/tools/cmd/goimports@latest))
$(eval $(call tool,golangci-lint,github.com/golangci/golangci-lint/cmd/golangci-lint@v1.50))
$(eval $(call tool,gomod,github.com/Helcaraxan/gomod@latest))
$(eval $(call tool,mockgen,github.com/golang/mock/mockgen@v1.6.0))
.PHONY: tools
tools: $(TOOLS)
#
# Development
#
BENCH ?= .
TESTARGS ?=
.PHONY: clean
clean:
rm -f $(TOOLS)
rm -f ./coverage.out ./go.mod.tidy-check ./go.sum.tidy-check
.PHONY: test
test:
go test $(V) -count=1 -race $(TESTARGS) ./...
.PHONY: test-deps
test-deps:
go test all
.PHONY: lint
lint: $(TOOLDIR)/golangci-lint
golangci-lint $(V) run
.PHONY: format
format: $(TOOLDIR)/goimports $(TOOLDIR)/gofumpt
goimports -w . && gofumpt -w .
.SILENT: bench
.PHONY: bench
bench:
go test $(V) -count=1 -bench=$(BENCH) $(TESTARGS) ./...
#
# Code Generation
#
.PHONY: generate
generate: $(TOOLDIR)/mockgen
go generate ./...
.PHONY: check-generate
check-generate: $(TOOLDIR)/mockgen
$(eval CHKDIR := $(shell mktemp -d))
cp -av . "$(CHKDIR)"
make -C "$(CHKDIR)/" generate
( diff -rN . "$(CHKDIR)" && rm -rf "$(CHKDIR)" ) || \
( rm -rf "$(CHKDIR)" && exit 1 )
#
# Coverage
#
.PHONY: cov
cov: coverage.out
.PHONY: cov-html
cov-html: coverage.out
go tool cover -html=./coverage.out
.PHONY: cov-func
cov-func: coverage.out
go tool cover -func=./coverage.out
coverage.out: $(SOURCES)
go test $(V) -count=1 -race \
-covermode=atomic -coverprofile=./coverage.out ./...
#
# Dependencies
#
.PHONY: deps
deps:
go mod download
.PHONY: deps-update
deps-update:
go get -u -t ./...
.PHONY: deps-analyze
deps-analyze: $(TOOLDIR)/gomod
gomod analyze
.PHONY: tidy
tidy:
go mod tidy $(V)
.PHONY: verify
verify:
go mod verify
.SILENT: check-tidy
.PHONY: check-tidy
check-tidy:
cp go.mod go.mod.tidy-check
cp go.sum go.sum.tidy-check
go mod tidy
( \
diff go.mod go.mod.tidy-check && \
diff go.sum go.sum.tidy-check && \
rm -f go.mod go.sum && \
mv go.mod.tidy-check go.mod && \
mv go.sum.tidy-check go.sum \
) || ( \
rm -f go.mod go.sum && \
mv go.mod.tidy-check go.mod && \
mv go.sum.tidy-check go.sum; \
exit 1 \
)
#
# Documentation
#
# Serve docs
.PHONY: docs
docs: $(TOOLDIR)/godoc
$(info serviing docs on http://127.0.0.1:6060/pkg/$(GOMODNAME)/)
@godoc -http=127.0.0.1:6060
#
# Release
#
.PHONY: new-version
new-version: check-npx
npx standard-version
.PHONY: next-version
next-version: check-npx
npx standard-version --dry-run
.PHONY: check-npx
check-npx:
$(if $(shell which npx),,\
$(error No npx found in PATH, please install NodeJS))

14
ts/go.mod Normal file
View File

@@ -0,0 +1,14 @@
module github.com/jimeh/go-tyme/ts
go 1.18
require (
github.com/jimeh/go-tyme/dur v0.0.0-20221030033507-5d31aa674303
github.com/stretchr/testify v1.8.1
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
)

19
ts/go.sum Normal file
View File

@@ -0,0 +1,19 @@
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/jimeh/go-tyme/dur v0.0.0-20221030033507-5d31aa674303 h1:nTg0rfEObislvl5SOmEyjE2iV/BTNCG2ts36cjXMNfk=
github.com/jimeh/go-tyme/dur v0.0.0-20221030033507-5d31aa674303/go.mod h1:9zwXRzQlr7JTL5wUVkdCnZY0NR08ZtFMaehZNpZNV+w=
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/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.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
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/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=

248
ts/marshal_test.go Normal file
View File

@@ -0,0 +1,248 @@
package ts
import "time"
var (
marshalUnmarshalTestCases = []struct {
name string
t time.Time
second string
millisecond string
microsecond string
nanosecond string
}{
{
name: "UTC-8",
t: time.Date(
2022, 10, 29, 6, 40, 34, 934349003,
time.FixedZone("UTC-8", -8*60*60),
),
second: "1667054434",
millisecond: "1667054434934",
microsecond: "1667054434934349",
nanosecond: "1667054434934349003",
},
{
name: "UTC-2",
t: time.Date(
2022, 10, 29, 12, 40, 34, 934349003,
time.FixedZone("UTC-2", -2*60*60),
),
second: "1667054434",
millisecond: "1667054434934",
microsecond: "1667054434934349",
nanosecond: "1667054434934349003",
},
{
name: "UTC",
t: time.Date(
2022, 10, 29, 14, 40, 34, 934349003, time.UTC,
),
second: "1667054434",
millisecond: "1667054434934",
microsecond: "1667054434934349",
nanosecond: "1667054434934349003",
},
{
name: "UTC+2",
t: time.Date(
2022, 10, 29, 16, 40, 34, 934349003,
time.FixedZone("UTC+2", 2*60*60),
),
second: "1667054434",
millisecond: "1667054434934",
microsecond: "1667054434934349",
nanosecond: "1667054434934349003",
},
{
name: "UTC+8",
t: time.Date(
2022, 10, 29, 22, 40, 34, 934349003,
time.FixedZone("UTC+8", 8*60*60),
),
second: "1667054434",
millisecond: "1667054434934",
microsecond: "1667054434934349",
nanosecond: "1667054434934349003",
},
{
name: "epoch",
t: time.Date(
1970, 1, 1, 0, 0, 0, 0, time.UTC,
),
second: "0",
millisecond: "0",
microsecond: "0",
nanosecond: "0",
},
{
name: "min second",
t: time.Date(292277026596, 12, 4, 15, 30, 8, 0, time.UTC),
second: "-9223372036854775808",
millisecond: "0",
microsecond: "0",
nanosecond: "0",
},
{
name: "max second",
t: time.Date(292277026596, 12, 4, 15, 30, 7, 0, time.UTC),
second: "9223372036854775807",
millisecond: "-1000",
microsecond: "-1000000",
nanosecond: "-1000000000",
},
{
name: "min millisecond",
t: time.Date(
-292275055, 5, 16, 16, 47, 4, 192000000, time.UTC,
),
second: "-9223372036854776",
millisecond: "-9223372036854775808",
microsecond: "0",
nanosecond: "0",
},
{
name: "max millisecond",
t: time.Date(
292278994, 8, 17, 7, 12, 55, 807000000, time.UTC,
),
second: "9223372036854775",
millisecond: "9223372036854775807",
microsecond: "-1000",
nanosecond: "-1000000",
},
{
name: "min microseconds",
t: time.Date(
-290308, 12, 21, 19, 59, 5, 224192000, time.UTC,
),
second: "-9223372036855",
millisecond: "-9223372036854776",
microsecond: "-9223372036854775808",
nanosecond: "0",
},
{
name: "max microseconds",
t: time.Date(
294247, 1, 10, 4, 0, 54, 775807000, time.UTC,
),
second: "9223372036854",
millisecond: "9223372036854775",
microsecond: "9223372036854775807",
nanosecond: "-1000",
},
{
name: "min nanoseconds",
t: time.Date(1677, 9, 21, 0, 12, 43, 145224192, time.UTC),
second: "-9223372037",
millisecond: "-9223372036855",
microsecond: "-9223372036854776",
nanosecond: "-9223372036854775808",
},
{
name: "max nanoseconds",
t: time.Date(
2262, 4, 11, 23, 47, 16, 854775807, time.UTC,
),
second: "9223372036",
millisecond: "9223372036854",
microsecond: "9223372036854775",
nanosecond: "9223372036854775807",
},
{
name: "year 1092",
t: time.Date(1092, 3, 23, 3, 52, 8, 734829384, time.UTC),
second: "-27699912472",
millisecond: "-27699912471266",
microsecond: "-27699912471265171",
nanosecond: "9193575676153932616",
},
{
name: "year -1000",
t: time.Date(
-1000, 10, 29, 22, 40, 34, 934349003,
time.FixedZone("UTC+8", 8*60*60),
),
second: "-93698068766",
millisecond: "-93698068765066",
microsecond: "-93698068765065651",
nanosecond: "-1464348396517892917",
},
{
name: "year 10449",
t: time.Date(
10449, 10, 29, 22, 40, 34, 934349003,
time.FixedZone("UTC+8", 8*60*60),
),
second: "267597528034",
millisecond: "267597528034934",
microsecond: "267597528034934349",
nanosecond: "-9103633070708925237",
},
{
name: "year 1044938",
t: time.Date(
1044938, 10, 29, 22, 40, 34, 934349003,
time.FixedZone("UTC+8", 8*60*60),
),
second: "32912917195234",
millisecond: "32912917195234934",
microsecond: "-3980570952184168883",
nanosecond: "3925767737094266059",
},
}
unmarshalTestCases = []struct {
name string
second string
millisecond string
microsecond string
nanosecond string
t time.Time
wantErr string
}{
{
name: "string",
second: `"2019-01-01T00:00:00Z"`,
millisecond: `"2019-01-01T00:00:00Z"`,
microsecond: `"2019-01-01T00:00:00Z"`,
nanosecond: `"2019-01-01T00:00:00Z"`,
wantErr: "invalid numeric timestamp",
},
{
name: "array",
second: `[1, "true", false]`,
millisecond: `[1, "true", false]`,
microsecond: `[1, "true", false]`,
nanosecond: `[1, "true", false]`,
wantErr: "invalid numeric timestamp",
},
{
name: "object",
second: `{"object": "Object"}`,
millisecond: `{"object": "Object"}`,
microsecond: `{"object": "Object"}`,
nanosecond: `{"object": "Object"}`,
wantErr: "invalid numeric timestamp",
},
{
name: "whitespace",
second: " 1667054434 ",
millisecond: " 1667054434934 ",
microsecond: " 1667054434934349 ",
nanosecond: " 1667054434934349003 ",
t: time.Date(
2022, 10, 29, 14, 40, 34, 934349003, time.UTC,
),
},
{
name: "float",
second: "1667054434.123456789",
millisecond: "1667054434934.123456",
microsecond: "1667054434934349.123",
nanosecond: "",
t: time.Date(
2022, 10, 29, 14, 40, 34, 934349003, time.UTC,
),
},
}
)

88
ts/microsecond.go Normal file
View File

@@ -0,0 +1,88 @@
package ts
import (
"strconv"
"time"
"gopkg.in/yaml.v3"
)
// Microsecond is a wrapper around time.Time for marshaling to/from JSON/YAML as
// microsecond-based numeric Unix timestamps.
//
// It marshals to a JSON/YAML number representing the number of microseconds
// since the Unix time epoch.
//
// It unmarshals from a JSON/YAML number representing the number of microseconds
// since the Unix time epoch.
type Microsecond time.Time
// Time returns the time.Time corresponding to the microsecond instant s.
func (ms Microsecond) Time() time.Time {
return time.Time(ms)
}
// Local returns the local time corresponding to the microsecond instant s.
func (ms Microsecond) Local() Microsecond {
return Microsecond(time.Time(ms).Local())
}
// GoString implements the fmt.GoStringer interface.
func (ms Microsecond) GoString() string {
return time.Time(ms).GoString()
}
// IsDST reports whether the microsecond instant s occurs within Daylight Saving
// Time.
func (ms Microsecond) IsDST() bool {
return time.Time(ms).IsDST()
}
// IsZero returns true if the Microsecond is the zero value.
func (ms Microsecond) IsZero() bool {
return time.Time(ms).IsZero()
}
// String calls time.Time.String.
func (ms Microsecond) String() string {
return time.Time(ms).String()
}
// UTC returns a copy of the Microsecond with the location set to UTC.
func (ms Microsecond) UTC() Microsecond {
return Microsecond(time.Time(ms).UTC())
}
// MarshalJSON implements the json.Marshaler interface.
func (ms Microsecond) MarshalJSON() ([]byte, error) {
return []byte(strconv.FormatInt(time.Time(ms).UnixMicro(), 10)), nil
}
// UnmarshalJSON implements the json.Unmarshaler interface.
func (ms *Microsecond) UnmarshalJSON(data []byte) error {
i, err := unmarshalBytes(data)
if err != nil {
return err
}
*ms = UnixMicro(i)
return nil
}
// MarshalJSON implements the yaml.Marshaler interface.
func (ms Microsecond) MarshalYAML() (interface{}, error) {
return time.Time(ms).UnixMicro(), nil
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (ms *Microsecond) UnmarshalYAML(node *yaml.Node) error {
i, err := unmarshalYAMLNode(node)
if err != nil {
return err
}
*ms = UnixMicro(i)
return nil
}

168
ts/microsecond_test.go Normal file
View File

@@ -0,0 +1,168 @@
package ts
import (
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
var (
minMicro = time.UnixMicro(-9223372036854775808).UTC()
maxMicro = time.UnixMicro(9223372036854775807).UTC()
)
func microsecondSkipTestCase(t *testing.T, ti time.Time) bool {
t.Helper()
return ti.Before(minMicro) || ti.After(maxMicro)
}
func TestMicrosecond_MarshalUnmarshalJSON(t *testing.T) {
for _, tt := range marshalUnmarshalTestCases {
t.Run(tt.name, func(t *testing.T) {
v := Microsecond(tt.t)
b, err := json.Marshal(v)
require.NoError(t, err)
assert.Equal(t, tt.microsecond, string(b))
if microsecondSkipTestCase(t, tt.t) {
return
}
var got Microsecond
err = json.Unmarshal(b, &got)
require.NoError(t, err)
want := tt.t.Truncate(time.Microsecond)
assert.Equal(t, want.UTC(), time.Time(got).UTC())
assert.Equal(t, time.Local, time.Time(got).Location())
})
}
}
func TestMicrosecond_MarshalUnmarshalYAML(t *testing.T) {
for _, tt := range marshalUnmarshalTestCases {
t.Run(tt.name, func(t *testing.T) {
v := Microsecond(tt.t)
b, err := yaml.Marshal(v)
require.NoError(t, err)
assert.Equal(t, tt.microsecond+"\n", string(b))
if microsecondSkipTestCase(t, tt.t) {
return
}
var got Microsecond
err = yaml.Unmarshal(b, &got)
require.NoError(t, err)
want := tt.t.Truncate(time.Microsecond)
assert.Equal(t, want.UTC(), time.Time(got).UTC())
assert.Equal(t, time.Local, time.Time(got).Location())
})
}
}
func TestMicrosecond_MarshalJSON(t *testing.T) {
for _, tt := range marshalUnmarshalTestCases {
t.Run(tt.name, func(t *testing.T) {
ts := Microsecond(tt.t)
b, err := json.Marshal(ts)
require.NoError(t, err)
assert.Equal(t, tt.microsecond, string(b))
})
}
}
func TestMicrosecond_MarshalYAML(t *testing.T) {
for _, tt := range marshalUnmarshalTestCases {
t.Run(tt.name, func(t *testing.T) {
ts := Microsecond(tt.t)
b, err := yaml.Marshal(ts)
require.NoError(t, err)
assert.Equal(t, tt.microsecond+"\n", string(b))
})
}
}
func TestMicrosecond_UnmarshalJSON(t *testing.T) {
for _, tt := range marshalUnmarshalTestCases {
if microsecondSkipTestCase(t, tt.t) {
continue
}
t.Run(tt.name, func(t *testing.T) {
var ts Microsecond
err := json.Unmarshal([]byte(tt.microsecond), &ts)
require.NoError(t, err)
want := tt.t.Truncate(time.Microsecond)
assert.Equal(t, want.UTC(), time.Time(ts).UTC())
})
}
for _, tt := range unmarshalTestCases {
t.Run(tt.name, func(t *testing.T) {
var ts Microsecond
err := json.Unmarshal([]byte(tt.microsecond), &ts)
if tt.wantErr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
} else {
require.NoError(t, err)
want := tt.t.Truncate(time.Microsecond)
assert.Equal(t, want.UTC(), time.Time(ts).UTC())
}
})
}
}
func TestMicrosecond_UnmarshalYAML(t *testing.T) {
for _, tt := range marshalUnmarshalTestCases {
if microsecondSkipTestCase(t, tt.t) {
continue
}
t.Run(tt.name, func(t *testing.T) {
var ts Microsecond
err := yaml.Unmarshal([]byte(tt.microsecond), &ts)
require.NoError(t, err)
want := tt.t.Truncate(time.Microsecond)
assert.Equal(t, want.UTC(), time.Time(ts).UTC())
})
}
for _, tt := range unmarshalTestCases {
t.Run(tt.name, func(t *testing.T) {
var ts Microsecond
err := yaml.Unmarshal([]byte(tt.microsecond), &ts)
if tt.wantErr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
} else {
require.NoError(t, err)
want := tt.t.Truncate(time.Microsecond)
assert.Equal(t, want.UTC(), time.Time(ts).UTC())
}
})
}
}

88
ts/millisecond.go Normal file
View File

@@ -0,0 +1,88 @@
package ts
import (
"strconv"
"time"
"gopkg.in/yaml.v3"
)
// Millisecond is a wrapper around time.Time for marshaling to/from JSON/YAML as
// millisecond-based numeric Unix timestamps.
//
// It marshals to a JSON/YAML number representing the number of milliseconds
// since the Unix time epoch.
//
// It unmarshals from a JSON/YAML number representing the number of milliseconds
// since the Unix time epoch.
type Millisecond time.Time
// Time returns the time.Time corresponding to the millisecond instant s.
func (ms Millisecond) Time() time.Time {
return time.Time(ms)
}
// Local returns the local time corresponding to the millisecond instant s.
func (ms Millisecond) Local() Millisecond {
return Millisecond(time.Time(ms).Local())
}
// GoString implements the fmt.GoStringer interface.
func (ms Millisecond) GoString() string {
return time.Time(ms).GoString()
}
// IsDST reports whether the millisecond instant s occurs within Daylight Saving
// Time.
func (ms Millisecond) IsDST() bool {
return time.Time(ms).IsDST()
}
// IsZero returns true if the Millisecond is the zero value.
func (ms Millisecond) IsZero() bool {
return time.Time(ms).IsZero()
}
// String calls time.Time.String.
func (ms Millisecond) String() string {
return time.Time(ms).String()
}
// UTC returns a copy of the Millisecond with the location set to UTC.
func (ms Millisecond) UTC() Millisecond {
return Millisecond(time.Time(ms).UTC())
}
// MarshalJSON implements the json.Marshaler interface.
func (ms Millisecond) MarshalJSON() ([]byte, error) {
return []byte(strconv.FormatInt(time.Time(ms).UnixMilli(), 10)), nil
}
// UnmarshalJSON implements the json.Unmarshaler interface.
func (ms *Millisecond) UnmarshalJSON(data []byte) error {
i, err := unmarshalBytes(data)
if err != nil {
return err
}
*ms = UnixMilli(i)
return nil
}
// MarshalJSON implements the yaml.Marshaler interface.
func (ms Millisecond) MarshalYAML() (interface{}, error) {
return time.Time(ms).UnixMilli(), nil
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (ms *Millisecond) UnmarshalYAML(node *yaml.Node) error {
i, err := unmarshalYAMLNode(node)
if err != nil {
return err
}
*ms = UnixMilli(i)
return nil
}

168
ts/millisecond_test.go Normal file
View File

@@ -0,0 +1,168 @@
package ts
import (
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
var (
minMilli = time.UnixMilli(-9223372036854775808).UTC()
maxMilli = time.UnixMilli(9223372036854775807).UTC()
)
func millisecondSkipTestCase(t *testing.T, ti time.Time) bool {
t.Helper()
return ti.Before(minMilli) || ti.After(maxMilli)
}
func TestMillisecond_MarshalUnmarshalJSON(t *testing.T) {
for _, tt := range marshalUnmarshalTestCases {
t.Run(tt.name, func(t *testing.T) {
v := Millisecond(tt.t)
b, err := json.Marshal(v)
require.NoError(t, err)
assert.Equal(t, tt.millisecond, string(b))
if millisecondSkipTestCase(t, tt.t) {
return
}
var got Millisecond
err = json.Unmarshal(b, &got)
require.NoError(t, err)
want := tt.t.Truncate(time.Millisecond)
assert.Equal(t, want.UTC(), time.Time(got).UTC())
assert.Equal(t, time.Local, time.Time(got).Location())
})
}
}
func TestMillisecond_MarshalUnmarshalYAML(t *testing.T) {
for _, tt := range marshalUnmarshalTestCases {
t.Run(tt.name, func(t *testing.T) {
v := Millisecond(tt.t)
b, err := yaml.Marshal(v)
require.NoError(t, err)
assert.Equal(t, tt.millisecond+"\n", string(b))
if millisecondSkipTestCase(t, tt.t) {
return
}
var got Millisecond
err = yaml.Unmarshal(b, &got)
require.NoError(t, err)
want := tt.t.Truncate(time.Millisecond)
assert.Equal(t, want.UTC(), time.Time(got).UTC())
assert.Equal(t, time.Local, time.Time(got).Location())
})
}
}
func TestMillisecond_MarshalJSON(t *testing.T) {
for _, tt := range marshalUnmarshalTestCases {
t.Run(tt.name, func(t *testing.T) {
ts := Millisecond(tt.t)
b, err := json.Marshal(ts)
require.NoError(t, err)
assert.Equal(t, tt.millisecond, string(b))
})
}
}
func TestMillisecond_MarshalYAML(t *testing.T) {
for _, tt := range marshalUnmarshalTestCases {
t.Run(tt.name, func(t *testing.T) {
ts := Millisecond(tt.t)
b, err := yaml.Marshal(ts)
require.NoError(t, err)
assert.Equal(t, tt.millisecond+"\n", string(b))
})
}
}
func TestMillisecond_UnmarshalJSON(t *testing.T) {
for _, tt := range marshalUnmarshalTestCases {
if millisecondSkipTestCase(t, tt.t) {
continue
}
t.Run(tt.name, func(t *testing.T) {
var ts Millisecond
err := json.Unmarshal([]byte(tt.millisecond), &ts)
require.NoError(t, err)
want := tt.t.Truncate(time.Millisecond)
assert.Equal(t, want.UTC(), time.Time(ts).UTC())
})
}
for _, tt := range unmarshalTestCases {
t.Run(tt.name, func(t *testing.T) {
var ts Millisecond
err := json.Unmarshal([]byte(tt.millisecond), &ts)
if tt.wantErr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
} else {
require.NoError(t, err)
want := tt.t.Truncate(time.Millisecond)
assert.Equal(t, want.UTC(), time.Time(ts).UTC())
}
})
}
}
func TestMillisecond_UnmarshalYAML(t *testing.T) {
for _, tt := range marshalUnmarshalTestCases {
if millisecondSkipTestCase(t, tt.t) {
continue
}
t.Run(tt.name, func(t *testing.T) {
var ts Millisecond
err := yaml.Unmarshal([]byte(tt.millisecond), &ts)
require.NoError(t, err)
want := tt.t.Truncate(time.Millisecond)
assert.Equal(t, want.UTC(), time.Time(ts).UTC())
})
}
for _, tt := range unmarshalTestCases {
t.Run(tt.name, func(t *testing.T) {
var ts Millisecond
err := yaml.Unmarshal([]byte(tt.millisecond), &ts)
if tt.wantErr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
} else {
require.NoError(t, err)
want := tt.t.Truncate(time.Millisecond)
assert.Equal(t, want.UTC(), time.Time(ts).UTC())
}
})
}
}

92
ts/nanosecond.go Normal file
View File

@@ -0,0 +1,92 @@
package ts
import (
"strconv"
"time"
"gopkg.in/yaml.v3"
)
// Nanosecond is a wrapper around time.Time for marshaling to/from JSON/YAML as
// nanosecond-based numeric Unix timestamps.
//
// It marshals to a JSON/YAML number representing the number of nanoseconds
// since the Unix time epoch.
//
// It unmarshals from a JSON/YAML number representing the number of nanoseconds
// since the Unix time epoch.
type Nanosecond time.Time
// Time returns the time.Time corresponding to the nanosecond instant s.
func (ns Nanosecond) Time() time.Time {
return time.Time(ns)
}
// Local returns the local time corresponding to the nanosecond instant s.
func (ns Nanosecond) Local() Nanosecond {
return Nanosecond(time.Time(ns).Local())
}
// GoString implements the fmt.GoStringer interface.
func (ns Nanosecond) GoString() string {
return time.Time(ns).GoString()
}
// IsDST reports whether the nanosecond instant s occurs within Daylight Saving
// Time.
func (ns Nanosecond) IsDST() bool {
return time.Time(ns).IsDST()
}
// IsZero returns true if the Nanosecond is the zero value.
func (ns Nanosecond) IsZero() bool {
return time.Time(ns).IsZero()
}
// String calls time.Time.String.
func (ns Nanosecond) String() string {
return time.Time(ns).String()
}
// UTC returns a copy of the Nanosecond with the location set to UTC.
func (ns Nanosecond) UTC() Nanosecond {
return Nanosecond(time.Time(ns).UTC())
}
// MarshalJSON implements the json.Marshaler interface.
func (ns Nanosecond) MarshalJSON() ([]byte, error) {
return []byte(strconv.FormatInt(time.Time(ns).UnixNano(), 10)), nil
}
// UnmarshalJSON implements the json.Unmarshaler interface.
func (ns *Nanosecond) UnmarshalJSON(data []byte) error {
i, err := unmarshalBytes(data)
if err != nil {
return err
}
*ns = UnixNano(i)
return nil
}
// MarshalJSON implements the yaml.Marshaler interface.
func (ns Nanosecond) MarshalYAML() (interface{}, error) {
return time.Time(ns).UnixNano(), nil
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (ns *Nanosecond) UnmarshalYAML(node *yaml.Node) error {
i, err := unmarshalYAMLNode(node)
if err != nil {
return err
}
*ns = UnixNano(i)
return nil
}
func unixNano(ts int64) time.Time {
return time.Unix(ts/1e9, ts%1e9)
}

174
ts/nanosecond_test.go Normal file
View File

@@ -0,0 +1,174 @@
package ts
import (
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
var (
minNano = unixNano(-9223372036854775808).UTC()
maxNano = unixNano(9223372036854775807).UTC()
)
func nanosecondSkipTestCase(t *testing.T, ti time.Time) bool {
t.Helper()
return ti.Before(minNano) || ti.After(maxNano)
}
func TestNanosecond_MarshalUnmarshalJSON(t *testing.T) {
for _, tt := range marshalUnmarshalTestCases {
t.Run(tt.name, func(t *testing.T) {
v := Nanosecond(tt.t)
b, err := json.Marshal(v)
require.NoError(t, err)
assert.Equal(t, tt.nanosecond, string(b))
if nanosecondSkipTestCase(t, tt.t) {
return
}
var got Nanosecond
err = json.Unmarshal(b, &got)
require.NoError(t, err)
want := tt.t.Truncate(time.Nanosecond)
assert.Equal(t, want.UTC(), time.Time(got).UTC())
assert.Equal(t, time.Local, time.Time(got).Location())
})
}
}
func TestNanosecond_MarshalUnmarshalYAML(t *testing.T) {
for _, tt := range marshalUnmarshalTestCases {
t.Run(tt.name, func(t *testing.T) {
v := Nanosecond(tt.t)
b, err := yaml.Marshal(v)
require.NoError(t, err)
assert.Equal(t, tt.nanosecond+"\n", string(b))
if nanosecondSkipTestCase(t, tt.t) {
return
}
var got Nanosecond
err = yaml.Unmarshal(b, &got)
require.NoError(t, err)
want := tt.t.Truncate(time.Nanosecond)
assert.Equal(t, want.UTC(), time.Time(got).UTC())
assert.Equal(t, time.Local, time.Time(got).Location())
})
}
}
func TestNanosecond_MarshalJSON(t *testing.T) {
for _, tt := range marshalUnmarshalTestCases {
t.Run(tt.name, func(t *testing.T) {
ts := Nanosecond(tt.t)
b, err := json.Marshal(ts)
require.NoError(t, err)
assert.Equal(t, tt.nanosecond, string(b))
})
}
}
func TestNanosecond_MarshalYAML(t *testing.T) {
for _, tt := range marshalUnmarshalTestCases {
t.Run(tt.name, func(t *testing.T) {
ts := Nanosecond(tt.t)
b, err := yaml.Marshal(ts)
require.NoError(t, err)
assert.Equal(t, tt.nanosecond+"\n", string(b))
})
}
}
func TestNanosecond_UnmarshalJSON(t *testing.T) {
for _, tt := range marshalUnmarshalTestCases {
if nanosecondSkipTestCase(t, tt.t) {
continue
}
t.Run(tt.name, func(t *testing.T) {
var ts Nanosecond
err := json.Unmarshal([]byte(tt.nanosecond), &ts)
require.NoError(t, err)
want := tt.t.Truncate(time.Nanosecond)
assert.Equal(t, want.UTC(), time.Time(ts).UTC())
})
}
for _, tt := range unmarshalTestCases {
if tt.nanosecond == "" {
continue
}
t.Run(tt.name, func(t *testing.T) {
var ts Nanosecond
err := json.Unmarshal([]byte(tt.nanosecond), &ts)
if tt.wantErr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
} else {
require.NoError(t, err)
want := tt.t.Truncate(time.Nanosecond)
assert.Equal(t, want.UTC(), time.Time(ts).UTC())
}
})
}
}
func TestNanosecond_UnmarshalYAML(t *testing.T) {
for _, tt := range marshalUnmarshalTestCases {
if nanosecondSkipTestCase(t, tt.t) {
continue
}
t.Run(tt.name, func(t *testing.T) {
var ts Nanosecond
err := yaml.Unmarshal([]byte(tt.nanosecond), &ts)
require.NoError(t, err)
want := tt.t.Truncate(time.Nanosecond)
assert.Equal(t, want.UTC(), time.Time(ts).UTC())
})
}
for _, tt := range unmarshalTestCases {
if tt.nanosecond == "" {
continue
}
t.Run(tt.name, func(t *testing.T) {
var ts Nanosecond
err := yaml.Unmarshal([]byte(tt.nanosecond), &ts)
if tt.wantErr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
} else {
require.NoError(t, err)
want := tt.t.Truncate(time.Nanosecond)
assert.Equal(t, want.UTC(), time.Time(ts).UTC())
}
})
}
}

88
ts/second.go Normal file
View File

@@ -0,0 +1,88 @@
package ts
import (
"strconv"
"time"
"gopkg.in/yaml.v3"
)
// Second is a wrapper around time.Time for marshaling to/from JSON/YAML as
// second-based numeric Unix timestamps.
//
// It marshals to a JSON/YAML number representing the number of seconds since
// the Unix time epoch.
//
// It unmarshals from a JSON/YAML number representing the number of seconds
// since the Unix time epoch.
type Second time.Time
// Time returns the time.Time corresponding to the second instant s.
func (s Second) Time() time.Time {
return time.Time(s)
}
// Local returns the local time corresponding to the second instant s.
func (s Second) Local() Second {
return Second(time.Time(s).Local())
}
// GoString implements the fmt.GoStringer interface.
func (s Second) GoString() string {
return time.Time(s).GoString()
}
// IsDST reports whether the second instant s occurs within Daylight Saving
// Time.
func (s Second) IsDST() bool {
return time.Time(s).IsDST()
}
// IsZero returns true if the Second is the zero value.
func (s Second) IsZero() bool {
return time.Time(s).IsZero()
}
// String calls time.Time.String.
func (s Second) String() string {
return time.Time(s).String()
}
// UTC returns a copy of the Second with the location set to UTC.
func (s Second) UTC() Second {
return Second(time.Time(s).UTC())
}
// MarshalJSON implements the json.Marshaler interface.
func (s Second) MarshalJSON() ([]byte, error) {
return []byte(strconv.FormatInt(time.Time(s).Unix(), 10)), nil
}
// UnmarshalJSON implements the json.Unmarshaler interface.
func (s *Second) UnmarshalJSON(data []byte) error {
i, err := unmarshalBytes(data)
if err != nil {
return err
}
*s = UnixSecond(i)
return nil
}
// MarshalJSON implements the yaml.Marshaler interface.
func (s Second) MarshalYAML() (interface{}, error) {
return time.Time(s).Unix(), nil
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (s *Second) UnmarshalYAML(node *yaml.Node) error {
i, err := unmarshalYAMLNode(node)
if err != nil {
return err
}
*s = UnixSecond(i)
return nil
}

143
ts/second_test.go Normal file
View File

@@ -0,0 +1,143 @@
package ts
import (
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
func TestSecond_MarshalUnmarshalJSON(t *testing.T) {
for _, tt := range marshalUnmarshalTestCases {
t.Run(tt.name, func(t *testing.T) {
v := Second(tt.t)
b, err := json.Marshal(v)
require.NoError(t, err)
assert.Equal(t, tt.second, string(b))
var got Second
err = json.Unmarshal(b, &got)
require.NoError(t, err)
want := tt.t.Truncate(time.Second)
assert.Equal(t, want.UTC(), time.Time(got).UTC())
assert.Equal(t, time.Local, time.Time(got).Location())
})
}
}
func TestSecond_MarshalUnmarshalYAML(t *testing.T) {
for _, tt := range marshalUnmarshalTestCases {
t.Run(tt.name, func(t *testing.T) {
v := Second(tt.t)
b, err := yaml.Marshal(v)
require.NoError(t, err)
assert.Equal(t, tt.second+"\n", string(b))
var got Second
err = yaml.Unmarshal(b, &got)
require.NoError(t, err)
want := tt.t.Truncate(time.Second)
assert.Equal(t, want.UTC(), time.Time(got).UTC())
assert.Equal(t, time.Local, time.Time(got).Location())
})
}
}
func TestSecond_MarshalJSON(t *testing.T) {
for _, tt := range marshalUnmarshalTestCases {
t.Run(tt.name, func(t *testing.T) {
ts := Second(tt.t)
b, err := json.Marshal(ts)
require.NoError(t, err)
assert.Equal(t, tt.second, string(b))
})
}
}
func TestSecond_MarshalYAML(t *testing.T) {
for _, tt := range marshalUnmarshalTestCases {
t.Run(tt.name, func(t *testing.T) {
ts := Second(tt.t)
b, err := yaml.Marshal(ts)
require.NoError(t, err)
assert.Equal(t, tt.second+"\n", string(b))
})
}
}
func TestSecond_UnmarshalJSON(t *testing.T) {
for _, tt := range marshalUnmarshalTestCases {
t.Run(tt.name, func(t *testing.T) {
var ts Second
err := json.Unmarshal([]byte(tt.second), &ts)
require.NoError(t, err)
want := tt.t.Truncate(time.Second)
assert.Equal(t, want.UTC(), time.Time(ts).UTC())
})
}
for _, tt := range unmarshalTestCases {
t.Run(tt.name, func(t *testing.T) {
var ts Second
err := json.Unmarshal([]byte(tt.second), &ts)
if tt.wantErr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
} else {
require.NoError(t, err)
want := tt.t.Truncate(time.Second)
assert.Equal(t, want.UTC(), time.Time(ts).UTC())
}
})
}
}
func TestSecond_UnmarshalYAML(t *testing.T) {
for _, tt := range marshalUnmarshalTestCases {
t.Run(tt.name, func(t *testing.T) {
var ts Second
err := yaml.Unmarshal([]byte(tt.second), &ts)
require.NoError(t, err)
want := tt.t.Truncate(time.Second)
assert.Equal(t, want.UTC(), time.Time(ts).UTC())
})
}
for _, tt := range unmarshalTestCases {
t.Run(tt.name, func(t *testing.T) {
var ts Second
err := yaml.Unmarshal([]byte(tt.second), &ts)
if tt.wantErr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
} else {
require.NoError(t, err)
want := tt.t.Truncate(time.Second)
assert.Equal(t, want.UTC(), time.Time(ts).UTC())
}
})
}
}

69
ts/ts.go Normal file
View File

@@ -0,0 +1,69 @@
// Package ts provides wrapper types around time.Time, with support for
// JSON/YAML marshaling to/from numeric Unix timestamps of various precisions.
//
// Unmarshaling supports both numeric and string values. Marshaling always
// produces a integer value.
package ts
import (
"time"
"github.com/jimeh/go-tyme/dur"
)
// Timestamp is a type constraint that matches against time.Time, Second,
// Millisecond, Microsecond, and Nanosecond.
type Timestamp interface {
time.Time | Second | Millisecond | Microsecond | Nanosecond
}
// Duration is a type constraint that matches against time.Duration and
// dur.Duration.
type Duration interface {
time.Duration | dur.Duration
}
// Add returns a new Timestamp with given Duration added to it, using
// time.Time.Add.
func Add[T Timestamp, D Duration](ts T, d D) T {
var t time.Time
t = time.Time(ts)
t = t.Add(time.Duration(d))
return T(t)
}
// Sub returns the dur.Duration between two Timestamps, using time.Time.Sub.
func Sub[T, U Timestamp](t T, u U) dur.Duration {
return dur.Duration(time.Time(t).Sub(time.Time(u)))
}
// After reports whether the Timestamp instant t is after u, using
// time.Time.After.
func After[T, U Timestamp](t T, u U) bool {
return time.Time(t).After(time.Time(u))
}
// Before reports whether the Timestamp instant t is before u, using
// time.Time.Before.
func Before[T, U Timestamp](t T, u U) bool {
return time.Time(t).Before(time.Time(u))
}
// Equal reports whether t and u represent the same Timestamp instant, using
// time.Time.Equal.
func Equal[T, U Timestamp](t T, u U) bool {
return time.Time(t).Equal(time.Time(u))
}
// Round returns the result of rounding t to the nearest multiple of d, using
// time.Time.Round.
func Round[T Timestamp, D Duration](t T, d D) T {
return T(time.Time(t).Round(time.Duration(d)))
}
// Truncate returns the result of trucating t down to the nearest multiple of d,
// using time.Time.Truncate.
func Truncate[T Timestamp, D Duration](t T, d D) T {
return T(time.Time(t).Truncate(time.Duration(d)))
}

524
ts/ts_test.go Normal file
View File

@@ -0,0 +1,524 @@
package ts
import (
"fmt"
"testing"
"time"
"github.com/jimeh/go-tyme/dur"
"github.com/stretchr/testify/assert"
)
func TestAdd(t *testing.T) {
testAdd[time.Time, time.Duration](t)
testAdd[time.Time, dur.Duration](t)
testAdd[Second, time.Duration](t)
testAdd[Second, dur.Duration](t)
testAdd[Millisecond, time.Duration](t)
testAdd[Millisecond, dur.Duration](t)
testAdd[Microsecond, time.Duration](t)
testAdd[Microsecond, dur.Duration](t)
testAdd[Nanosecond, time.Duration](t)
testAdd[Nanosecond, dur.Duration](t)
}
func testAdd[T Timestamp, D Duration](t *testing.T) {
t.Run(
fmt.Sprintf("[T %T, D %T]", T(time.Time{}), D(0)),
func(t *testing.T) {
tests := []struct {
name string
t T
d D
want T
}{
{
name: "add 1s",
t: T(
time.Date(2020, 1, 1, 0, 0, 2, 0, time.UTC),
),
d: D(time.Second),
want: T(
time.Date(2020, 1, 1, 0, 0, 3, 0, time.UTC),
),
},
{
name: "remove 1s",
t: T(
time.Date(2020, 1, 1, 0, 0, 2, 0, time.UTC),
),
d: D(-time.Second),
want: T(
time.Date(2020, 1, 1, 0, 0, 1, 0, time.UTC),
),
},
{
name: "add 250ms",
t: T(
time.Date(2020, 1, 1, 0, 0, 2, 0, time.UTC),
),
d: D(250 * time.Millisecond),
want: T(
time.Date(2020, 1, 1, 0, 0, 2, 250000000, time.UTC),
),
},
{
name: "remove 250ms",
t: T(
time.Date(2020, 1, 1, 0, 0, 2, 0, time.UTC),
),
d: D(-250 * time.Millisecond),
want: T(
time.Date(2020, 1, 1, 0, 0, 1, 750000000, time.UTC),
),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Add(tt.t, tt.d)
assert.Equal(t, tt.want, got)
})
}
},
)
}
func TestSub(t *testing.T) {
testSub[time.Time, time.Time](t)
testSub[time.Time, Second](t)
testSub[time.Time, Millisecond](t)
testSub[time.Time, Microsecond](t)
testSub[time.Time, Nanosecond](t)
testSub[Second, time.Time](t)
testSub[Second, Second](t)
testSub[Second, Millisecond](t)
testSub[Second, Microsecond](t)
testSub[Second, Nanosecond](t)
testSub[Millisecond, time.Time](t)
testSub[Millisecond, Millisecond](t)
testSub[Millisecond, Second](t)
testSub[Millisecond, Microsecond](t)
testSub[Millisecond, Nanosecond](t)
testSub[Microsecond, time.Time](t)
testSub[Microsecond, Second](t)
testSub[Microsecond, Millisecond](t)
testSub[Microsecond, Microsecond](t)
testSub[Microsecond, Nanosecond](t)
testSub[Nanosecond, time.Time](t)
testSub[Nanosecond, Second](t)
testSub[Nanosecond, Millisecond](t)
testSub[Nanosecond, Microsecond](t)
testSub[Nanosecond, Nanosecond](t)
}
func testSub[T, U Timestamp](t *testing.T) {
t.Run(
fmt.Sprintf("[T %T, U %T]", T(time.Time{}), U(time.Time{})),
func(t *testing.T) {
now := time.Now()
tests := []struct {
name string
t T
u U
want time.Duration
}{
{
name: "-1s",
t: T(now),
u: U(now.Add(time.Second)),
want: -time.Second,
},
{
name: "1s",
t: T(now),
u: U(now.Add(-time.Second)),
want: time.Second,
},
{
name: "-250ms",
t: T(now),
u: U(now.Add(250 * time.Millisecond)),
want: -250 * time.Millisecond,
},
{
name: "250ms",
t: T(now),
u: U(now.Add(-250 * time.Millisecond)),
want: 250 * time.Millisecond,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Sub(tt.t, tt.u)
assert.Equal(t, tt.want, time.Duration(got))
})
}
t1 := T(now)
u1 := U(now.Add(-time.Second))
got1 := Sub(t1, u1)
assert.Equal(t, time.Second, time.Duration(got1))
t2 := T(now)
u2 := U(now.Add(time.Second))
got2 := Sub(t2, u2)
assert.Equal(t, -time.Second, time.Duration(got2))
},
)
}
func TestAfter(t *testing.T) {
testAfter[time.Time, time.Time](t)
testAfter[time.Time, Second](t)
testAfter[time.Time, Millisecond](t)
testAfter[time.Time, Microsecond](t)
testAfter[time.Time, Nanosecond](t)
testAfter[Second, time.Time](t)
testAfter[Second, Second](t)
testAfter[Second, Millisecond](t)
testAfter[Second, Microsecond](t)
testAfter[Second, Nanosecond](t)
testAfter[Millisecond, time.Time](t)
testAfter[Millisecond, Millisecond](t)
testAfter[Millisecond, Second](t)
testAfter[Millisecond, Microsecond](t)
testAfter[Millisecond, Nanosecond](t)
testAfter[Microsecond, time.Time](t)
testAfter[Microsecond, Second](t)
testAfter[Microsecond, Millisecond](t)
testAfter[Microsecond, Microsecond](t)
testAfter[Microsecond, Nanosecond](t)
testAfter[Nanosecond, time.Time](t)
testAfter[Nanosecond, Second](t)
testAfter[Nanosecond, Millisecond](t)
testAfter[Nanosecond, Microsecond](t)
testAfter[Nanosecond, Nanosecond](t)
}
func testAfter[T, u Timestamp](t *testing.T) {
t.Run(
fmt.Sprintf("[T %T, U %T]", T(time.Time{}), u(time.Time{})),
func(t *testing.T) {
t1 := T(time.Now())
o1 := u(time.Now().Add(-time.Second))
got1 := After(t1, o1)
assert.True(t, got1)
t2 := T(time.Now())
o2 := u(time.Now().Add(time.Second))
got2 := After(t2, o2)
assert.False(t, got2)
},
)
}
func TestBefore(t *testing.T) {
testBefore[time.Time, time.Time](t)
testBefore[time.Time, Second](t)
testBefore[time.Time, Millisecond](t)
testBefore[time.Time, Microsecond](t)
testBefore[time.Time, Nanosecond](t)
testBefore[Second, time.Time](t)
testBefore[Second, Second](t)
testBefore[Second, Millisecond](t)
testBefore[Second, Microsecond](t)
testBefore[Second, Nanosecond](t)
testBefore[Millisecond, time.Time](t)
testBefore[Millisecond, Millisecond](t)
testBefore[Millisecond, Second](t)
testBefore[Millisecond, Microsecond](t)
testBefore[Millisecond, Nanosecond](t)
testBefore[Microsecond, time.Time](t)
testBefore[Microsecond, Second](t)
testBefore[Microsecond, Millisecond](t)
testBefore[Microsecond, Microsecond](t)
testBefore[Microsecond, Nanosecond](t)
testBefore[Nanosecond, time.Time](t)
testBefore[Nanosecond, Second](t)
testBefore[Nanosecond, Millisecond](t)
testBefore[Nanosecond, Microsecond](t)
testBefore[Nanosecond, Nanosecond](t)
}
func testBefore[T, U Timestamp](t *testing.T) {
t.Run(
fmt.Sprintf("[T %T, U %T]", T(time.Time{}), U(time.Time{})),
func(t *testing.T) {
t1 := T(time.Now())
o1 := U(time.Now().Add(-time.Second))
got1 := Before(t1, o1)
assert.False(t, got1)
t2 := T(time.Now())
o2 := U(time.Now().Add(time.Second))
got2 := Before(t2, o2)
assert.True(t, got2)
},
)
}
func TestEqual(t *testing.T) {
testEqual[time.Time, time.Time](t)
testEqual[time.Time, Second](t)
testEqual[time.Time, Millisecond](t)
testEqual[time.Time, Microsecond](t)
testEqual[time.Time, Nanosecond](t)
testEqual[Second, time.Time](t)
testEqual[Second, Second](t)
testEqual[Second, Millisecond](t)
testEqual[Second, Microsecond](t)
testEqual[Second, Nanosecond](t)
testEqual[Millisecond, time.Time](t)
testEqual[Millisecond, Millisecond](t)
testEqual[Millisecond, Second](t)
testEqual[Millisecond, Microsecond](t)
testEqual[Millisecond, Nanosecond](t)
testEqual[Microsecond, time.Time](t)
testEqual[Microsecond, Second](t)
testEqual[Microsecond, Millisecond](t)
testEqual[Microsecond, Microsecond](t)
testEqual[Microsecond, Nanosecond](t)
testEqual[Nanosecond, time.Time](t)
testEqual[Nanosecond, Second](t)
testEqual[Nanosecond, Millisecond](t)
testEqual[Nanosecond, Microsecond](t)
testEqual[Nanosecond, Nanosecond](t)
}
func testEqual[T, U Timestamp](t *testing.T) {
t.Run(
fmt.Sprintf("[T %T, U %T]", T(time.Time{}), U(time.Time{})),
func(t *testing.T) {
t1 := T(time.Now())
o1 := U(t1)
got1 := Equal(t1, o1)
assert.True(t, got1)
t2 := T(time.Now())
o2 := U(time.Now().Add(time.Second))
got2 := Equal(t2, o2)
assert.False(t, got2)
},
)
}
func TestRound(t *testing.T) {
testRound[time.Time, time.Duration](t)
testRound[time.Time, dur.Duration](t)
testRound[Second, time.Duration](t)
testRound[Second, dur.Duration](t)
testRound[Millisecond, time.Duration](t)
testRound[Millisecond, dur.Duration](t)
testRound[Microsecond, time.Duration](t)
testRound[Microsecond, dur.Duration](t)
testRound[Nanosecond, time.Duration](t)
testRound[Nanosecond, dur.Duration](t)
}
func testRound[T Timestamp, D Duration](t *testing.T) {
t.Run(
fmt.Sprintf("[T %T, D %T]", T(time.Time{}), D(0)),
func(t *testing.T) {
tests := []struct {
name string
t T
d D
want T
}{
{
name: "round up to nearest hour",
t: T(
time.Date(2020, 1, 1, 0, 30, 0, 0, time.UTC),
),
d: D(time.Hour),
want: T(time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC)),
},
{
name: "round down to nearest hour",
t: T(
time.Date(2020, 1, 1, 0, 29, 0, 0, time.UTC),
),
d: D(time.Hour),
want: T(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)),
},
{
name: "round up to nearest minute",
t: T(
time.Date(2020, 1, 1, 0, 0, 30, 0, time.UTC),
),
d: D(time.Minute),
want: T(time.Date(2020, 1, 1, 0, 1, 0, 0, time.UTC)),
},
{
name: "round down to nearest minute",
t: T(
time.Date(2020, 1, 1, 0, 0, 29, 0, time.UTC),
),
d: D(time.Minute),
want: T(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)),
},
{
name: "round up to nearest second",
t: T(
time.Date(2020, 1, 1, 0, 0, 2, 500000000, time.UTC),
),
d: D(time.Second),
want: T(time.Date(2020, 1, 1, 0, 0, 3, 0, time.UTC)),
},
{
name: "round down to nearest second",
t: T(
time.Date(2020, 1, 1, 0, 0, 2, 499999999, time.UTC),
),
d: D(time.Second),
want: T(time.Date(2020, 1, 1, 0, 0, 2, 0, time.UTC)),
},
{
name: "round up to nearest millisecond",
t: T(
time.Date(2020, 1, 1, 0, 0, 2, 500000, time.UTC),
),
d: D(time.Millisecond),
want: T(time.Date(2020, 1, 1, 0, 0, 2, 1000000, time.UTC)),
},
{
name: "round down to nearest millisecond",
t: T(
time.Date(2020, 1, 1, 0, 0, 2, 499999, time.UTC),
),
d: D(time.Millisecond),
want: T(time.Date(2020, 1, 1, 0, 0, 2, 0, time.UTC)),
},
{
name: "round up to nearest microsecond",
t: T(
time.Date(2020, 1, 1, 0, 0, 2, 500, time.UTC),
),
d: D(time.Microsecond),
want: T(time.Date(2020, 1, 1, 0, 0, 2, 1000, time.UTC)),
},
{
name: "round down to nearest microsecond",
t: T(
time.Date(2020, 1, 1, 0, 0, 2, 499, time.UTC),
),
d: D(time.Microsecond),
want: T(time.Date(2020, 1, 1, 0, 0, 2, 0, time.UTC)),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Round(tt.t, tt.d)
assert.Equal(t, tt.want, got)
})
}
},
)
}
func TestTruncate(t *testing.T) {
testTruncate[time.Time, time.Duration](t)
testTruncate[time.Time, dur.Duration](t)
testTruncate[Second, time.Duration](t)
testTruncate[Second, dur.Duration](t)
testTruncate[Millisecond, time.Duration](t)
testTruncate[Millisecond, dur.Duration](t)
testTruncate[Microsecond, time.Duration](t)
testTruncate[Microsecond, dur.Duration](t)
testTruncate[Nanosecond, time.Duration](t)
testTruncate[Nanosecond, dur.Duration](t)
}
func testTruncate[T Timestamp, D Duration](t *testing.T) {
t.Run(
fmt.Sprintf("[T %T, D %T]", T(time.Time{}), D(0)),
func(t *testing.T) {
tests := []struct {
name string
t T
d D
want T
}{
{
name: "truncate down to nearest hour",
t: T(
time.Date(2020, 1, 1, 0, 29, 0, 0, time.UTC),
),
d: D(time.Hour),
want: T(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)),
},
{
name: "truncate down to nearest minute",
t: T(
time.Date(2020, 1, 1, 0, 0, 29, 0, time.UTC),
),
d: D(time.Minute),
want: T(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)),
},
{
name: "truncate down to nearest second",
t: T(
time.Date(2020, 1, 1, 0, 0, 2, 499999999, time.UTC),
),
d: D(time.Second),
want: T(time.Date(2020, 1, 1, 0, 0, 2, 0, time.UTC)),
},
{
name: "truncate down to nearest millisecond",
t: T(
time.Date(2020, 1, 1, 0, 0, 2, 499999, time.UTC),
),
d: D(time.Millisecond),
want: T(time.Date(2020, 1, 1, 0, 0, 2, 0, time.UTC)),
},
{
name: "truncate down to nearest microsecond",
t: T(
time.Date(2020, 1, 1, 0, 0, 2, 499, time.UTC),
),
d: D(time.Microsecond),
want: T(time.Date(2020, 1, 1, 0, 0, 2, 0, time.UTC)),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Truncate(tt.t, tt.d)
assert.Equal(t, tt.want, got)
})
}
},
)
}

23
ts/unix.go Normal file
View File

@@ -0,0 +1,23 @@
package ts
import "time"
// UnixSecond parses a given int64 as a Unix timestamp with second accuracy.
func UnixSecond(ts int64) Second {
return Second(time.Unix(ts, 0))
}
// UnixMilli parses a given int64 as a Unix timestamp with millisecond accuracy.
func UnixMilli(ts int64) Millisecond {
return Millisecond(time.UnixMilli(ts))
}
// UnixMicro parses a given int64 as a Unix timestamp with microsecond accuracy.
func UnixMicro(ts int64) Microsecond {
return Microsecond(time.UnixMicro(ts))
}
// UnixNano parses a given int64 as a Unix timestamp with nanosecond accuracy.
func UnixNano(ts int64) Nanosecond {
return Nanosecond(unixNano(ts))
}

81
ts/unix_test.go Normal file
View File

@@ -0,0 +1,81 @@
package ts
import (
"strconv"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestUnixSecond(t *testing.T) {
for _, tt := range marshalUnmarshalTestCases {
i, err := strconv.ParseInt(tt.second, 10, 64)
if err != nil {
continue
}
t.Run(tt.name, func(t *testing.T) {
got := UnixSecond(i)
assert.IsType(t, Second(time.Time{}), got)
want := tt.t.Truncate(time.Second)
assert.Equal(t, want.UTC(), time.Time(got).UTC())
})
}
}
func TestUnixMilli(t *testing.T) {
for _, tt := range marshalUnmarshalTestCases {
i, err := strconv.ParseInt(tt.millisecond, 10, 64)
if err != nil || millisecondSkipTestCase(t, tt.t) {
continue
}
t.Run(tt.name, func(t *testing.T) {
got := UnixMilli(i)
assert.IsType(t, Millisecond(time.Time{}), got)
want := tt.t.Truncate(time.Millisecond)
assert.Equal(t, want.UTC(), time.Time(got).UTC())
})
}
}
func TestUnixMicro(t *testing.T) {
for _, tt := range marshalUnmarshalTestCases {
i, err := strconv.ParseInt(tt.microsecond, 10, 64)
if err != nil || microsecondSkipTestCase(t, tt.t) {
continue
}
t.Run(tt.name, func(t *testing.T) {
got := UnixMicro(i)
assert.IsType(t, Microsecond(time.Time{}), got)
want := tt.t.Truncate(time.Microsecond)
assert.Equal(t, want.UTC(), time.Time(got).UTC())
})
}
}
func TestUnixNano(t *testing.T) {
for _, tt := range marshalUnmarshalTestCases {
i, err := strconv.ParseInt(tt.nanosecond, 10, 64)
if err != nil || nanosecondSkipTestCase(t, tt.t) {
continue
}
t.Run(tt.name, func(t *testing.T) {
got := UnixNano(i)
assert.IsType(t, Nanosecond(time.Time{}), got)
want := tt.t.Truncate(time.Nanosecond)
assert.Equal(t, want.UTC(), time.Time(got).UTC())
})
}
}

51
ts/unmarshal.go Normal file
View File

@@ -0,0 +1,51 @@
package ts
import (
"fmt"
"strconv"
"gopkg.in/yaml.v3"
)
func unmarshalBytes(data []byte) (int64, error) {
s, err := strconv.Unquote(string(data))
if err == nil {
data = []byte(s)
}
i, err := strconv.ParseInt(string(data), 10, 64)
if err != nil {
var f float64
f, err = strconv.ParseFloat(string(data), 64)
i = int64(f)
}
if err != nil {
return 0, fmt.Errorf("invalid numeric timestamp: %s", string(data))
}
return i, nil
}
func unmarshalYAMLNode(node *yaml.Node) (int64, error) {
var i int64
var err error
var invalid bool
switch node.Tag {
case "!!int", "!!str":
i, err = strconv.ParseInt(node.Value, 10, 64)
case "!!float":
var f float64
f, err = strconv.ParseFloat(node.Value, 64)
i = int64(f)
default:
invalid = true
}
if err != nil || invalid {
return 0, &yaml.TypeError{Errors: []string{"invalid numeric timestamp"}}
}
return i, nil
}