feat(tyme): add basic tyme package

This commit is contained in:
2022-10-30 03:30:34 +00:00
parent b34d102e09
commit 572d532b77
12 changed files with 945 additions and 0 deletions

2
.gitignore vendored Normal file
View File

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

94
.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
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
go.mod Normal file
View File

@@ -0,0 +1,14 @@
module github.com/jimeh/go-tyme
go 1.18
require (
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
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
)

23
go.sum Normal file
View File

@@ -0,0 +1,23 @@
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
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/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
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/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
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.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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=

27
parse.go Normal file
View File

@@ -0,0 +1,27 @@
package tyme
import "github.com/araddon/dateparse"
var (
// RetryAmbiguousDateWithSwap is option available in dateparse. This var
// controls if Time's unmarshalers enables it or not.
RetryAmbiguousDateWithSwap = false
// PreferMonthFirst is option available in dateparse. This var
// controls if Time's unmarshalers enables it or not.
PreferMonthFirst = false
)
// Parse is a helper function to parse a wide range of string date and time formats using dateparse.ParseAny.
func Parse(s string) (Time, error) {
t, err := dateparse.ParseAny(
s,
dateparse.RetryAmbiguousDateWithSwap(RetryAmbiguousDateWithSwap),
dateparse.PreferMonthFirst(PreferMonthFirst),
)
if err != nil {
return Time{}, err
}
return Time(t), nil
}

79
rfc3339.go Normal file
View File

@@ -0,0 +1,79 @@
package tyme
import (
"strconv"
"time"
"gopkg.in/yaml.v3"
)
// TimeRFC3339 is a wrapper around time.Time that implements JSON/YAML
// marshaling/unmarshaling interfaces. As opposed to Time, only RFC 3339
// formatted input is accepted when unmarshaling.
//
// It marshals to a string in RFC 3339 format, with sub-second precision added
// if present.
//
// It will only unmarshal from a string in RFC 3339 format. Any other format
// will cause an unmarshaling error.
type TimeRFC3339 time.Time
// RFC3339NanoJSON is the time.RFC3339Nano format with double quotes around it.
const RFC3339NanoJSON = `"` + time.RFC3339Nano + `"`
// MarshalJSON implements the json.Marshaler interface, and formats the time as
// a JSON string in RFC 3339 format, with sub-second precision added if
// present.
func (t TimeRFC3339) MarshalJSON() ([]byte, error) {
return []byte(time.Time(t).Format(RFC3339NanoJSON)), nil
}
// UnmarshalJSON implements the json.Unmarshaler interface, and parses a
// JSON string in RFC 3339 format, with sub-second precision added if
// present.
func (t *TimeRFC3339) UnmarshalJSON(b []byte) error {
s, err := strconv.Unquote(string(b))
if err != nil {
return err
}
nt, err := time.Parse(time.RFC3339Nano, s)
if err != nil {
return err
}
*t = TimeRFC3339(nt)
return err
}
// MarshalYAML implements the yaml.Marshaler interface, and formats the time as
// a YAML timestamp type in RFC 3339 string format, with sub-second precision
// added if present.
func (t TimeRFC3339) MarshalYAML() (interface{}, error) {
return time.Time(t), nil
}
// UnmarshalYAML implements the yaml.Unmarshaler interface, and parses a YAML
// timestamp or string formatted according to RFC 3339, with sub-second
// precision added if present.
func (t *TimeRFC3339) UnmarshalYAML(node *yaml.Node) error {
var nt time.Time
var err error
switch node.Tag {
case "!!timestamp":
err = node.Decode(&nt)
case "!!str":
nt, err = time.Parse(time.RFC3339Nano, node.Value)
default:
return &yaml.TypeError{Errors: []string{"invalid time format"}}
}
if err != nil {
return err
}
*t = TimeRFC3339(nt)
return nil
}

163
rfc3339_test.go Normal file
View File

@@ -0,0 +1,163 @@
package tyme
import (
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
var timeRFC3339UnmarshalTestCases = []struct {
name string
s string
want time.Time
}{
{
name: "UTC nanosecond precision",
s: `2022-10-29T14:40:34.934349003Z`,
want: utc.Round(time.Nanosecond),
},
{
name: "UTC microsecond precision",
s: `2022-10-29T14:40:34.934349Z`,
want: utc.Round(time.Microsecond),
},
{
name: "UTC millisecond precision",
s: `2022-10-29T14:40:34.934Z`,
want: utc.Round(time.Millisecond),
},
{
name: "UTC second precision",
s: `2022-10-29T14:40:35Z`,
want: utc.Round(time.Second),
},
{
name: "UTC minute precision",
s: `2022-10-29T14:41:00Z`,
want: utc.Round(time.Minute),
},
{
name: "UTC+8 nanosecond precision",
s: `2022-10-29T22:40:34.934349003+08:00`,
want: utc8.Round(time.Nanosecond),
},
{
name: "UTC+8 microsecond precision",
s: `2022-10-29T22:40:34.934349+08:00`,
want: utc8.Round(time.Microsecond),
},
{
name: "UTC+8 millisecond precision",
s: `2022-10-29T22:40:34.934+08:00`,
want: utc8.Round(time.Millisecond),
},
{
name: "UTC+8 second precision",
s: `2022-10-29T22:40:35+08:00`,
want: utc8.Round(time.Second),
},
{
name: "UTC+8 minute precision",
s: `2022-10-29T22:41:00+08:00`,
want: utc8.Round(time.Minute),
},
}
func TestTimeRFC3339_MarshalUnmarshalJSON(t *testing.T) {
for _, tt := range timeMarshalUnmarshalTestCases {
t.Run(tt.name, func(t *testing.T) {
t1 := TimeRFC3339(tt.t)
b, err := json.Marshal(t1)
require.NoError(t, err)
var t2 TimeRFC3339
err = json.Unmarshal(b, &t2)
require.NoError(t, err)
assert.WithinDuration(t, tt.t, time.Time(t2), time.Nanosecond)
})
}
}
func TestTimeRFC3339_MarshalJSON(t *testing.T) {
for _, tt := range timeMarshalTestCases {
t.Run(tt.name, func(t *testing.T) {
t1 := TimeRFC3339(tt.t)
b, err := json.Marshal(t1)
require.NoError(t, err)
assert.Equal(t, "\""+tt.want+"\"", string(b))
})
}
}
func TestTimeRFC3339_UnmarshalJSON(t *testing.T) {
for _, tt := range timeRFC3339UnmarshalTestCases {
t.Run(tt.name, func(t *testing.T) {
var got TimeRFC3339
err := json.Unmarshal([]byte("\""+tt.s+"\""), &got)
require.NoError(t, err)
assert.WithinDuration(t, tt.want, time.Time(got), time.Nanosecond)
})
}
}
func TestTimeRFC3339_MarshalUnmarshalYAML(t *testing.T) {
for _, tt := range timeMarshalUnmarshalTestCases {
t.Run(tt.name, func(t *testing.T) {
t1 := TimeRFC3339(tt.t)
b, err := yaml.Marshal(t1)
require.NoError(t, err)
var t2 TimeRFC3339
err = yaml.Unmarshal(b, &t2)
require.NoError(t, err)
assert.WithinDuration(t, tt.t, time.Time(t2), time.Nanosecond)
})
}
}
func TestTimeRFC3339_MarshalYAML(t *testing.T) {
for _, tt := range timeMarshalTestCases {
t.Run(tt.name, func(t *testing.T) {
t1 := TimeRFC3339(tt.t)
b, err := yaml.Marshal(t1)
require.NoError(t, err)
assert.Equal(t, tt.want+"\n", string(b))
})
}
}
func TestTimeRFC3339_UnmarshalYAML(t *testing.T) {
for _, tt := range timeRFC3339UnmarshalTestCases {
t.Run(tt.name+" (YAML string type)", func(t *testing.T) {
var got TimeRFC3339
err := yaml.Unmarshal([]byte("\""+tt.s+"\""), &got)
require.NoError(t, err)
assert.WithinDuration(t, tt.want, time.Time(got), time.Nanosecond)
})
t.Run(tt.name+" (YAML timestamp type)", func(t *testing.T) {
var got TimeRFC3339
err := yaml.Unmarshal([]byte(tt.s), &got)
require.NoError(t, err)
assert.WithinDuration(t, tt.want, time.Time(got), time.Nanosecond)
})
}
}

75
time.go Normal file
View File

@@ -0,0 +1,75 @@
package tyme
import (
"strconv"
"time"
"gopkg.in/yaml.v3"
)
// Time is a wrapper around time.Time that implements JSON and YAML
// marshaler and unmarshaler interfaces.
//
// It marshals to a string in RFC 3339 format, with sub-second
// precision added if present.
//
// It unmarshals from a wide range of string date and time formats, by using the
// dateparse package.
type Time time.Time
// MarshalJSON implements the json.Marshaler interface, and formats the time as
// a JSON string in RFC 3339 format, with sub-second precision added if
// present.
func (t Time) MarshalJSON() ([]byte, error) {
return []byte(time.Time(t).Format(RFC3339NanoJSON)), nil
}
// UnmarshalJSON implements the json.Unmarshaler interface, and parses a wide
// range of string date and time formats, by using the dateparse package.
func (t *Time) UnmarshalJSON(b []byte) error {
s, err := strconv.Unquote(string(b))
if err != nil {
return err
}
nt, err := Parse(s)
if err != nil {
return err
}
*t = nt
return err
}
// MarshalYAML implements the yaml.Marshaler interface, and formats the time as
// a YAML timestamp type in RFC 3339 string format, with sub-second precision
// added if present.
func (t Time) MarshalYAML() (interface{}, error) {
return time.Time(t), nil
}
// UnmarshalYAML implements the yaml.Unmarshaler interface, and parses a wide
// range of date and time formats, by using the dateparse package.
func (t *Time) UnmarshalYAML(node *yaml.Node) error {
var nt Time
var err error
switch node.Tag {
case "!!timestamp":
var tt time.Time
err = node.Decode(&tt)
nt = Time(tt)
case "!!str":
nt, err = Parse(node.Value)
default:
return &yaml.TypeError{Errors: []string{"invalid time format"}}
}
if err != nil {
return err
}
*t = nt
return nil
}

36
time_example_test.go Normal file
View File

@@ -0,0 +1,36 @@
package tyme_test
import (
"encoding/json"
"fmt"
"time"
"github.com/jimeh/go-tyme"
"gopkg.in/yaml.v3"
)
func ExampleTime_MarshalJSON() {
type Order struct {
Date tyme.Time `json:"date"`
}
t := time.Date(2006, 1, 2, 15, 4, 5, 999000000, time.UTC)
order := Order{Date: tyme.Time(t)}
b, _ := json.Marshal(order)
fmt.Println(string(b))
// Output:
// {"date":"2006-01-02T15:04:05.999Z"}
}
func ExampleTime_MarshalYAML() {
type Order struct {
Date tyme.Time `yaml:"date"`
}
t := time.Date(2006, 1, 2, 15, 4, 5, 999000000, time.UTC)
order := Order{Date: tyme.Time(t)}
b, _ := yaml.Marshal(order)
fmt.Println(string(b))
// Output:
// date: 2006-01-02T15:04:05.999Z
}

231
time_test.go Normal file
View File

@@ -0,0 +1,231 @@
package tyme
import (
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
var (
loc = time.FixedZone("UTC+8", 8*60*60)
utc = time.Date(2022, 10, 29, 14, 40, 34, 934349003, time.UTC)
utc8 = utc.In(loc)
)
var timeMarshalUnmarshalTestCases = []struct {
name string
t time.Time
}{
{
name: "UTC nanosecond precision",
t: utc.Round(time.Nanosecond),
},
{
name: "UTC microsecond precision",
t: utc.Round(time.Microsecond),
},
{
name: "UTC millisecond precision",
t: utc.Round(time.Millisecond),
},
{
name: "UTC second precision",
t: utc.Round(time.Second),
},
{
name: "UTC minute precision",
t: utc.Round(time.Second),
},
{
name: "UTC+8 nanosecond precision",
t: utc8.Round(time.Nanosecond),
},
{
name: "UTC+8 microsecond precision",
t: utc8.Round(time.Microsecond),
},
{
name: "UTC+8 millisecond precision",
t: utc8.Round(time.Millisecond),
},
{
name: "UTC+8 second precision",
t: utc8.Round(time.Second),
},
{
name: "UTC+8 minute precision",
t: utc8.Round(time.Second),
},
}
var timeMarshalTestCases = []struct {
name string
t time.Time
want string
}{
{
name: "UTC nanosecond precision",
t: utc.Round(time.Nanosecond),
want: `2022-10-29T14:40:34.934349003Z`,
},
{
name: "UTC microsecond precision",
t: utc.Round(time.Microsecond),
want: `2022-10-29T14:40:34.934349Z`,
},
{
name: "UTC millisecond precision",
t: utc.Round(time.Millisecond),
want: `2022-10-29T14:40:34.934Z`,
},
{
name: "UTC second precision",
t: utc.Round(time.Second),
want: `2022-10-29T14:40:35Z`,
},
{
name: "UTC minute precision",
t: utc.Round(time.Minute),
want: `2022-10-29T14:41:00Z`,
},
{
name: "UTC+8 nanosecond precision",
t: utc8.Round(time.Nanosecond),
want: `2022-10-29T22:40:34.934349003+08:00`,
},
{
name: "UTC+8 microsecond precision",
t: utc8.Round(time.Microsecond),
want: `2022-10-29T22:40:34.934349+08:00`,
},
{
name: "UTC+8 millisecond precision",
t: utc8.Round(time.Millisecond),
want: `2022-10-29T22:40:34.934+08:00`,
},
{
name: "UTC+8 second precision",
t: utc8.Round(time.Second),
want: `2022-10-29T22:40:35+08:00`,
},
{
name: "UTC+8 minute precision",
t: utc8.Round(time.Minute),
want: `2022-10-29T22:41:00+08:00`,
},
}
var timeUnmarshalTestCases = []struct {
name string
s string
want time.Time
}{
{s: `1667054434934349003`, want: utc.Round(time.Nanosecond)},
{s: `1667054434934349`, want: utc.Round(time.Microsecond)},
{s: `1667054434934`, want: utc.Round(time.Millisecond)},
{s: `1667054435`, want: utc.Round(time.Second)},
{s: `20221029144035`, want: utc.Round(time.Second)},
{s: `221029 14:40:35`, want: utc.Round(time.Second)},
{s: `October 29th, 2022, 14:40:35`, want: utc.Round(time.Second)},
{s: `2022-10-29 14:40:35`, want: utc.Round(time.Second)},
{s: `2022-10-29T14:40:34.934349003Z`, want: utc.Round(time.Nanosecond)},
{s: `2022-10-29T14:40:34.934349Z`, want: utc.Round(time.Microsecond)},
{s: `2022-10-29T14:40:34.934Z`, want: utc.Round(time.Millisecond)},
{s: `2022-10-29T14:40:35Z`, want: utc.Round(time.Second)},
{s: `2022-10-29T14:41:00Z`, want: utc.Round(time.Minute)},
{s: `2022-10-29T22:40:34.934349003+08:00`, want: utc8.Round(time.Nanosecond)},
{s: `2022-10-29T22:40:34.934349+08:00`, want: utc8.Round(time.Microsecond)},
{s: `2022-10-29T22:40:34.934+08:00`, want: utc8.Round(time.Millisecond)},
{s: `2022-10-29T22:40:35+08:00`, want: utc8.Round(time.Second)},
{s: `2022-10-29T22:41:00+08:00`, want: utc8.Round(time.Minute)},
}
func TestTime_MarshalUnmarshalJSON(t *testing.T) {
for _, tt := range timeMarshalUnmarshalTestCases {
t.Run(tt.name, func(t *testing.T) {
t1 := Time(tt.t)
b, err := json.Marshal(t1)
require.NoError(t, err)
var t2 Time
err = json.Unmarshal(b, &t2)
require.NoError(t, err)
assert.WithinDuration(t, tt.t, time.Time(t2), time.Nanosecond)
})
}
}
func TestTime_MarshalJSON(t *testing.T) {
for _, tt := range timeMarshalTestCases {
t.Run(tt.name, func(t *testing.T) {
t1 := Time(tt.t)
b, err := json.Marshal(t1)
require.NoError(t, err)
assert.Equal(t, "\""+tt.want+"\"", string(b))
})
}
}
func TestTime_UnmarshalJSON(t *testing.T) {
for _, tt := range timeUnmarshalTestCases {
t.Run(tt.s, func(t *testing.T) {
var got Time
err := json.Unmarshal([]byte("\""+tt.s+"\""), &got)
require.NoError(t, err)
assert.WithinDuration(t, tt.want, time.Time(got), time.Nanosecond)
})
}
}
func TestTime_MarshalUnmarshalYAML(t *testing.T) {
for _, tt := range timeMarshalUnmarshalTestCases {
t.Run(tt.name, func(t *testing.T) {
t1 := Time(tt.t)
b, err := yaml.Marshal(t1)
require.NoError(t, err)
var t2 Time
err = yaml.Unmarshal(b, &t2)
require.NoError(t, err)
assert.WithinDuration(t, tt.t, time.Time(t2), time.Nanosecond)
})
}
}
func TestTime_MarshalYAML(t *testing.T) {
for _, tt := range timeMarshalTestCases {
t.Run(tt.name, func(t *testing.T) {
t1 := Time(tt.t)
b, err := yaml.Marshal(t1)
require.NoError(t, err)
assert.Equal(t, tt.want+"\n", string(b))
})
}
}
func TestTime_UnmarshalYAML(t *testing.T) {
for _, tt := range timeUnmarshalTestCases {
t.Run(tt.s, func(t *testing.T) {
var got Time
err := yaml.Unmarshal([]byte("\""+tt.s+"\""), &got)
require.NoError(t, err)
assert.WithinDuration(t, tt.want, time.Time(got), time.Nanosecond)
})
}
}

10
tyme.go Normal file
View File

@@ -0,0 +1,10 @@
// Package tyme provides wrapper types around time.Time, with sensible JSON/YAML
// marshaling/unmarshaling support.
//
// Unmarshaling supports a wide variety of formats, automatically doing a best
// effort to understand the given input, thanks to using the
// github.com/araddon/dateparse package.
//
// Marshaling always produces a string in RFC 3339 format, by simply formatting
// the Time with the time.RFC3339Nano layout.
package tyme