mirror of
https://github.com/jimeh/go-tyme.git
synced 2026-02-19 01:46:41 +00:00
feat(tyme): add basic tyme package
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
bin/*
|
||||
coverage.*
|
||||
94
.golangci.yml
Normal file
94
.golangci.yml
Normal 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
191
Makefile
Normal 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
14
go.mod
Normal 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
23
go.sum
Normal 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
27
parse.go
Normal 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
79
rfc3339.go
Normal 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
163
rfc3339_test.go
Normal 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
75
time.go
Normal 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
36
time_example_test.go
Normal 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
231
time_test.go
Normal 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
10
tyme.go
Normal 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
|
||||
Reference in New Issue
Block a user