feat(tyme/dur): add dur package as separate module

This commit is contained in:
2022-10-30 03:31:46 +00:00
parent 572d532b77
commit 5d31aa6743
13 changed files with 940 additions and 0 deletions

2
dur/.gitignore vendored Normal file
View File

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

94
dur/.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
dur/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))

11
dur/dur.go Normal file
View File

@@ -0,0 +1,11 @@
// Package dur provides a wrapper dur.Duration around time.Duration, with
// sensible JSON/YAML marshaling/unmarshaling support.
//
// Unmarshaling supports standard time.Duration formats string formats such as
// "5s, ""1h30m", all parsed by time.ParseDuration. It also supports integer and
// float values which are interpreted as seconds, rather than nanoseconds, like
// the regular time.Duration does.
//
// Marshaling always outputs a string, using the standard time.Duration format,
// by calling time.Duration(d).String().
package dur

79
dur/duration.go Normal file
View File

@@ -0,0 +1,79 @@
package dur
import (
"encoding/json"
"strconv"
"time"
"gopkg.in/yaml.v3"
)
// Duration is a wrapper around time.Duration that implements JSON and YAML
// marshaler and unmarshaler interfaces.
//
// When unmarshaling, string values in JSON and YAML are parsed using
// time.ParseDuration. Numeric values are parsed as number of seconds.
//
// When marshaling, the duration is formatted as a string using time.Duration's
// String method.
type Duration time.Duration
// MarshalJSON implements the json.Marshaler interface, returning the duration
// as a string in the format "1h2m3s", same as time.Duration.String().
func (d Duration) MarshalJSON() ([]byte, error) {
return json.Marshal(time.Duration(d).String())
}
// UmarshalJSON implements the json.Unmarshaler interface. Supports string,
// numeric JSON types, converting them to a Duration using ParseDuration.
func (d *Duration) UnmarshalJSON(b []byte) error {
var x interface{}
if err := json.Unmarshal(b, &x); err != nil {
return err
}
pd, err := Parse(x)
if err != nil {
return err
}
*d = pd
return nil
}
// MarshalJSON implements the yaml.Marshaler interface, returning the duration
// as a string in the format 1h2m3s, same as time.Duration.String().
func (d Duration) MarshalYAML() (interface{}, error) {
return time.Duration(d).String(), nil
}
// UmarshalYAML implements the yaml.Unmarshaler interface. Supports string, int
// and float YAML types, converting them to a Duration using ParseDuration.
func (d *Duration) UnmarshalYAML(node *yaml.Node) error {
var x interface{}
var err error
switch node.Tag {
case "!!str":
x = node.Value
case "!!int":
x, err = strconv.Atoi(node.Value)
case "!!float":
x, err = strconv.ParseFloat(node.Value, 64)
default:
return &yaml.TypeError{Errors: []string{"invalid duration"}}
}
if err != nil {
return err
}
pd, err := Parse(x)
if err != nil {
return err
}
*d = pd
return nil
}

View File

@@ -0,0 +1,72 @@
package dur_test
import (
"encoding/json"
"fmt"
"time"
"github.com/jimeh/go-tyme/dur"
"gopkg.in/yaml.v3"
)
func ExampleDuration_MarshalJSON() {
type Connection struct {
Timeout dur.Duration `json:"timeout"`
}
conn := Connection{Timeout: dur.Duration(5 * time.Second)}
b, _ := json.Marshal(conn)
fmt.Println(string(b))
// Output:
// {"timeout":"5s"}
}
func ExampleDuration_UnmarshalJSON() {
type Connection struct {
Timeout dur.Duration `json:"timeout"`
}
conn := Connection{}
_ = json.Unmarshal([]byte(`{"timeout": "10s"}`), &conn)
fmt.Printf("%+v (%+v)\n", conn.Timeout, time.Duration(conn.Timeout))
_ = json.Unmarshal([]byte(`{"timeout": 5}`), &conn)
fmt.Printf("%+v (%+v)\n", conn.Timeout, time.Duration(conn.Timeout))
_ = json.Unmarshal([]byte(`{"timeout": 0.5}`), &conn)
fmt.Printf("%+v (%+v)\n", conn.Timeout, time.Duration(conn.Timeout))
// Output:
// 10000000000 (10s)
// 5000000000 (5s)
// 500000000 (500ms)
}
func ExampleDuration_MarshalYAML() {
type Connection struct {
Timeout dur.Duration `yaml:"timeout"`
}
conn := Connection{Timeout: dur.Duration(5 * time.Second)}
b, _ := yaml.Marshal(conn)
fmt.Println(string(b))
// Output:
// timeout: 5s
}
func ExampleDuration_UnmarshalYAML() {
type Connection struct {
Timeout dur.Duration `yaml:"timeout"`
}
conn := Connection{}
_ = yaml.Unmarshal([]byte(`timeout: 10s`), &conn)
fmt.Printf("%+v (%+v)\n", conn.Timeout, time.Duration(conn.Timeout))
_ = yaml.Unmarshal([]byte(`timeout: 5`), &conn)
fmt.Printf("%+v (%+v)\n", conn.Timeout, time.Duration(conn.Timeout))
_ = yaml.Unmarshal([]byte(`timeout: 0.5`), &conn)
fmt.Printf("%+v (%+v)\n", conn.Timeout, time.Duration(conn.Timeout))
// Output:
// 10000000000 (10s)
// 5000000000 (5s)
// 500000000 (500ms)
}

344
dur/duration_test.go Normal file
View File

@@ -0,0 +1,344 @@
package dur
import (
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
func TestDuration_MarshalUnmarshalJSON(t *testing.T) {
tests := []struct {
name string
d time.Duration
}{
{name: "zero", d: 0},
{name: "1ns", d: 1 * time.Nanosecond},
{name: "2ns", d: 2 * time.Nanosecond},
{name: "1µs", d: 1 * time.Microsecond},
{name: "2µs", d: 2 * time.Microsecond},
{name: "1ms", d: 1 * time.Millisecond},
{name: "2ms", d: 2 * time.Millisecond},
{name: "1s", d: 1 * time.Second},
{name: "2s", d: 2 * time.Second},
{name: "90s", d: 90 * time.Second},
{name: "1m", d: 1 * time.Minute},
{name: "2m", d: 2 * time.Minute},
{name: "90m", d: 90 * time.Minute},
{name: "1h", d: 1 * time.Hour},
{name: "2h", d: 2 * time.Hour},
{name: "36h", d: 36 * time.Hour},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
d := Duration(tt.d)
b, err := json.Marshal(d)
require.NoError(t, err)
var d2 Duration
err = json.Unmarshal(b, &d2)
require.NoError(t, err)
assert.Equal(t, tt.d, time.Duration(d2))
})
}
}
func TestDuration_MarshalJSON(t *testing.T) {
tests := []struct {
name string
d time.Duration
want string
}{
{name: "zero", d: 0, want: `"0s"`},
{name: "1ns", d: 1 * time.Nanosecond, want: `"1ns"`},
{name: "2ns", d: 2 * time.Nanosecond, want: `"2ns"`},
{name: "1µs", d: 1 * time.Microsecond, want: `"1µs"`},
{name: "2µs", d: 2 * time.Microsecond, want: `"2µs"`},
{name: "1ms", d: 1 * time.Millisecond, want: `"1ms"`},
{name: "2ms", d: 2 * time.Millisecond, want: `"2ms"`},
{name: "1s", d: 1 * time.Second, want: `"1s"`},
{name: "2s", d: 2 * time.Second, want: `"2s"`},
{name: "90s", d: 90 * time.Second, want: `"1m30s"`},
{name: "1m", d: 1 * time.Minute, want: `"1m0s"`},
{name: "2m", d: 2 * time.Minute, want: `"2m0s"`},
{name: "90m", d: 90 * time.Minute, want: `"1h30m0s"`},
{name: "1h", d: 1 * time.Hour, want: `"1h0m0s"`},
{name: "2h", d: 2 * time.Hour, want: `"2h0m0s"`},
{name: "36h", d: 36 * time.Hour, want: `"36h0m0s"`},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
d := Duration(tt.d)
b, err := json.Marshal(d)
require.NoError(t, err)
assert.Equal(t, tt.want, string(b))
})
}
}
func TestDuration_UnmarshalJSON(t *testing.T) {
tests := []struct {
name string
s string
want time.Duration
wantErr string
}{
{s: `"1ns"`, want: 1 * time.Nanosecond},
{s: `"2ns"`, want: 2 * time.Nanosecond},
{s: `"1µs"`, want: 1 * time.Microsecond},
{s: `"2µs"`, want: 2 * time.Microsecond},
{s: `"1ms"`, want: 1 * time.Millisecond},
{s: `"2ms"`, want: 2 * time.Millisecond},
{s: `"1s"`, want: 1 * time.Second},
{s: `"2s"`, want: 2 * time.Second},
{s: `"90s"`, want: 90 * time.Second},
{s: `"1m30s"`, want: 90 * time.Second},
{s: `"1m0s"`, want: 1 * time.Minute},
{s: `"2m0s"`, want: 2 * time.Minute},
{s: `"90m"`, want: 90 * time.Minute},
{s: `"1h30m"`, want: 90 * time.Minute},
{s: `"1h30m0s"`, want: 90 * time.Minute},
{s: `"1h0m0s"`, want: 1 * time.Hour},
{s: `"2h0m0s"`, want: 2 * time.Hour},
{s: `"36h0m0s"`, want: 36 * time.Hour},
{s: "0.000000001", want: 1 * time.Nanosecond},
{s: "0.000001", want: 1 * time.Microsecond},
{s: "0.001", want: 1 * time.Millisecond},
{s: "0.1", want: 100 * time.Millisecond},
{s: "1", want: 1 * time.Second},
{s: "1.0", want: 1 * time.Second},
{s: "2.0", want: 2 * time.Second},
{s: "90", want: 90 * time.Second},
{s: "90.001", want: (90 * time.Second) + (1 * time.Millisecond)},
{s: "90.999", want: (90 * time.Second) + (999 * time.Millisecond)},
{name: "empty", s: "", wantErr: "unexpected end of JSON input"},
{
s: "'2ms'",
wantErr: "invalid character '\\'' looking for beginning of value",
},
{
s: "nil",
wantErr: "invalid character 'i' in literal null (expecting 'u')",
},
{s: `"nil"`, wantErr: "time: invalid duration \"nil\""},
{
s: "foo",
wantErr: "invalid character 'o' in literal false (expecting 'a')",
},
{s: `"foo"`, wantErr: "time: invalid duration \"foo\""},
{s: "null", wantErr: "time: invalid duration <nil>"},
{s: `"null"`, wantErr: "time: invalid duration \"null\""},
}
for _, tt := range tests {
name := tt.name
if name == "" {
name = tt.s
}
t.Run(name, func(t *testing.T) {
var d Duration
err := json.Unmarshal([]byte(tt.s), &d)
if tt.wantErr != "" {
assert.EqualError(t, err, tt.wantErr)
} else {
require.NoError(t, err)
}
assert.Equal(t, tt.want, time.Duration(d))
})
}
}
func TestDuration_MarshalUnmarshalYAML(t *testing.T) {
tests := []struct {
name string
d time.Duration
}{
{name: "zero", d: 0},
{name: "1ns", d: 1 * time.Nanosecond},
{name: "2ns", d: 2 * time.Nanosecond},
{name: "1µs", d: 1 * time.Microsecond},
{name: "2µs", d: 2 * time.Microsecond},
{name: "1ms", d: 1 * time.Millisecond},
{name: "2ms", d: 2 * time.Millisecond},
{name: "1s", d: 1 * time.Second},
{name: "2s", d: 2 * time.Second},
{name: "90s", d: 90 * time.Second},
{name: "1m", d: 1 * time.Minute},
{name: "2m", d: 2 * time.Minute},
{name: "90m", d: 90 * time.Minute},
{name: "1h", d: 1 * time.Hour},
{name: "2h", d: 2 * time.Hour},
{name: "36h", d: 36 * time.Hour},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
d := Duration(tt.d)
b, err := yaml.Marshal(d)
require.NoError(t, err)
var d2 Duration
err = yaml.Unmarshal(b, &d2)
require.NoError(t, err)
assert.Equal(t, tt.d, time.Duration(d2))
})
}
}
func TestDuration_MarshalYAML(t *testing.T) {
tests := []struct {
name string
d time.Duration
want string
}{
{name: "zero", d: 0, want: "0s\n"},
{name: "1ns", d: 1 * time.Nanosecond, want: "1ns\n"},
{name: "2ns", d: 2 * time.Nanosecond, want: "2ns\n"},
{name: "1µs", d: 1 * time.Microsecond, want: "1µs\n"},
{name: "2µs", d: 2 * time.Microsecond, want: "2µs\n"},
{name: "1ms", d: 1 * time.Millisecond, want: "1ms\n"},
{name: "2ms", d: 2 * time.Millisecond, want: "2ms\n"},
{name: "1s", d: 1 * time.Second, want: "1s\n"},
{name: "2s", d: 2 * time.Second, want: "2s\n"},
{name: "90s", d: 90 * time.Second, want: "1m30s\n"},
{name: "1m", d: 1 * time.Minute, want: "1m0s\n"},
{name: "2m", d: 2 * time.Minute, want: "2m0s\n"},
{name: "90m", d: 90 * time.Minute, want: "1h30m0s\n"},
{name: "1h", d: 1 * time.Hour, want: "1h0m0s\n"},
{name: "2h", d: 2 * time.Hour, want: "2h0m0s\n"},
{name: "36h", d: 36 * time.Hour, want: "36h0m0s\n"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
d := Duration(tt.d)
b, err := yaml.Marshal(d)
require.NoError(t, err)
assert.Equal(t, tt.want, string(b))
})
}
}
func TestDuration_UnmarshalYAML(t *testing.T) {
tests := []struct {
name string
s string
want time.Duration
wantErr string
}{
{s: `"1ns"`, want: 1 * time.Nanosecond},
{s: `"2ns"`, want: 2 * time.Nanosecond},
{s: `"1µs"`, want: 1 * time.Microsecond},
{s: `"2µs"`, want: 2 * time.Microsecond},
{s: `"1ms"`, want: 1 * time.Millisecond},
{s: `"2ms"`, want: 2 * time.Millisecond},
{s: `"1s"`, want: 1 * time.Second},
{s: `"2s"`, want: 2 * time.Second},
{s: `"90s"`, want: 90 * time.Second},
{s: `"1m30s"`, want: 90 * time.Second},
{s: `"1m0s"`, want: 1 * time.Minute},
{s: `"2m0s"`, want: 2 * time.Minute},
{s: `"90m"`, want: 90 * time.Minute},
{s: `"1h30m"`, want: 90 * time.Minute},
{s: `"1h30m0s"`, want: 90 * time.Minute},
{s: `"1h0m0s"`, want: 1 * time.Hour},
{s: `"2h0m0s"`, want: 2 * time.Hour},
{s: `"36h0m0s"`, want: 36 * time.Hour},
{s: "'1ns'", want: 1 * time.Nanosecond},
{s: "'2ns'", want: 2 * time.Nanosecond},
{s: "'1µs'", want: 1 * time.Microsecond},
{s: "'2µs'", want: 2 * time.Microsecond},
{s: "'1ms'", want: 1 * time.Millisecond},
{s: "'2ms'", want: 2 * time.Millisecond},
{s: "'1s'", want: 1 * time.Second},
{s: "'2s'", want: 2 * time.Second},
{s: "'90s'", want: 90 * time.Second},
{s: "'1m30s'", want: 90 * time.Second},
{s: "'1m0s'", want: 1 * time.Minute},
{s: "'2m0s'", want: 2 * time.Minute},
{s: "'90m'", want: 90 * time.Minute},
{s: "'1h30m'", want: 90 * time.Minute},
{s: "'1h30m0s'", want: 90 * time.Minute},
{s: "'1h0m0s'", want: 1 * time.Hour},
{s: "'2h0m0s'", want: 2 * time.Hour},
{s: "'36h0m0s'", want: 36 * time.Hour},
{s: "1ns", want: 1 * time.Nanosecond},
{s: "2ns", want: 2 * time.Nanosecond},
{s: "1µs", want: 1 * time.Microsecond},
{s: "2µs", want: 2 * time.Microsecond},
{s: "1ms", want: 1 * time.Millisecond},
{s: "2ms", want: 2 * time.Millisecond},
{s: "1s", want: 1 * time.Second},
{s: "2s", want: 2 * time.Second},
{s: "90s", want: 90 * time.Second},
{s: "1m30s", want: 90 * time.Second},
{s: "1m0s", want: 1 * time.Minute},
{s: "2m0s", want: 2 * time.Minute},
{s: "90m", want: 90 * time.Minute},
{s: "1h30m", want: 90 * time.Minute},
{s: "1h30m0s", want: 90 * time.Minute},
{s: "1h0m0s", want: 1 * time.Hour},
{s: "2h0m0s", want: 2 * time.Hour},
{s: "36h0m0s", want: 36 * time.Hour},
{name: "empty", s: "", want: 0},
{s: "nil", wantErr: "time: invalid duration \"nil\""},
{s: "foo", wantErr: "time: invalid duration \"foo\""},
{s: "null", want: 0},
{s: "0.000000001", want: 1 * time.Nanosecond},
{s: "0.000001", want: 1 * time.Microsecond},
{s: "0.001", want: 1 * time.Millisecond},
{s: "0.1", want: 100 * time.Millisecond},
{s: "1", want: 1 * time.Second},
{s: "1.0", want: 1 * time.Second},
{s: "2.0", want: 2 * time.Second},
{s: "90", want: 90 * time.Second},
{s: "90.001", want: (90 * time.Second) + (1 * time.Millisecond)},
{s: "90.999", want: (90 * time.Second) + (999 * time.Millisecond)},
{s: "yes", wantErr: "time: invalid duration \"yes\""},
{s: "no", wantErr: "time: invalid duration \"no\""},
{s: "true", wantErr: "yaml: unmarshal errors:\n invalid duration"},
{s: "false", wantErr: "yaml: unmarshal errors:\n invalid duration"},
{
s: "[foo, bar]",
wantErr: "yaml: unmarshal errors:\n invalid duration",
},
{
s: "{foo: bar}",
wantErr: "yaml: unmarshal errors:\n invalid duration",
},
{
s: "2001-12-15T02:59:43.1Z",
wantErr: "yaml: unmarshal errors:\n invalid duration",
},
}
for _, tt := range tests {
name := tt.name
if name == "" {
name = tt.s
}
t.Run(name, func(t *testing.T) {
var d Duration
err := yaml.Unmarshal([]byte(tt.s), &d)
if tt.wantErr != "" {
assert.EqualError(t, err, tt.wantErr)
} else {
require.NoError(t, err)
}
assert.Equal(t, tt.want, time.Duration(d))
})
}
}

13
dur/go.mod Normal file
View File

@@ -0,0 +1,13 @@
module github.com/jimeh/go-tyme/dur
go 1.18
require (
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
)

17
dur/go.sum Normal file
View File

@@ -0,0 +1,17 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/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=

33
dur/parse.go Normal file
View File

@@ -0,0 +1,33 @@
package dur
import (
"fmt"
"time"
)
const floatSecond = float64(time.Second)
// Parse parses given interface to a Duration.
//
// If the interface is a string, it will be parsed using time.Parse. If
// the interface is a int or float64, it will be parsed as a number of seconds.
func Parse(x interface{}) (Duration, error) {
var d Duration
switch value := x.(type) {
case string:
td, err := time.ParseDuration(value)
if err != nil {
return 0, err
}
d = Duration(td)
case float64:
d = Duration(time.Duration(value * floatSecond))
case int:
d = Duration(time.Duration(value) * time.Second)
default:
return 0, fmt.Errorf("time: invalid duration %+v", x)
}
return d, nil
}

74
dur/parse_test.go Normal file
View File

@@ -0,0 +1,74 @@
package dur
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParse(t *testing.T) {
tests := []struct {
name string
x interface{}
want time.Duration
wantErr string
}{
{x: "1ns", want: 1 * time.Nanosecond},
{x: "2ns", want: 2 * time.Nanosecond},
{x: "1µs", want: 1 * time.Microsecond},
{x: "2µs", want: 2 * time.Microsecond},
{x: "1ms", want: 1 * time.Millisecond},
{x: "2ms", want: 2 * time.Millisecond},
{x: "1s", want: 1 * time.Second},
{x: "2s", want: 2 * time.Second},
{x: "90s", want: 90 * time.Second},
{x: "1m30s", want: 90 * time.Second},
{x: "1m0s", want: 1 * time.Minute},
{x: "2m0s", want: 2 * time.Minute},
{x: "90m", want: 90 * time.Minute},
{x: "1h30m", want: 90 * time.Minute},
{x: "1h30m0s", want: 90 * time.Minute},
{x: "1h0m0s", want: 1 * time.Hour},
{x: "2h0m0s", want: 2 * time.Hour},
{x: "36h0m0s", want: 36 * time.Hour},
{x: 0.000000001, want: 1 * time.Nanosecond},
{x: 0.000001, want: 1 * time.Microsecond},
{x: 0.001, want: 1 * time.Millisecond},
{x: 0.1, want: 100 * time.Millisecond},
{x: 1, want: 1 * time.Second},
{x: 1.0, want: 1 * time.Second},
{x: 2.0, want: 2 * time.Second},
{x: 90, want: 90 * time.Second},
{x: 90.001, want: (90 * time.Second) + (1 * time.Millisecond)},
{x: 90.999, want: (90 * time.Second) + (999 * time.Millisecond)},
{name: "nil", x: nil, wantErr: "time: invalid duration <nil>"},
{name: "empty string", x: "", wantErr: "time: invalid duration \"\""},
{x: "'2ms'", wantErr: "time: invalid duration \"'2ms'\""},
{x: "nil", wantErr: "time: invalid duration \"nil\""},
{x: "foo", wantErr: "time: invalid duration \"foo\""},
{x: "\"foo\"", wantErr: "time: invalid duration \"\\\"foo\\\"\""},
{x: "null", wantErr: "time: invalid duration \"null\""},
{x: "\"null\"", wantErr: "time: invalid duration \"\\\"null\\\"\""},
}
for _, tt := range tests {
name := tt.name
if name == "" {
name = fmt.Sprintf("%#v", tt.x)
}
t.Run(name, func(t *testing.T) {
got, err := Parse(tt.x)
if tt.wantErr != "" {
assert.EqualError(t, err, tt.wantErr)
} else {
require.NoError(t, err)
}
assert.Equal(t, tt.want, time.Duration(got))
})
}
}

6
go.work Normal file
View File

@@ -0,0 +1,6 @@
go 1.19
use (
.
./dur
)

4
go.work.sum Normal file
View File

@@ -0,0 +1,4 @@
github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg=
github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4 h1:8qmTC5ByIXO3GP/IzBkxcZ/99VITvnIETDhdFz/om7A=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=