add ISODuration type for ISO8061 duration compatibility

This commit is contained in:
2025-12-11 20:25:29 -08:00
parent 6ecb12c98c
commit a84cfde045
4 changed files with 140 additions and 0 deletions
+18
View File
@@ -1,3 +1,21 @@
# clocktime
clocktime provides a type that holds a calendar-independent time of day value in 24-hour format.
clocktime provides basic types that extend Go's built-in time.Time to provide time-of-day and duration support.
## `clocktime.ClockTime
Holds calendar-independent time-of-day in 24-hour format.
- Converts to and from `HH:MM:SS` format (subset of ISO8061)
- Marshals to `[]byte` containing string `HH:MM:SS` for SQL
- Marshals to `string` in format `HH:MM:SS` for JSON & gqlgen
## `clocktime.Duration`
Wraps time.Duration with prioritized ISO8061 support and opinionated marshalling.
- Converts to and from ISO8061 format (utilises [sosodev/duration](https://github.com/sosodev/duration))
- Marshals to `uint64` with nanosecond precision. Largest representatable duration is about 290 years, limited by underlying time.Duration type.
- Marshals to `string` in ISO8061 format for JSON & gqlgen
+99
View File
@@ -0,0 +1,99 @@
package clocktime
import (
"encoding/json"
"fmt"
"io"
"time"
"github.com/sosodev/duration"
)
// ISODuration wraps a time.Duration to provide custom JSON and SQL
// serialization and full ISO8061 compatibility. Note: ISODuration is limited
// to the same range as time.Duration despite ISO8061 supporting larger durations.
type ISODuration time.Duration
// NewISODuration creates a new duration from numeric components.
func NewISODuration(hours, minutes, seconds int) ISODuration {
return ISODuration(
time.Hour*time.Duration(hours) +
time.Minute*time.Duration(minutes) +
time.Second*time.Duration(seconds),
)
}
// DurationFromISOString parses an ISO8601 duration string (e.g., "PT1H30M45S")
// and returns an ISODuration.
func DurationFromISOString(s string) (ISODuration, error) {
d, err := duration.Parse(s)
if err != nil {
return 0, fmt.Errorf("error parsing ISO8061 duration: %w", err)
}
return ISODuration(d.ToTimeDuration()), nil
}
// ISODurationFromDuration converts a time.Duration to an ISODuration.
func ISODurationFromDuration(d time.Duration) ISODuration {
return ISODuration(d)
}
// String returns the ISO8601 string representation of the ISODuration.
func (d ISODuration) String() string {
td := time.Duration(d)
isoDur := duration.FromTimeDuration(td)
return isoDur.String()
}
// Duration returns the time.Duration representation of the ISODuration.
func (d ISODuration) Duration() time.Duration {
return time.Duration(d)
}
// MarshalJSON implements the json.Marshaler interface for ISODuration,
// serializing it as an ISO8601 duration string.
func (d ISODuration) MarshalJSON() ([]byte, error) {
return json.Marshal("\"" + d.String() + "\"")
}
// UnmarshalJSON implements the json.Unmarshaler interface for ISODuration,
// parsing an ISO8601 duration string.
func (d *ISODuration) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
parsedDur, err := DurationFromISOString(s)
if err != nil {
return err
}
*d = parsedDur
return nil
}
// MarshalGQL implements the graphql.Marshaler interface for ISODuration,
// serializing it as an ISO8601 duration string.
func (d ISODuration) MarshalGQL(w io.Writer) {
fmt.Fprint(w, "\""+d.String()+"\"")
}
// UnmarshalGQL implements the graphql.Unmarshaler interface for ISODuration,
// parsing an ISO8601 duration string. nil values are treated as zero duration.
func (d *ISODuration) UnmarshalGQL(value any) error {
if value == nil {
*d = ISODuration(0)
return nil
}
s, ok := value.(string)
if !ok {
return fmt.Errorf("ISODuration must be a string")
}
parsedDur, err := DurationFromISOString(s)
if err != nil {
return err
}
*d = parsedDur
return nil
}
+11
View File
@@ -1,3 +1,14 @@
module gitea.auvem.com/go-toolkit/clocktime
go 1.24.0
require (
github.com/sosodev/duration v1.3.1
github.com/stretchr/testify v1.11.1
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+12
View File
@@ -0,0 +1,12 @@
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/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=