timex is an extension of the Go standard time library. It wraps time.Time
and time.Duration with first-class support for positive / negative infinity,
adds end-of-month aware date arithmetic, supports Java SimpleDateFormat
style layouts, and provides a small time interval type.
import "go.gh.ink/timex"
- Infinite time & duration — represent
+∞/-∞time points and durations, with consistent comparison, arithmetic and formatting semantics. - End-of-month arithmetic —
AddDateEOMkeeps month-end dates aligned to the last day of the target month (useful for financial schedules). - Java-style layouts — parse and format with patterns such as
yyyy-MM-dd HH:mm:ssin addition to Go's reference-time layouts. - Intervals — model
[start, end],(start, end),[start, end)ranges with inclusive/exclusive bounds and containment checks. - Serialization — all types implement
encoding.TextMarshaler/TextUnmarshaler(JSON, XML, TOML, map keys),encoding.BinaryMarshaler/BinaryUnmarshaler(gob and other binary codecs), anddatabase/sql/driver.Valuer/database/sql.Scanner(SQL columns, viadatabase/sqland ORMs), so they round-trip with no format-specific code. - Drop-in feel — method names and signatures mirror the standard
timepackage, with an extraInfFlagreturn value where infinity matters.
go get go.gh.ink/timexRequires Go 1.24.0 or later.
Every value that may be infinite carries an InfFlag:
| Constant | Value | Meaning |
|---|---|---|
FiniteTime |
0 |
a finite value |
PosInfTime |
1 |
positive infinity |
NegInfTime |
-1 |
negative infinity |
String representations are exposed as PosInfTimeStr ("positive infinite") and
NegInfTimeStr ("negative infinite").
Many accessor methods return the flag alongside the value, e.g.
func (t Time) Year() (year int, inf InfFlag). When inf != FiniteTime, the
numeric result reflects the extreme bound used internally and should be ignored.
Time— a time point that is finite or infinite.Duration— a duration that is finite or infinite.Interval— a range bounded by twoTimevalues with inclusive/exclusive ends.
t := timex.Now()
t = timex.FromStdTime(time.Now())
t = timex.Date(2024, 1, 2, 15, 4, 5, 0, time.UTC)
t = timex.Unix(1704207845, 0)
t = timex.UnixMilli(1704207845000)
t = timex.UnixMicro(1704207845000000)
posInf := timex.NewPosInfTime() // +∞
negInf := timex.NewNegInfTime() // -∞t, err := timex.Parse("yyyy-MM-dd HH:mm:ss", "2024-01-02 15:04:05")
t, err = timex.ParseInLocation("yyyy-MM-dd'T'HH:mm:ssZ", "2024-01-02T15:04:05+0800", time.UTC)
t.Format("yyyy-MM-dd") // "2024-01-02"
t.Format("yyyy-MM-dd HH:mm:ss.SSS") // "2024-01-02 15:04:05.678"
t.Format("EEEE, MMMM dd yyyy") // "Tuesday, January 02 2024"Standard Go reference layouts (e.g. "2006-01-02") are still accepted; the
Java-style conversion only kicks in when a recognized pattern is present.
| Pattern | Go layout | Meaning |
|---|---|---|
yyyy |
2006 |
4-digit year |
yy |
06 |
2-digit year |
MMMM |
January |
full month name |
MMM |
Jan |
short month name |
MM |
01 |
2-digit month |
M |
1 |
month |
dd |
02 |
2-digit day |
d |
2 |
day |
HH/H |
15 |
hour (24h) |
hh |
03 |
hour (12h, padded) |
h |
3 |
hour (12h) |
mm/m |
04/4 |
minute |
ss/s |
05/5 |
second |
SSSSSS |
000000 |
microseconds |
SSS |
000 |
milliseconds |
EEEE |
Monday |
full weekday name |
EEE/E |
Mon |
short weekday name |
a |
PM |
AM/PM marker |
z |
MST |
timezone name |
Z |
-0700 |
timezone offset |
a.After(b) // bool
a.Before(b) // bool
a.Equal(b) // bool
a.Compare(b) // -1, 0, or 1
a.IsZero() // bool
timex.NewPosInfTime().After(timex.Now()) // truelater, err := t.Add(timex.FromStdDuration(time.Hour))
// Standard calendar arithmetic (day overflows into the next month).
t.AddDate(0, 1, 0)
// End-of-month aware: 2024-01-31 + 1 month => 2024-02-29.
t.AddDateEOM(0, 1, 0)
// Difference between two times.
d := a.Sub(b)
d = timex.Since(t) // Now().Sub(t)
d = timex.Until(t) // t.Sub(Now())Adding two opposite infinities (e.g. +∞ + (-∞)) returns ErrInvalidInfiniteOp.
rt, err := t.Round(timex.FromStdDuration(time.Hour))
tt, err := t.Truncate(timex.FromStdDuration(time.Minute))All return the value plus an InfFlag:
year, month, day, inf := t.Date()
hour, min, sec, inf := t.Clock()
y, inf := t.Year()
m, inf := t.Month()
d, inf := t.Day()
h, inf := t.Hour()
mi, inf := t.Minute()
s, inf := t.Second()
ns, inf := t.Nanosecond()
isoYear, isoWeek, inf := t.ISOWeek()
wd, inf := t.Weekday() // 0 = Sunday … 6 = Saturday
t.IsDst() // boolsec, inf := t.Unix()
ms, inf := t.UnixMilli()
us, inf := t.UnixMicro()
ns, inf := t.UnixNano()
stdTime, inf := t.ToStdTime() // back to time.Time + InfFlagt = t.In(loc)
t = t.Local()
t = t.UTC()
name, offset := t.Zone()
loc := t.Location()
bounds, inf := t.ZoneBounds() // Interval covering the current zone offsetInfinite times are preserved across In / Local / UTC.
d := timex.FromStdDuration(2 * time.Hour)
d = timex.NewPosInfDuration()
d = timex.NewNegInfDuration()
std, inf := d.ToStdDuration()
h, inf := d.Hours()
m, inf := d.Minutes()
s, inf := d.Seconds()
ms, inf := d.Milliseconds()
us, inf := d.Microseconds()
ns, inf := d.Nanoseconds()
rd, err := d.Round(timex.FromStdDuration(time.Minute))
td, err := d.Truncate(timex.FromStdDuration(time.Minute))
ad, err := d.Abs()
d.String() // "2h0m0s" or "positive infinite" / "negative infinite"For infinite durations the unit accessors return ±math.MaxFloat64 (floats) or
math.MinInt64/math.MaxInt64 (integers) together with the matching InfFlag.
start := timex.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
end := timex.Date(2024, 1, 2, 0, 0, 0, 0, time.UTC)
mid := timex.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
// [start, end)
iv := timex.NewInterval(start, end, true, false)
iv.Contain(start) // true (start included)
iv.Contain(end) // false (end excluded)
iv.Contain(mid) // true
iv.Before(t) // whole interval lies before t
iv.After(t) // whole interval lies after t
s, sIncluded := iv.Start()
e, eIncluded := iv.End()Combined with infinite times, intervals can model open-ended ranges such as
[start, +∞).
Time, Duration and Interval implement encoding.TextMarshaler /
encoding.TextUnmarshaler. A single text form is therefore reused automatically
by encoding/json, encoding/xml and TOML — for both values and map keys — so
no format-specific code is needed. (YAML's gopkg.in/yaml.v3 only consults its
own Marshaler interface, so add a small MarshalYAML/UnmarshalYAML adapter
there if you need it.)
type Schedule struct {
Name string `json:"name"`
Window timex.Interval `json:"window"`
Every timex.Duration `json:"every"`
Until timex.Time `json:"until"`
}
s := Schedule{
Name: "Q1",
Window: timex.NewInterval(start, end, true, false),
Every: timex.FromStdDuration(24 * time.Hour),
Until: timex.NewPosInfTime(),
}
data, _ := json.Marshal(s)
// {
// "name": "Q1",
// "window": "[2024-01-01T00:00:00Z,2024-04-01T00:00:00Z)",
// "every": "24h0m0s",
// "until": "positive infinite"
// }
var back Schedule
_ = json.Unmarshal(data, &back) // round-trips exactlyText forms:
| Type | Finite | Infinite |
|---|---|---|
Time |
RFC 3339, e.g. 2024-01-02T15:04:05Z |
positive infinite / negative infinite |
Duration |
Go duration, e.g. 24h0m0s |
positive infinite / negative infinite |
Interval |
[start,end) notation (see below) |
endpoints use the Time forms above |
Interval brackets encode the bounds: [/] is inclusive, (/) is exclusive
— e.g. [a,b] closed, (a,b) open, [a,b) left-closed/right-open. Whitespace
around endpoints is tolerated when decoding. Open-ended ranges serialize
naturally, e.g. [2024-01-01T00:00:00Z,positive infinite).
For binary protocols, the same types implement encoding.BinaryMarshaler /
encoding.BinaryUnmarshaler, which encoding/gob uses automatically — no type
registration required:
var buf bytes.Buffer
_ = gob.NewEncoder(&buf).Encode(s) // s is the Schedule above
var back Schedule
_ = gob.NewDecoder(&buf).Decode(&back)The binary form is compact (each value is prefixed by its InfFlag byte, so an
infinite Time/Duration is a single byte) and is meant for machine exchange,
not human inspection — use the text form for JSON and friends.
For databases, the same types implement database/sql/driver.Valuer and
database/sql.Scanner, persisting through their text form. They can be used
directly as column values with database/sql and ORMs built on it (xorm, gorm,
sqlx, …) — no per-app adapter type needed:
type Schedule struct {
Window timex.Interval
Every timex.Duration
Until timex.Time
}
// Window/Every/Until map to text/varchar columns; Insert/Update/Select just work.Store the columns as text/varchar: that representation is portable across
drivers and lossless, preserving infinite bounds and interval inclusivity that
no native SQL type captures. A SQL NULL scans back to the zero value (use a
pointer field or an sql.Null… wrapper if you must distinguish NULL).
All three types also report IsZero() and have a String() form, so they read
naturally in logs and satisfy reflection-based zero-checks (e.g. ORM helpers
deciding whether to write a column).
timex.Sleep(timex.FromStdDuration(time.Second)) // sleeps 1s
timex.Sleep(timex.NewNegInfDuration()) // returns immediately
timex.Sleep(timex.NewPosInfDuration()) // blocks forever| Error | When it occurs |
|---|---|
ErrInvalidInfiniteOp |
Adding ±∞ to ∓∞, or rounding/truncating with infinite bounds |
ErrInvalidInterval |
Decoding an interval from text that is not valid [start,end) notation |
ErrInvalidBinary |
Decoding a value from malformed or truncated binary data |
go test ./... -vLicensed under the Apache License 2.0.