it's time :]
Chron is a UTC-first Go time library that wraps time.Time with precision metadata,
calendar-aware durations, and half-open intervals. Use it where instants, buckets, and
fuzzy calendar math need to stay distinct — without leaving the stdlib ecosystem.
The original dustinevan/chron tackled a familiar
problem: time.Time is used for everything — event timestamps, billing months, hourly
rollups, holiday dates — each with different precision and comparison semantics. The
first design split time into three interfaces (chron.Time, dura.Time, chron.Span)
with nine precision structs (Year … Chron) and cross-conversion methods.
This keeps the motivation but simplifies the model: four concrete types you can grep
and reason about — no interface lattice, no separate dura package.
| Type | Role |
|---|---|
Chron |
An instant with optional Precision metadata |
Duration |
Calendar + clock offsets (years, months, weeks, hours, …) |
Span |
Half-open interval [Start, End) for buckets and containment |
Precision |
Truncation, serialization, and span granularity |
Design rationale lives in chron_v1.md and chron_v2.md.
Chron still embeds time.Time for calculations, so accuracy matches the stdlib. All
constructors normalize to UTC.
go get github.com/hydronica/chronRequires Go 1.22+.
import "github.com/hydronica/chron"
now := chron.Now()
feb := chron.Date(2026, 2, 1)
startOfMonth := now.Truncate(chron.Month)
next := now.Add(chron.Months(1).Days(3).Hours(4))
parsed, _ := chron.Parse("2026-02")Truncate(p) returns the inclusive start of the unit containing the instant and sets
precision on the result. Add and Sub apply a Duration using AddDate for calendar
fields and Add for clock fields.
d := chron.Years(1).Months(3).Hours(12)
d, _ = chron.ParseDuration("p1y3m12h")
d.String() // "p1y3m12h"Duration unifies what stdlib splits across AddDate and Add. Parse accepts ISO 8601
(PnYnMnDTnHnMnS) plus chron extensions (bare 14d, ms/µs/ns). Zero durations
marshal as JSON null and stringify to "".
window := expiry.Span(expiry.Precision()) // [Start, End)
db.Query(`WHERE at >= $1 AND at < $2`, window.Start.AsTime(), window.End.AsTime())
if window.Contains(event) { /* membership, not point ordering */ }
if window.Overlaps(other) { /* scheduling conflicts */ }Span is half-open: End is exclusive. Build arbitrary ranges with NewSpan /
MustSpan, or derive a calendar bucket from any instant via c.Span(p).
chron.DefaultWeekStart = chron.MondayWeek // ISO week (default)
monday := now.Truncate(chron.MondayWeek)
sunday := now.Truncate(chron.SundayWeek)
week := now.Truncate(chron.Week) // uses DefaultWeekStartMondayWeek and SundayWeek select explicit week boundaries; stored precision
normalizes to Week.
AsTime()— pass aChronto any API that expectstime.Time- Embedded
time.Time—Format,Unix,Year,Month, and other stdlib methods work directly; useSpan.Containsfor interval membership instead of overloadingBefore/After Parse/ParseFormats— registered layouts set precision from the match- JSON —
MarshalJSONencodes a precision-aware string, ornullwhen zero - SQL —
ScanandValuefordatabase/sqlround-trips
All times belong in UTC until a human needs to see them. Constructors (Now, Date,
Parse, FromTime) guarantee UTC internal storage. Use InLocation for display only.
go test ./...CI runs tests with the race detector on every even go version starting with Go 1.22 and uploads coverage to Codecov using the latest stable Go release.
Open an issue on GitHub if you have questions, ideas, or bugs to report.
