A Go library for extended-hour clocks and business-day date/time handling.
An extended-hour clock can represent values beyond 24:00, such as 26:30
or 30:00. This is useful when a business day does not end at midnight.
With the default cutoff hour of 6, wall-clock 2026-07-05 02:30 is
represented as business 2026-07-04 26:30. With the default system, the valid
clock range is from 00:00:00 through 30:00:00.
Use this package when your domain represents a business day with extended-hour
values such as 26:30 or 30:00.
Do not use it as a replacement for time.Time when you only need ordinary
wall-clock timestamps.
go get github.com/Chiyoshic/go-extclockgo-extclock requires Go 1.20 or later. CI tests Go 1.20.x through the latest stable Go release.
Full API documentation is available on pkg.go.dev.
package main
import (
"fmt"
"log"
"time"
"github.com/Chiyoshic/go-extclock"
)
func main() {
clock, err := extclock.ParseClock("26:30")
if err != nil {
log.Fatal(err)
}
dt, err := extclock.ParseDateTime("2026-07-04 26:30")
if err != nil {
log.Fatal(err)
}
wall := time.Date(2026, time.July, 5, 2, 30, 0, 0, time.UTC)
business, err := extclock.FromTime(wall)
if err != nil {
log.Fatal(err)
}
back, err := extclock.DefaultSystem().ToTime(dt, time.UTC)
if err != nil {
log.Fatal(err)
}
fmt.Println(clock.StringHM())
fmt.Println(business.StringHM())
fmt.Println(back.Format(time.RFC3339))
}Output:
26:30
2026-07-04 26:30
2026-07-05T02:30:00Z
Clock is a date-less extended-hour clock. It has no date and no timezone.
Its validity depends on a System: the default system accepts values up to
30:00:00, while custom systems can use other maximum hours. Package-level
constructors such as NewClock use DefaultSystem().
Date is a calendar date only. It has no clock and no timezone. Dates are
validated against the library's supported year range, and invalid calendar
dates such as 2026-02-31 are rejected.
DateTime is a business date paired with an extended-hour clock. Its clock
validity depends on a System. It represents business-day time, not
necessarily a normalized wall-clock instant.
System owns cutoff-hour-specific rules. The default system uses cutoff hour
6, so MaxHour() is 30. In general, MaxHour() is 24 + CutoffHour().
Use System methods when working with custom cutoff rules.
Package-level functions use DefaultSystem(). Use them when the default
cutoff hour of 6 matches your domain.
package main
import (
"log"
"time"
"github.com/Chiyoshic/go-extclock"
)
func main() {
clock, err := extclock.NewClock(26, 30, 0, 0)
if err != nil {
log.Fatal(err)
}
parsedClock, err := extclock.ParseClock("26:30")
if err != nil {
log.Fatal(err)
}
parsedDateTime, err := extclock.ParseDateTime("2026-07-04 26:30")
if err != nil {
log.Fatal(err)
}
business, err := extclock.FromTime(time.Date(2026, time.July, 5, 2, 30, 0, 0, time.UTC))
if err != nil {
log.Fatal(err)
}
_, _, _, _ = clock, parsedClock, parsedDateTime, business
}Create a Config and then a System when your business day uses a different
cutoff hour. The maximum clock is derived from the cutoff hour.
package main
import (
"log"
"time"
"github.com/Chiyoshic/go-extclock"
)
func main() {
cfg, err := extclock.NewConfig(10)
if err != nil {
log.Fatal(err)
}
s, err := extclock.NewSystem(cfg)
if err != nil {
log.Fatal(err)
}
clock, err := s.ParseClock("33:30")
if err != nil {
log.Fatal(err)
}
dt, err := s.NewDateTime(extclock.MustDate(2026, time.July, 4), clock)
if err != nil {
log.Fatal(err)
}
if err := s.ValidateDateTime(dt); err != nil {
log.Fatal(err)
}
}With cutoff hour 10, the maximum clock is 34:00:00.
Most parse functions have an Auto layout variant. Auto layout detects the
input shape. For formatting, Auto uses the package-defined default behavior for
that value type.
Clock inputs:
| Input | Meaning |
|---|---|
26:30 |
hour and minute |
26:30:00 |
hour, minute, second |
26:30:00.123456789 |
nanosecond precision |
Date inputs:
| Input | Meaning |
|---|---|
2026-07-04 |
ISO date |
20260704 |
compact date |
DateTime inputs:
| Input | Meaning |
|---|---|
2026-07-04 26:30 |
space separator |
2026-07-04T26:30 |
T separator |
2026-07-04 26:30:00.123456789 |
nanosecond precision |
Formatting methods include Clock.Format, Date.Format, DateTime.Format,
and their package-level or System variants. System formatting methods
validate under that system before formatting.
Clock, Date, and DateTime implement text and JSON encoding. JSON values
are strings:
{
"starts_at": "2026-07-04 26:30:00"
}Text and JSON unmarshaling for Clock and DateTime is not tied to a specific
System. When system-specific validation matters, call System.ValidateClock
or System.ValidateDateTime after decoding.
System.FromTime converts wall-clock time to business DateTime.
System.ToTime converts business DateTime back to wall-clock time.
System.DayRange and System.DayRangeDate return a half-open business-day
range [start, end).
Default cutoff examples:
2026-07-05 02:30 -> 2026-07-04 26:30
2026-07-05 06:30 -> 2026-07-05 06:30
Clock duration operations come in two styles:
- Bounded clock operations do not cross the business-day boundary.
- Shift clock operations may cross the boundary and return a
dayOffset.
DateTime duration operations may cross business days directly and return a new
DateTime. Bounded DateTime duration operations reject crossing.
29:30 + 30m -> 30:00
29:30 + 1h -> error for bounded clock add
29:30 + 1h -> 06:30, dayOffset 1 for clock shift
2026-07-04 29:30 + 1h -> 2026-07-05 06:30
Relevant APIs include System.AddClockDurationBounded,
System.SubClockDurationBounded, System.ShiftClockDuration,
System.UnshiftClockDuration, System.AddDateTimeDuration, and
System.AddDateTimeDurationBounded.
DateTime.Compare is a pure value comparison: it compares the stored business
date and clock lexicographically. System.CompareDateTime is system-aware: it
compares normalized wall-clock instants.
Under the default system, these two values may represent the same wall-clock instant:
2026-07-04 30:00
2026-07-05 06:00
That means pure value comparison and system-aware comparison can differ.
package main
import (
"fmt"
"github.com/Chiyoshic/go-extclock"
)
func main() {
s := extclock.DefaultSystem()
a := s.MustDateTime(extclock.MustDate(2026, 7, 4), s.MustClock(30, 0, 0, 0))
b := s.MustDateTime(extclock.MustDate(2026, 7, 5), s.MustClock(6, 0, 0, 0))
fmt.Println(a.Compare(b) == 0)
n, err := s.CompareDateTime(a, b)
if err != nil {
panic(err)
}
fmt.Println(n == 0)
}The package exposes sentinel errors for category checks:
ErrInvalidConfigErrInvalidClockErrInvalidDateErrInvalidDateTimeErrInvalidLayoutErrParseClockErrParseDateErrParseDateTimeErrOutOfRange
Parsing failures may be returned as *ParseError. Use errors.Is to check
error categories and wrapped validation causes. Do not rely on exact error
strings.
package main
import (
"errors"
"fmt"
"time"
"github.com/Chiyoshic/go-extclock"
)
func main() {
_, err := extclock.ClockFromDuration(31 * time.Hour)
if errors.Is(err, extclock.ErrOutOfRange) {
fmt.Println("range error")
}
}- With the default system,
30:00:00is valid and30:00:00.000000001is invalid. - In any system,
MaxHour():00:00is valid; values after that boundary are invalid. Datesupports years1through9999. The zero valueDate{}is invalid.ClockandDateTimedo not carry a timezone.DateTimevalues near the maximum supported date may validate but still fail conversion to wall-clock time if extended-hour normalization would move past year9999.- Very large date/time differences can exceed
time.Durationrange and returnErrOutOfRange. DayRangeandDayRangeDateuse calendar-day arithmetic in the provided location; daylight-saving transitions can affect elapsed wall-clock duration.
go test ./...CI runs formatting, go vet, tests on Go 1.20.x through stable, and race
tests on stable Go.
go-extclock is licensed under the MIT License. See LICENSE.