commit cef104053234f26c03cfff53a0cf1aa027e09833 Author: Elijah Duffy Date: Tue Jul 22 18:22:49 2025 -0700 initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..3c91d33 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# clocktime + +clocktime provides a type that holds a calendar-independent time of day value in 24-hour format. diff --git a/clocktime.go b/clocktime.go new file mode 100644 index 0000000..1fd46c3 --- /dev/null +++ b/clocktime.go @@ -0,0 +1,117 @@ +package clocktime + +import ( + "database/sql/driver" + "encoding/json" + "fmt" + "io" + "time" +) + +// ClockTime represents a time of day without a date component in 24-hour format. +// It is used to represent times in a way that is independent of any specific date. +type ClockTime struct { + Hour int `json:"hour"` + Minute int `json:"minute"` + Second int `json:"second"` +} + +// NewClockTime creates a new ClockTime instance. +func NewClockTime(hour, minute, second int) ClockTime { + return ClockTime{ + Hour: hour, + Minute: minute, + Second: second, + } +} + +// ClockTimeFromString parses a ClockTime from a string in the format "HH:MM:SS" (Subset of RFC 8601). +func ClockTimeFromString(s string) (ClockTime, error) { + var hour, minute, second int + n, err := fmt.Sscanf(s, "%d:%d:%d", &hour, &minute, &second) + if err != nil || n != 3 { + return ClockTime{}, fmt.Errorf("invalid time format: %s", s) + } + if hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59 { + return ClockTime{}, fmt.Errorf("time out of range: %s", s) + } + return NewClockTime(hour, minute, second), nil +} + +// ClockTimeFromTime converts a time.Time to a ClockTime. +func ClockTimeFromTime(t time.Time) ClockTime { + return NewClockTime(t.Hour(), t.Minute(), t.Second()) +} + +// String returns the string representation of the ClockTime in "HH:MM:SS" format. +func (t ClockTime) String() string { + return fmt.Sprintf("%02d:%02d:%02d", t.Hour, t.Minute, t.Second) +} + +// Time returns a time.Time representation of the ClockTime. +// It uses a fixed date (January 1, 1970) to create a time.Time object. +func (t ClockTime) Time() time.Time { + return time.Date(1970, 1, 1, t.Hour, t.Minute, t.Second, 0, time.UTC) +} + +// MarshalJSON implements the json.Marshaler interface for ClockTime. +func (t ClockTime) MarshalJSON() ([]byte, error) { + return json.Marshal(t.String()) +} + +// UnmarshalJSON implements the json.Unmarshaler interface for ClockTime. +func (t *ClockTime) UnmarshalJSON(data []byte) error { + var timeString string + if err := json.Unmarshal(data, &timeString); err != nil { + return err + } + parsedTime, err := ClockTimeFromString(timeString) + if err != nil { + return err + } + *t = parsedTime + return nil +} + +// MarshalGQL implements the graphql.Marshaler interface for ClockTime. +func (t ClockTime) MarshalGQL(w io.Writer) { + fmt.Fprint(w, t.String()) +} + +// UnmarshalGQL implements the graphql.Unmarshaler interface for ClockTime. +func (t *ClockTime) UnmarshalGQL(value any) error { + if value == nil { + *t = ClockTime{} + return nil + } + str, ok := value.(string) + if !ok { + return fmt.Errorf("ClockTime must be a string, got %T", value) + } + parsedTime, err := ClockTimeFromString(str) + if err != nil { + return err + } + *t = parsedTime + return nil +} + +// Value implements the database/sql/driver.Valuer interface for ClockTime. +// Marshals the ClockTime to a time.Time for database storage. +func (t ClockTime) Value() (driver.Value, error) { + return t.Time(), nil +} + +// Scan implements the database/sql.Scanner interface for ClockTime. +func (t *ClockTime) Scan(value any) error { + if value == nil { + *t = ClockTime{} + return nil + } + rawTime, ok := value.(time.Time) + if !ok { + return fmt.Errorf("ClockTime must be a time.Time, got %T", value) + } + *t = ClockTimeFromTime(rawTime) + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b74beee --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module gitea.auvem.com/go-toolkit/clocktime + +go 1.24.0