diff --git a/dur/.gitignore b/dur/.gitignore new file mode 100644 index 0000000..2e64ef0 --- /dev/null +++ b/dur/.gitignore @@ -0,0 +1,2 @@ +bin/* +coverage.* diff --git a/dur/.golangci.yml b/dur/.golangci.yml new file mode 100644 index 0000000..1269134 --- /dev/null +++ b/dur/.golangci.yml @@ -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 diff --git a/dur/Makefile b/dur/Makefile new file mode 100644 index 0000000..8895cd7 --- /dev/null +++ b/dur/Makefile @@ -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)) diff --git a/dur/dur.go b/dur/dur.go new file mode 100644 index 0000000..4707459 --- /dev/null +++ b/dur/dur.go @@ -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 diff --git a/dur/duration.go b/dur/duration.go new file mode 100644 index 0000000..d6b8aec --- /dev/null +++ b/dur/duration.go @@ -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 +} diff --git a/dur/duration_example_test.go b/dur/duration_example_test.go new file mode 100644 index 0000000..cc3ee18 --- /dev/null +++ b/dur/duration_example_test.go @@ -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) +} diff --git a/dur/duration_test.go b/dur/duration_test.go new file mode 100644 index 0000000..f367f36 --- /dev/null +++ b/dur/duration_test.go @@ -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 "}, + {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)) + }) + } +} diff --git a/dur/go.mod b/dur/go.mod new file mode 100644 index 0000000..f182c64 --- /dev/null +++ b/dur/go.mod @@ -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 +) diff --git a/dur/go.sum b/dur/go.sum new file mode 100644 index 0000000..2ec90f7 --- /dev/null +++ b/dur/go.sum @@ -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= diff --git a/dur/parse.go b/dur/parse.go new file mode 100644 index 0000000..031f573 --- /dev/null +++ b/dur/parse.go @@ -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 +} diff --git a/dur/parse_test.go b/dur/parse_test.go new file mode 100644 index 0000000..7331249 --- /dev/null +++ b/dur/parse_test.go @@ -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 "}, + {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)) + }) + } +} diff --git a/go.work b/go.work new file mode 100644 index 0000000..d3de252 --- /dev/null +++ b/go.work @@ -0,0 +1,6 @@ +go 1.19 + +use ( + . + ./dur +) diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..5482b9e --- /dev/null +++ b/go.work.sum @@ -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=