gotime

package module
v0.6.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 12, 2026 License: MIT Imports: 13 Imported by: 0

README

Go Time

Go Version License

A Go time semantics library for parsing, computing, and serializing precise time value objects. Hands off to stdlib types — time.Time, time.Duration — for any rendering / formatting need.

Features

  • Typed value objects: Work with Instant, DateTime, LocalDateTime, Date, Time, Duration, Period, Interval, Zone
  • Two parse entry points: Parse returns a tagged-sum ParseResult; typed ParseInstant / ParseDateTime / ParseLocalDateTime / … helpers return a typed (value, error) pair
  • One concept per type: Duration is exact nanoseconds, Period is calendar Y/M/D — P1Y parses as Period, PT1H as Duration, mixed inputs are rejected
  • Stdlib bridges: .Std(), .Clock(), Duration.Decompose() send values back to time.Time / time.Duration / structured clock slots so any formatter can consume them
  • Timezone-aware semantics: Strict IANA zones, fuzzy zone resolution, explicit local-time resolution, floating-time detection (ParseResult.HasZone)
  • Calendar-safe computation: DateTime.Add is exact; AddPeriod applies end-of-month clamping and exposes DST gaps/overlaps as a resolution
  • Stable JSON schemas: Deterministic wire formats with strict decoding; no schema depends on time.Now() or runtime locale
  • No display locale stack: This package ships no formatter types, CLDR data, or message-format dependency. Parsing includes small, controlled input phrase grammars selected by language.Tag; display remains the caller's choice.

Installation

go get github.com/agentable/go-time

Requires Go 1.26.5 or newer.

Quick Start

Most code knows what type it expects — reach for the typed helpers (ParseDateTime, ParseLocalDateTime, ParseInstant, ParseDate, …). They return (value, error) and hide the tagged-sum dispatch:

package main

import (
	"fmt"
	"log"

	gotime "github.com/agentable/go-time"
)

func main() {
	dt, err := gotime.ParseDateTime("2026-03-27T13:00:00",
		gotime.WithZone(gotime.MustLoadZone("Asia/Tokyo")),
	)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(dt.Std().Format("2006-01-02 15:04 MST"))
	// 2026-03-27 13:00 JST
}
Natural language

Pass a locale, an explicit reference instant, and the zone whose calendar defines natural date/datetime phrases. Product code chooses "now" and its calendar frame at the boundary; tests pin both:

zone := gotime.MustLoadZone("America/New_York")
now := gotime.Now()

dt, err := gotime.ParseDateTime("tomorrow at 3pm",
	gotime.WithInputLocale(language.English),
	gotime.WithZone(zone),
	gotime.WithReference(now),
)

Natural datetimes keep their wall-clock intent until the same LocalDateTime.Resolve path used by formal local input applies the zone. A DST gap returns ErrNonexistentTime; a fold remains ambiguous through ParseResult.Candidates and typed parsing returns ErrDuplicateTime.

Deterministic tests

Use a fixed reference instant when you need reproducible output:

ref, _ := gotime.ParseInstant("2026-03-30T12:00:00Z")

dt, err := gotime.ParseDateTime("tomorrow at 3pm",
	gotime.WithInputLocale(language.English),
	gotime.WithZone(gotime.MustLoadZone("America/New_York")),
	gotime.WithReference(ref),
)

Parsing

Typed helpers — the default path

When the kind is known up front, the typed helpers return (value, error) and translate StatusAmbiguous / StatusInvalid / Kind mismatch into a *TimeError:

i,  err := gotime.ParseInstant("2026-03-27T04:00:00Z")
dt, err := gotime.ParseDateTime("2026-03-27T13:00:00", gotime.WithZone(gotime.MustLoadZone("Asia/Tokyo")))
ldt, err := gotime.ParseLocalDateTime("2026-03-27T13:00:00")
d,  err := gotime.ParseDate("2026-03-27")
t,  err := gotime.ParseTime("15:00")
du, err := gotime.ParseDuration("PT1H30M")
p,  err := gotime.ParsePeriod("P1Y3M")
iv, err := gotime.ParseInterval("2026-03-27T00:00:00Z/2026-03-28T00:00:00Z")

A mismatched kind (e.g. ParseInstant("2026-03-27")) returns *TimeError wrapping ErrIncompatibleTypes.

Parse — inspection / polymorphic dispatch

Reach for Parse when you do not know the kind ahead of time, or when you need access to Candidates, Warnings, HasZone, or Reference. Dispatch through Status, Kind, and the comma-ok accessors:

r := gotime.Parse(input, opts...)

switch r.Status {
case gotime.StatusResolved:
	switch r.Kind {
	case gotime.KindDateTime:
		if dt, ok := r.DateTime(); ok {
			fmt.Println(dt)
		}
	case gotime.KindLocalDateTime:
		if ldt, ok := r.LocalDateTime(); ok {
			fmt.Println(ldt)
		}
	case gotime.KindDate:
		if d, ok := r.Date(); ok {
			fmt.Println(d)
		}
	case gotime.KindDuration:
		if d, ok := r.Duration(); ok {
			fmt.Println(d)
		}
	}
case gotime.StatusAmbiguous:
	for _, c := range r.Candidates {
		fmt.Println(c.Kind)
	}
case gotime.StatusInvalid:
	fmt.Println(r.Error.Code, r.Error.Hint)
}

The comma-ok accessors return ok=false unless the result is resolved and the kind matches the accessor.

Supported inputs
Input kind Examples Routes to
RFC 3339 datetime with offset 2026-03-27T13:00:00+09:00, 20260327T130000+0900, 2026-03-27T04:00:00Z KindInstant
Local datetime 2026-03-27T13:00:00, 20260327T130000 KindLocalDateTime; KindDateTime only with WithZone
Date 2026-03-27, 2026-W13-5, 2026-086, 04/05/2026 (locale-disambiguated) KindDate
Time 15:00, 08:30:45, 3:30 PM KindTime
ISO duration (clock only) PT1H30M, PT0S KindDuration
ISO period (calendar only) P1Y3M7D, P2W, P5D KindPeriod
Mixed P{date}T{time} P1DT2H, P1Y2DT3H StatusInvalid + CodeInvalidFormat
Interval 2026-03-27T00:00:00Z/2026-03-28T00:00:00Z, 2026-03-27T09:00:00Z/PT9H KindInterval
Natural language tomorrow at 3pm, 下周五下午三点, 明日, 다음 주 금요일 오후 3시 varies

Duration and Period are distinct types and cannot mix in a single ISO string: a 24-hour clock span is PT24H (Duration), a calendar day is P1D (Period).

Parse options
Option Purpose
WithInputLocale(tag language.Tag) Language hint for natural-language input. Supported slash policies make 04/05/2026 May 4 in en-GB/en-AU and April 5 in en-US; unsupported tags remain ambiguity-aware.
WithZone(zone) Resolve formal floating datetimes; also supplies the required calendar frame for relative natural dates/datetimes
WithReference(instant) Anchor relative natural date/datetime expressions such as tomorrow or next Friday; use with WithZone

Ambiguity is surfaced through ParseResult.Candidates, not pre-selected by an option — callers decide. There is no WithStrategy knob.

Relative natural dates and datetimes require both WithReference and WithZone: omitting the reference returns ErrInvalidFormat; omitting its calendar zone returns ErrInvalidZone. Exact natural durations and periods do not require either option.

WithInputLocale takes golang.org/x/text/language.Tag — the de facto Go BCP-47 type. Only the language subtag is used; Unicode -u- extensions (hour cycle, calendar) belong to the rendering layer, not the parser.

ParseResult.HasZone reports whether the input itself included an explicit zone or offset. Formal floating datetime input without WithZone stays as KindLocalDateTime; adding WithZone resolves it into a zoned DateTime. Relative natural dates and datetimes are different: their reference instant has no calendar meaning until WithZone is supplied.

When the zone identifier comes from user input, resolve it at the call site:

z, err := gotime.ResolveZone(userInput)
if err != nil {
	return err
}
r := gotime.Parse(input, gotime.WithZone(z))

Formatting

This package does not format. Display is the caller's concern. Every value object exposes a small, stable bridge back to stdlib so any formatter can consume it.

Stdlib time.Format
fmt.Println(dt.Std().Format("2006-01-02 15:04 MST"))
// 2026-03-27 13:00 JST
External renderers
type DateTimeRenderer interface {
	Format(time.Time) string
}

func render(dt gotime.DateTime, renderer DateTimeRenderer) string {
	return renderer.Format(dt.Std())
}
Duration → clock slots, Period → exported fields
// Duration: modular arithmetic is non-trivial, so a helper does it for you.
c := d.Decompose() // DurationComponents{Hours, Minutes, Seconds, ...}

clockSlots := struct {
	Hours        int64
	Minutes      int64
	Seconds      int64
	Milliseconds int64
}{
	Hours:        c.Hours,
	Minutes:      c.Minutes,
	Seconds:      c.Seconds,
	Milliseconds: c.Milliseconds,
}

// Period: fields are already exported — no Decompose, no parallel struct.
calendarSlots := struct {
	Years  int32
	Months int32
	Days   int32
}{
	Years:  p.Years,
	Months: p.Months,
	Days:   p.Days,
}
Bridge methods
Method Returns
Instant.Std() time.Time (UTC)
DateTime.Std() time.Time in the DateTime's zone
DateTime.Clock() gotime.Time (clock-time accessor)
Interval.StdRange() (start, end time.Time)
Duration.Std() time.Duration
Duration.Decompose() DurationComponents (clock slots)
Period.Years / .Months / .Days int32 fields (no Decompose)

Civil Date and Time values must be resolved before crossing to stdlib:

resolution := gotime.NewLocalDateTime(date, clock).Resolve(zone)
dt, err := resolution.Only()
if err != nil {
	return err
}
std := dt.Std()

See SPECS/30-formatting.md for the full contract.

Core Types

Type Meaning
Instant Absolute UTC moment
DateTime Zoned local date and time
LocalDateTime Date plus clock time before zone resolution
Date Calendar date without time
Time Clock time without date
Duration Exact elapsed nanoseconds (type Duration time.Duration)
Period Calendar offset of Years/Months/Days
Interval Half-open range [start, end)
Zone IANA timezone identity for persisted zones
DurationComponents Clock-slot decomposition of Duration (Hours…Nanoseconds) — bridge to external duration formatters

Checked civil values use the fixed wire-compatible year domain 0000..9999. DateFromTime, DateTimeFromTime, Instant.In, DateTime.In, and checked calendar/exact arithmetic return ErrOverflow instead of producing a value that cannot round-trip through JSON.

Duration vs Period

Duration is exact nanoseconds — composed from typed constants (Nanosecond, Microsecond, Millisecond, Second, Minute, Hour):

quarter   := 15 * gotime.Minute
twoDaysExact := 48 * gotime.Hour  // 48 exact elapsed hours

Period is a calendar offset — built via struct literal or sugar constructors (Years, Months, Days). Calendar days are DST-safe; they preserve wall-clock time across the boundary:

nextMonth   := gotime.Period{Months: 1}
twoCalDays  := gotime.Days(2)        // 2 calendar days (DST-safe)
twoWeeks    := gotime.Days(14)

There is intentionally no gotime.Day constant. 24 * gotime.Hour is exact 24 hours; gotime.Days(1) is a calendar day. Conflating them is the bug the type split exists to prevent.

DateTime exposes:

  • dt.Add(d Duration) (DateTime, error) — checked exact-time arithmetic; pass -d to move back
  • dt.AddPeriod(p Period) (LocalResolution, error) — checked calendar arithmetic with end-of-month clamping and explicit DST resolution
  • dt.Sub(other DateTime) Duration — difference between two DateTimes (mirrors time.Time.Sub)

Date.Add(p Period) (Date, error) handles checked calendar arithmetic. For differences, use the name that matches your intent: d.DaysUntil(other) for signed calendar-day counts, or d.PeriodUntil(other) for a checked greedy years/months/days period. Instant.Add(d Duration) Instant and Instant.Sub(other Instant) Duration round out the exact-time trio. The compiler enforces the distinction between Duration and Period — dt.Add(gotime.Months(1)) is a compile error.

Common Operations

zone := gotime.MustLoadZone("America/New_York")
tokyo := gotime.MustLoadZone("Asia/Tokyo")

now := gotime.Now()                  // Instant (UTC, no monotonic)
today := gotime.TodayIn(zone)        // Date — zone is required, never inferred
fromEpoch := gotime.UnixMillis(0)    // Instant from Unix epoch milliseconds

date, err := gotime.NewDate(2026, time.March, 27)
if err != nil {
	fmt.Println("date error:", err)
	return
}
clock, err := gotime.NewTime(9, 0, 0)
if err != nil {
	fmt.Println("time error:", err)
	return
}
dt, err := gotime.NewDateTime(date, clock, zone)
if err != nil {
	fmt.Println("datetime error:", err)
	return
}

local := gotime.NewLocalDateTime(date, clock)
resolution := local.Resolve(zone)
if resolution.Status == gotime.LocalAmbiguous {
	fmt.Println("choose between", resolution.Candidates)
}

exact, err := dt.Add(24 * gotime.Hour) // exact 24-hour span (Duration)
if err != nil {
	fmt.Println("exact arithmetic error:", err)
	return
}
calendarResolution, err := dt.AddPeriod(gotime.Days(1))
if err != nil {
	fmt.Println("calendar arithmetic error:", err)
	return
}
calendar, err := calendarResolution.Only()
if err != nil {
	fmt.Println("calendar resolution error:", err)
	return
}
shifted, err := dt.In(tokyo)
if err != nil {
	fmt.Println("zone projection error:", err)
	return
}

iv, err := gotime.NewInterval(dt.Instant(), exact.Instant())
if err != nil {
	fmt.Println("interval error:", err)
	return
}

// Selection composes with stdlib slices.MinFunc / MaxFunc + Compare.
import "slices"
earliest := slices.MinFunc(
	[]gotime.Instant{now, fromEpoch, dt.Instant()},
	gotime.Instant.Compare,
)

fmt.Println(now, today, exact, calendar, shifted, iv.Length(), earliest)

Timezones

Use LoadZone when you have a canonical IANA zone ID. Use ResolveZone only when you accept messy real-world input and need to normalize it. MustLoadZone is for var initialization with constant identifiers.

Call Use case
LoadZone("Asia/Tokyo") Strict IANA, returns error on miss
MustLoadZone("Asia/Tokyo") Constant zones in var blocks
ResolveZone("asia/tokyo") Forgiving case normalization
ResolveZone("Eastern Standard Time") Windows zone names
Zone{} Zero value, treated as UTC for stdlib interop
tokyo, err := gotime.LoadZone("Asia/Tokyo")
if err != nil {
	return err
}
name, offsetSeconds := gotime.Now().Std().In(tokyo.Location()).Zone()
fmt.Println(tokyo.ID(), name, offsetSeconds)
fmt.Println(len(gotime.Zones()), gotime.ZoneCatalogVersion())

ZoneCatalogVersion() identifies the tzdb version used to generate the Zones() catalog. It is catalog provenance, not a claim about the transition-rule data used by the Go runtime.

For interval rendering, project explicitly: start, end := iv.StdRange() returns two time.Time values you can pass to any range-formatter.

When a date plus clock time may cross a DST transition, resolve it explicitly:

local := gotime.NewLocalDateTime(date, clock)
resolution := local.Resolve(zone)
dt, err := resolution.Only() // ErrNonexistentTime or ErrDuplicateTime if not unique

Errors

go-time pairs sentinel errors (for errors.Is) with a typed *TimeError (for errors.As).

var te *gotime.TimeError
if errors.As(err, &te) {
	log.Printf("code=%s hint=%s", te.Code, te.Hint)
}

if errors.Is(err, gotime.ErrAmbiguousDate) {
	// control-flow branch
}

*TimeError unwraps its Err sentinel for control flow; Code remains JSON/log metadata. See SPECS/60-errors.md for the full code list.

JSON

All value objects implement stable JSON encoding via github.com/go-json-experiment/json. Decoding is strict: wrong kind, missing required fields, unknown fields, non-canonical values, and invalid identities are rejected.

b, err := json.Marshal(dt)
if err != nil {
	return err
}
fmt.Println(string(b))

Representative shapes:

{"kind":"datetime","instant":"2026-03-27T13:00:00Z","zone":"America/New_York"}
{"kind":"local_datetime","value":"2026-03-27T09:00:00"}
{"kind":"duration","iso":"PT1H30M"}
{"kind":"period","iso":"P1Y3M7D"}
{"kind":"zone","id":"America/New_York"}
  • Duration / Period carry only kind + iso — no components / years / months / days mirror fields. Duration sub-second ISO text uses decimal seconds, never scientific notation. Run Duration.Decompose() for clock slots; read Period.Years / .Months / .Days directly.
  • Zone JSON contains only {"kind":"zone","id":"…"}. A zero Zone encodes as {"kind":"zone","id":"UTC"}. Obtain time-dependent abbreviations and numeric offsets through instant.Std().In(zone.Location()).Zone(); they are never marshalled with zone identity.

API Reference

Development

task test
task lint
task fmt
task vet
task verify

For development guidelines and agent workflow, see AGENTS.md.

Contributing

Open an issue before large changes so the public API and specs can stay aligned.

License

This software is licensed under the MIT License. See the LICENSE file for full terms.

Documentation

Overview

Package gotime converts human time expressions into precise, computable value objects.

Parse entry points

Two layers, picked by what you know up front:

Current time

Now / NowIn / TodayIn return the current moment or calendar date. There is no zero-argument Today: a calendar date requires an explicit zone.

Panic convention

MustLoadZone panics only for invalid fixed IANA zone identifiers and is intended for package-level or test-time initialization.

Formatting is out of scope

Localization and display formatting are intentionally out of scope. Bridge gotime values to an external renderer through the Instant.Std, DateTime.Std, and Duration.Std adapters.

Example — typed helper (most common)

dt, err := gotime.ParseDateTime("2026-03-27T13:00:00",
    gotime.WithZone(gotime.MustLoadZone("Asia/Tokyo")),
)
if err != nil {
    return err
}
fmt.Println(dt.Std().Format("2006-01-02 15:04 MST"))

Example — natural language with locale

Natural date/datetime expressions require an explicit reference instant. Product code can pass Now at the boundary; tests usually pass a fixed Instant.

dt, err := gotime.ParseDateTime("tomorrow at 3pm",
    gotime.WithInputLocale(language.English),
    gotime.WithZone(gotime.MustLoadZone("America/New_York")),
    gotime.WithReference(gotime.Now()),
)
Example (JsonRoundTrip)

Value objects serialize to stable JSON schemas. All types round-trip through json.Marshal / json.Unmarshal without losing precision.

package main

import (
	"fmt"
	"time"

	"github.com/go-json-experiment/json"
	"github.com/go-json-experiment/json/jsontext"

	gotime "github.com/agentable/go-time"
)

func mustDate(year int, month time.Month, day int) gotime.Date {
	d, err := gotime.NewDate(year, month, day)
	if err != nil {
		panic(err)
	}
	return d
}

func mustTime(hour, minute, second int) gotime.Time {
	tm, err := gotime.NewTime(hour, minute, second)
	if err != nil {
		panic(err)
	}
	return tm
}

func mustDateTime(d gotime.Date, tm gotime.Time, z gotime.Zone) gotime.DateTime {
	dt, err := gotime.NewDateTime(d, tm, z)
	if err != nil {
		panic(err)
	}
	return dt
}

func main() {
	zone := gotime.MustLoadZone("Asia/Tokyo")
	dt := mustDateTime(
		mustDate(2026, time.March, 27),
		mustTime(13, 0, 0),
		zone,
	)

	b, _ := json.Marshal(dt, jsontext.WithIndent("  "))
	fmt.Println(string(b))

}
Output:
{
  "kind": "datetime",
  "instant": "2026-03-27T04:00:00Z",
  "zone": "Asia/Tokyo"
}

Index

Examples

Constants

View Source
const (
	Nanosecond  Duration = 1
	Microsecond          = 1000 * Nanosecond
	Millisecond          = 1000 * Microsecond
	Second               = 1000 * Millisecond
	Minute               = 60 * Second
	Hour                 = 60 * Minute
)

Duration unit constants mirror time.Duration's constants and support const arithmetic such as 5 * gotime.Minute.

Variables

View Source
var (
	ErrEmptyInput        = errors.New("gotime: empty input")
	ErrInvalidFormat     = errors.New("gotime: invalid format")
	ErrInvalidDate       = errors.New("gotime: invalid date")
	ErrInvalidTime       = errors.New("gotime: invalid time")
	ErrInvalidDuration   = errors.New("gotime: invalid duration")
	ErrInvalidPeriod     = errors.New("gotime: invalid period")
	ErrInvalidZone       = errors.New("gotime: invalid zone")
	ErrAmbiguousDate     = errors.New("gotime: ambiguous date")
	ErrNonexistentTime   = errors.New("gotime: nonexistent local time")
	ErrDuplicateTime     = errors.New("gotime: duplicate local time")
	ErrIntervalReversed  = errors.New("gotime: interval end before start")
	ErrIntervalsDisjoint = errors.New("gotime: intervals disjoint")
	ErrUnparseable       = errors.New("gotime: unparseable")
	ErrOverflow          = errors.New("gotime: overflow")
	ErrIncompatibleTypes = errors.New("gotime: incompatible types")
)

Sentinel errors — one per ErrorCode. Use these with errors.Is for control-flow matching. Use errors.As(&te) to extract Input/Hint/Message from a returned *TimeError.

View Source
var UTC = Zone{/* contains filtered or unexported fields */}

UTC is the UTC timezone.

Functions

func ZoneCatalogVersion added in v0.6.4

func ZoneCatalogVersion() string

ZoneCatalogVersion returns the IANA tzdb version used to generate Zones. It does not describe the transition-rule data used by time.LoadLocation.

func Zones

func Zones() []string

Zones returns a sorted copy of all known IANA timezone identifiers.

Types

type Date

type Date struct {
	// contains filtered or unexported fields
}

Date is a calendar date without time or timezone information.

func DateFromTime

func DateFromTime(t time.Time) (Date, error)

DateFromTime extracts the date from a time.Time using its location. It returns ErrOverflow when the projected year is outside 0000..9999.

func NewDate

func NewDate(year int, month time.Month, day int) (Date, error)

NewDate creates a Date from year, month, and day components.

func ParseDate

func ParseDate(input string, opts ...Option) (Date, error)

ParseDate parses input and returns a Date or an error.

func TodayIn

func TodayIn(z Zone) Date

TodayIn returns the current calendar date in zone z.

There is no zero-arg Today() helper: a calendar date is only meaningful relative to a zone. Applications that need the host's zone must discover or configure its IANA identifier and pass the resulting Zone explicitly.

func (Date) Add

func (d Date) Add(p Period) (Date, error)

Add returns a new Date advanced by p (calendar arithmetic). Month/year arithmetic applies end-of-month clamping. To move back, pass a negative Period literal or handle the error returned by Period.Negate. Add returns ErrInvalidDate for an invalid receiver and ErrOverflow when the result would leave the supported civil year domain.

func (Date) After

func (d Date) After(other Date) bool

After reports whether d is after other.

func (Date) Before

func (d Date) Before(other Date) bool

Before reports whether d is before other.

func (Date) Compare

func (d Date) Compare(other Date) int

Compare returns -1 if d is before other, 0 if equal, 1 if d is after other.

func (Date) Day

func (d Date) Day() int

Day returns the day component.

func (Date) DaysInMonth

func (d Date) DaysInMonth() int

DaysInMonth returns the number of days in the date's month.

func (Date) DaysUntil added in v0.6.4

func (d Date) DaysUntil(other Date) int

DaysUntil returns the signed number of calendar days from d to other.

func (Date) Equal

func (d Date) Equal(other Date) bool

Equal reports whether d and other represent the same calendar date.

func (Date) ISOWeek

func (d Date) ISOWeek() (year, week int)

ISOWeek returns the ISO 8601 year and week number.

func (Date) IsLeapYear

func (d Date) IsLeapYear() bool

IsLeapYear reports whether the date's year is a leap year.

func (Date) IsZero

func (d Date) IsZero() bool

IsZero reports whether d is the zero value.

func (Date) MarshalJSON

func (d Date) MarshalJSON() ([]byte, error)

MarshalJSON encodes d as {"kind":"date","value":"YYYY-MM-DD"}.

func (Date) Month

func (d Date) Month() time.Month

Month returns the month component.

func (Date) PeriodUntil added in v0.6.4

func (d Date) PeriodUntil(other Date) (Period, error)

PeriodUntil returns the signed greedy calendar period from d to other. It prefers years, then months, then days; use DaysUntil for exact day counts. It returns ErrInvalidDate if either endpoint is invalid.

func (Date) String

func (d Date) String() string

String returns the ISO 8601 date string "YYYY-MM-DD".

func (*Date) UnmarshalJSON

func (d *Date) UnmarshalJSON(b []byte) error

UnmarshalJSON decodes d from {"kind":"date","value":"YYYY-MM-DD"}.

func (Date) Weekday

func (d Date) Weekday() time.Weekday

Weekday returns the day of the week (Sunday = 0 through Saturday = 6).

func (Date) Year

func (d Date) Year() int

Year returns the year component.

func (Date) YearDay

func (d Date) YearDay() int

YearDay returns the day of the year (1–366).

type DateTime

type DateTime struct {
	// contains filtered or unexported fields
}

DateTime is a date and time in a specific timezone. It is the "human-readable" view of a moment.

func DateTimeFromTime

func DateTimeFromTime(t time.Time, z Zone) (DateTime, error)

DateTimeFromTime creates a DateTime from a stdlib time.Time and a Zone. It returns ErrOverflow when projection into z produces a year outside 0000..9999.

func NewDateTime

func NewDateTime(d Date, t Time, z Zone) (DateTime, error)

NewDateTime creates a DateTime from a Date, clock Time, and Zone.

func NowIn

func NowIn(z Zone) DateTime

NowIn returns the current moment projected into zone z as a DateTime.

func ParseDateTime

func ParseDateTime(input string, opts ...Option) (DateTime, error)

ParseDateTime parses input and returns a DateTime or an error.

func (DateTime) Add

func (dt DateTime) Add(d Duration) (DateTime, error)

Add returns a new DateTime advanced by d using exact (nanosecond) arithmetic. To move back, pass a negative Duration: dt.Add(-30 * gotime.Minute). It returns ErrOverflow when the result leaves the supported civil year domain.

func (DateTime) AddPeriod

func (dt DateTime) AddPeriod(p Period) (LocalResolution, error)

AddPeriod advances dt by p using calendar arithmetic and resolves the target local time in the same zone. Gaps and overlaps remain explicit in the result.

Example

Calendar math preserves wall-clock time across DST transitions and clamps end-of-month overflows. Exact math always adds precise durations.

package main

import (
	"fmt"
	"time"

	gotime "github.com/agentable/go-time"
)

func mustDate(year int, month time.Month, day int) gotime.Date {
	d, err := gotime.NewDate(year, month, day)
	if err != nil {
		panic(err)
	}
	return d
}

func mustTime(hour, minute, second int) gotime.Time {
	tm, err := gotime.NewTime(hour, minute, second)
	if err != nil {
		panic(err)
	}
	return tm
}

func mustDateTime(d gotime.Date, tm gotime.Time, z gotime.Zone) gotime.DateTime {
	dt, err := gotime.NewDateTime(d, tm, z)
	if err != nil {
		panic(err)
	}
	return dt
}

func main() {
	zone := gotime.MustLoadZone("America/New_York")

	jan31 := mustDateTime(
		mustDate(2026, time.January, 31),
		mustTime(12, 0, 0),
		zone,
	)
	febResolution, err := jan31.AddPeriod(gotime.Months(1))
	if err != nil {
		panic(err)
	}
	feb, err := febResolution.Only()
	if err != nil {
		panic(err)
	}
	fmt.Println("Jan 31 + 1 month:", feb.Date())

	exact, err := jan31.Add(48 * gotime.Hour)
	if err != nil {
		panic(err)
	}
	fmt.Println("Jan 31 + 48h:", exact.Date(), exact.Clock())

}
Output:
Jan 31 + 1 month: 2026-02-28
Jan 31 + 48h: 2026-02-02 12:00:00

func (DateTime) After

func (dt DateTime) After(other DateTime) bool

After reports whether dt is after other.

func (DateTime) Before

func (dt DateTime) Before(other DateTime) bool

Before reports whether dt is before other.

func (DateTime) Clock

func (dt DateTime) Clock() Time

Clock returns the clock time component of dt.

func (DateTime) Compare

func (dt DateTime) Compare(other DateTime) int

Compare returns -1 if dt < other, 0 if equal, 1 if dt > other.

func (DateTime) Date

func (dt DateTime) Date() Date

Date returns the calendar date component of dt.

func (DateTime) Equal

func (dt DateTime) Equal(other DateTime) bool

Equal reports whether dt and other represent the same absolute moment.

func (DateTime) In

func (dt DateTime) In(z Zone) (DateTime, error)

In converts dt to the same absolute moment expressed in zone z. It returns ErrOverflow when the projected year is outside 0000..9999.

func (DateTime) Instant

func (dt DateTime) Instant() Instant

Instant converts dt to an absolute UTC Instant.

func (DateTime) IsZero

func (dt DateTime) IsZero() bool

IsZero reports whether dt is the zero value.

func (DateTime) MarshalJSON

func (dt DateTime) MarshalJSON() ([]byte, error)

MarshalJSON encodes dt as {"kind":"datetime","instant":"<RFC3339Nano UTC>","zone":"<IANA id>"}.

func (DateTime) Std

func (dt DateTime) Std() time.Time

Std returns the underlying time.Time in dt's zone. Use this to bridge dt to any API expecting a stdlib time.Time (e.g. a formatter).

Example

Std returns the underlying time.Time. Use it to bridge a DateTime to any API expecting a stdlib time.Time — for example, a github.com/agentable/go-intl DateTimeFormat or any other formatter.

package main

import (
	"fmt"
	"time"

	gotime "github.com/agentable/go-time"
)

func mustDate(year int, month time.Month, day int) gotime.Date {
	d, err := gotime.NewDate(year, month, day)
	if err != nil {
		panic(err)
	}
	return d
}

func mustTime(hour, minute, second int) gotime.Time {
	tm, err := gotime.NewTime(hour, minute, second)
	if err != nil {
		panic(err)
	}
	return tm
}

func mustDateTime(d gotime.Date, tm gotime.Time, z gotime.Zone) gotime.DateTime {
	dt, err := gotime.NewDateTime(d, tm, z)
	if err != nil {
		panic(err)
	}
	return dt
}

func main() {
	zone := gotime.MustLoadZone("Asia/Tokyo")
	dt := mustDateTime(
		mustDate(2026, time.March, 27),
		mustTime(13, 0, 0),
		zone,
	)

	// Hand off to stdlib formatter.
	fmt.Println(dt.Std().Format("2006-01-02 15:04 MST"))

}
Output:
2026-03-27 13:00 JST

func (DateTime) String

func (dt DateTime) String() string

String returns the RFC3339Nano string with zone offset.

func (DateTime) Sub

func (dt DateTime) Sub(other DateTime) Duration

Sub returns the Duration from other to dt, mirroring time.Time.Sub. Use Add for arithmetic — there is no Sub(Duration) form.

func (*DateTime) UnmarshalJSON

func (dt *DateTime) UnmarshalJSON(b []byte) error

UnmarshalJSON decodes dt from {"kind":"datetime","instant":"<RFC3339Nano UTC>","zone":"<IANA id>"}.

func (DateTime) Zone

func (dt DateTime) Zone() Zone

Zone returns the timezone of dt.

type Duration

type Duration time.Duration

Duration represents an exact elapsed time with nanosecond precision. It is a typed alias for time.Duration so that const arithmetic works: callers write 5 * gotime.Minute exactly like stdlib.

Use Duration for exact-time math (timers, spans, sampling intervals). Use Period for calendar-aware math (months, years, "next Monday"). They are deliberately distinct types — the type system prevents mixing.

Day is intentionally NOT a Duration constant. A calendar day is a Period concept (Days(n)) and crosses DST boundaries safely; an exact 24-hour span is 24 * Hour. Conflating them is a frequent source of bugs.

func ParseDuration

func ParseDuration(input string, opts ...Option) (Duration, error)

ParseDuration parses input and returns a Duration or an error. Rejects ISO 8601 inputs with calendar components (P1Y, P5D) — use ParsePeriod for those.

func (Duration) Abs

func (d Duration) Abs() Duration

Abs returns the absolute value of d.

func (Duration) Decompose

func (d Duration) Decompose() DurationComponents

Decompose breaks d into renderable slots: Hours, Minutes, Seconds, Milliseconds, Microseconds, and Nanoseconds. Days/Months/Years are not extracted — Duration is exact wall-clock-free time, and elevating 24h to "1 day" presumes a calendar relationship the type does not carry. Callers can roll Hours into Days themselves if desired.

The sign of d is preserved across every non-zero slot; for example (-2*Hour - 30*Minute).Decompose() yields Hours=-2, Minutes=-30.

Example

Decompose breaks a Duration into the ECMA-402 DurationFormat slots (Hours/Minutes/Seconds/Milliseconds/Microseconds/Nanoseconds). The shape mirrors durationformat.Duration so a struct conversion is enough to hand it off — gotime never imports the formatter.

package main

import (
	"fmt"

	gotime "github.com/agentable/go-time"
)

func main() {
	d := 1*gotime.Hour + 30*gotime.Minute + 250*gotime.Millisecond
	c := d.Decompose()
	fmt.Printf("Hours=%d Minutes=%d Milliseconds=%d\n", c.Hours, c.Minutes, c.Milliseconds)

}
Output:
Hours=1 Minutes=30 Milliseconds=250

func (Duration) ISO8601

func (d Duration) ISO8601() string

ISO8601 returns the ISO 8601 duration string, e.g. "PT1H30M", "-PT30M", "PT0S". Sub-second precision uses fractional seconds.

func (Duration) InHours

func (d Duration) InHours() float64

InHours returns d in hours as a float64.

func (Duration) InMinutes

func (d Duration) InMinutes() float64

InMinutes returns d in minutes as a float64.

func (Duration) InSeconds

func (d Duration) InSeconds() float64

InSeconds returns d in seconds as a float64.

func (Duration) IsNegative

func (d Duration) IsNegative() bool

IsNegative reports whether d is negative.

func (Duration) IsZero

func (d Duration) IsZero() bool

IsZero reports whether d is zero.

func (Duration) MarshalJSON

func (d Duration) MarshalJSON() ([]byte, error)

MarshalJSON encodes d as {"kind":"duration","iso":"<ISO8601>"}. The ISO 8601 string is the single source of truth; callers that need structured slots can run d.Decompose() at the call site.

func (Duration) Milliseconds

func (d Duration) Milliseconds() int64

Milliseconds returns d in whole milliseconds.

func (Duration) Nanoseconds

func (d Duration) Nanoseconds() int64

Nanoseconds returns d in nanoseconds.

func (Duration) Std

func (d Duration) Std() time.Duration

Std returns d as a stdlib time.Duration.

func (Duration) String

func (d Duration) String() string

String returns d in the same format as time.Duration.String() ("1h30m0s", "500ms"). Identical to stdlib so round-trip with time.Duration is exact and callers can rely on a single well-known Stringer contract.

func (*Duration) UnmarshalJSON

func (d *Duration) UnmarshalJSON(b []byte) error

UnmarshalJSON decodes d from {"kind":"duration","iso":"<ISO8601>",...}.

type DurationComponents

type DurationComponents struct {
	// Hours is the whole-hour slot.
	Hours int64
	// Minutes is the remaining whole-minute slot.
	Minutes int64
	// Seconds is the remaining whole-second slot.
	Seconds int64
	// Milliseconds is the remaining whole-millisecond slot.
	Milliseconds int64
	// Microseconds is the remaining whole-microsecond slot.
	Microseconds int64
	// Nanoseconds is the remaining nanosecond slot.
	Nanoseconds int64
}

DurationComponents is a Duration broken into renderable clock slots: Hours through Nanoseconds. It mirrors the slot shape consumed by external duration formatters (e.g. an ECMA-402 Intl.DurationFormat implementation) so a caller can copy the fields directly:

c := d.Decompose()
out, _ := f.Format(durationformat.Duration{
    Hours:   c.Hours,
    Minutes: c.Minutes,
    // ...
})

gotime never imports any duration-formatter package — this type holds the shape, never the rendering. There are intentionally no calendar fields (Years/Months/Days): those belong to Period, whose fields are already exported, so widening them into a second struct would be duplication.

DurationComponents is a computed bridge, not a wire format. It is not marshalled by any value object; Duration's JSON contains only the ISO 8601 string. Run Decompose at the call site when you need structured slots.

type ErrorCode

type ErrorCode string

ErrorCode is the stable machine-readable JSON/wire category.

const (
	// CodeEmptyInput reports that the input string was empty.
	CodeEmptyInput ErrorCode = "EMPTY_INPUT"
	// CodeInvalidFormat reports a syntactically invalid input shape.
	CodeInvalidFormat ErrorCode = "INVALID_FORMAT"
	// CodeInvalidDate reports an invalid calendar date.
	CodeInvalidDate ErrorCode = "INVALID_DATE"
	// CodeInvalidTime reports an invalid clock time.
	CodeInvalidTime ErrorCode = "INVALID_TIME"
	// CodeInvalidDuration reports an invalid duration string.
	CodeInvalidDuration ErrorCode = "INVALID_DURATION"
	// CodeInvalidPeriod reports an invalid period string.
	CodeInvalidPeriod ErrorCode = "INVALID_PERIOD"
	// CodeInvalidZone reports an invalid or unresolvable timezone identifier.
	CodeInvalidZone ErrorCode = "INVALID_ZONE"
	// CodeAmbiguousDate reports multiple plausible date interpretations.
	CodeAmbiguousDate ErrorCode = "AMBIGUOUS_DATE"
	// CodeNonexistentTime reports a local time that falls in a DST gap.
	CodeNonexistentTime ErrorCode = "NONEXISTENT_LOCAL_TIME"
	// CodeDuplicateTime reports a local time that occurs twice during DST fall-back.
	CodeDuplicateTime ErrorCode = "DUPLICATE_LOCAL_TIME"
	// CodeIntervalReversed reports an Interval where end is before start.
	CodeIntervalReversed ErrorCode = "INTERVAL_END_BEFORE_START"
	// CodeIntervalsDisjoint reports an Interval Union where the inputs do not touch.
	CodeIntervalsDisjoint ErrorCode = "INTERVALS_DISJOINT"
	// CodeUnparseable reports input that no parser could interpret.
	CodeUnparseable ErrorCode = "UNPARSEABLE"
	// CodeOverflow reports arithmetic overflow.
	CodeOverflow ErrorCode = "OVERFLOW"
	// CodeIncompatibleTypes reports an operation on incompatible time types.
	CodeIncompatibleTypes ErrorCode = "INCOMPATIBLE_TYPES"
)

Error code constants — string-typed, prefixed Code*. These are the stable codes that appear in JSON, logs, and tooling. Pair each with a sentinel error below for use in errors.Is matching.

type Instant

type Instant struct {
	// contains filtered or unexported fields
}

Instant is an absolute UTC moment with nanosecond precision. It is the preferred type for storage, logging, and cross-system transfer.

func InstantFromTime

func InstantFromTime(t time.Time) Instant

InstantFromTime creates an Instant from a time.Time, forcing UTC.

func Now

func Now() Instant

Now returns the current moment as an Instant (UTC, no monotonic reading).

func ParseInstant

func ParseInstant(input string, opts ...Option) (Instant, error)

ParseInstant parses input and returns an Instant or an error.

func UnixMillis

func UnixMillis(ms int64) Instant

UnixMillis creates an Instant from a Unix timestamp in milliseconds.

func UnixNanos

func UnixNanos(ns int64) Instant

UnixNanos creates an Instant from a Unix timestamp in nanoseconds.

func UnixSeconds

func UnixSeconds(s int64) Instant

UnixSeconds creates an Instant from a Unix timestamp in seconds.

func (Instant) Add

func (i Instant) Add(d Duration) Instant

Add returns a new Instant advanced by d.

func (Instant) After

func (i Instant) After(other Instant) bool

After reports whether i is after other.

func (Instant) Before

func (i Instant) Before(other Instant) bool

Before reports whether i is before other.

func (Instant) Compare

func (i Instant) Compare(other Instant) int

Compare returns -1 if i < other, 0 if i == other, 1 if i > other. Use with slices.MinFunc / slices.MaxFunc / cmp.Or for selection and clamping:

earliest := slices.MinFunc(times, gotime.Instant.Compare)

func (Instant) Equal

func (i Instant) Equal(other Instant) bool

Equal reports whether i and other represent the same moment.

func (Instant) In

func (i Instant) In(z Zone) (DateTime, error)

In projects the Instant into a timezone, returning a DateTime. It returns ErrOverflow when the projected year is outside 0000..9999.

func (Instant) IsZero

func (i Instant) IsZero() bool

IsZero reports whether i is the zero value.

func (Instant) MarshalJSON

func (i Instant) MarshalJSON() ([]byte, error)

MarshalJSON encodes i as {"kind":"instant","iso":"<RFC3339Nano UTC>"}.

func (Instant) Std

func (i Instant) Std() time.Time

Std returns the underlying time.Time in UTC.

func (Instant) String

func (i Instant) String() string

String returns the RFC3339Nano representation in UTC.

func (Instant) Sub

func (i Instant) Sub(other Instant) Duration

Sub returns the Duration from other to i.

func (Instant) UnixMilli

func (i Instant) UnixMilli() int64

UnixMilli returns the instant as Unix time in milliseconds.

func (Instant) UnixNano

func (i Instant) UnixNano() int64

UnixNano returns the instant as Unix time in nanoseconds.

func (*Instant) UnmarshalJSON

func (i *Instant) UnmarshalJSON(b []byte) error

UnmarshalJSON decodes i from {"kind":"instant","iso":"<RFC3339Nano>",...}.

type Interval

type Interval struct {
	// contains filtered or unexported fields
}

Interval is a half-open time range [start, end) bounded by two Instants. Interval is zone-free — projection zone for display is the caller's concern; bridge via iv.StdRange() and format outside this package. Arithmetic is UTC-based on the underlying Instants.

func NewInterval

func NewInterval(start, end Instant) (Interval, error)

NewInterval creates an Interval. Returns ErrIntervalReversed if end < start.

func NewIntervalEndingAt added in v0.6.4

func NewIntervalEndingAt(end Instant, length Duration) (Interval, error)

NewIntervalEndingAt creates an Interval from an end Instant and non-negative length.

func NewIntervalStartingAt added in v0.6.4

func NewIntervalStartingAt(start Instant, length Duration) (Interval, error)

NewIntervalStartingAt creates an Interval from a start Instant and non-negative length.

func ParseInterval

func ParseInterval(input string, opts ...Option) (Interval, error)

ParseInterval parses input and returns an Interval or an error.

func (Interval) Adjacent

func (iv Interval) Adjacent(other Interval) bool

Adjacent reports whether iv and other share exactly one boundary with no overlap and no gap. For half-open intervals, [a, b) and [b, c) are adjacent.

func (Interval) Contains

func (iv Interval) Contains(i Instant) bool

Contains reports whether i is within the half-open interval [start, end).

func (Interval) End

func (iv Interval) End() Instant

End returns the end instant (exclusive).

func (Interval) Expand

func (iv Interval) Expand(before, after Duration) (Interval, error)

Expand returns a new Interval with start moved back by before and end moved forward by after.

func (Interval) Intersect

func (iv Interval) Intersect(other Interval) (Interval, bool)

Intersect returns the overlapping portion of iv and other. Returns (zero, false) if they are disjoint.

func (Interval) IsZero

func (iv Interval) IsZero() bool

IsZero reports whether iv is the zero value.

func (Interval) Length

func (iv Interval) Length() Duration

Length returns the duration of the interval.

func (Interval) MarshalJSON

func (iv Interval) MarshalJSON() ([]byte, error)

MarshalJSON encodes iv as {"kind":"interval","start":"<RFC3339Nano>","end":"<RFC3339Nano>"}.

func (Interval) Overlaps

func (iv Interval) Overlaps(other Interval) bool

Overlaps reports whether iv and other share any moment. Half-open intervals [a, b) and [b, c) do NOT overlap — they are adjacent.

func (Interval) Shift

func (iv Interval) Shift(d Duration) Interval

Shift returns a new Interval with start and end each advanced by d.

func (Interval) Start

func (iv Interval) Start() Instant

Start returns the start instant (inclusive).

func (Interval) StdRange

func (iv Interval) StdRange() (start, end time.Time)

StdRange returns the underlying start and end as stdlib time.Time values in UTC. Use this to bridge an Interval to any API expecting (time.Time, time.Time).

func (Interval) String

func (iv Interval) String() string

String returns the ISO 8601 interval notation "<start>/<end>".

func (Interval) Union

func (iv Interval) Union(other Interval) (Interval, error)

Union returns the smallest interval containing both iv and other. Adjacent intervals ([a, b) and [b, c)) can be unioned. Returns an error if the intervals are disjoint with a gap between them.

func (*Interval) UnmarshalJSON

func (iv *Interval) UnmarshalJSON(b []byte) error

UnmarshalJSON decodes iv from {"kind":"interval","start":"<RFC3339Nano>","end":"<RFC3339Nano>"}.

type Kind

type Kind string

Kind identifies the type of the parsed value.

const (
	// KindInstant identifies an absolute UTC timestamp.
	KindInstant Kind = "instant"
	// KindDateTime identifies a zoned local date-time.
	KindDateTime Kind = "datetime"
	// KindLocalDateTime identifies a local date-time without zone or offset.
	KindLocalDateTime Kind = "local_datetime"
	// KindDate identifies a calendar date.
	KindDate Kind = "date"
	// KindTime identifies a clock time.
	KindTime Kind = "time"
	// KindDuration identifies an exact-time duration.
	KindDuration Kind = "duration"
	// KindPeriod identifies a calendar period (years/months/days).
	KindPeriod Kind = "period"
	// KindInterval identifies a half-open time interval.
	KindInterval Kind = "interval"
)

type LocalDateTime added in v0.6.4

type LocalDateTime struct {
	// Date is the calendar date component.
	Date Date
	// Time is the clock time component.
	Time Time
}

LocalDateTime is a calendar date plus a clock time, without a zone. Resolving it in a Zone may produce zero, one, or multiple DateTime candidates.

func NewLocalDateTime added in v0.6.4

func NewLocalDateTime(d Date, t Time) LocalDateTime

NewLocalDateTime combines a Date and Time without resolving them into a zone.

func ParseLocalDateTime added in v0.6.4

func ParseLocalDateTime(input string, opts ...Option) (LocalDateTime, error)

ParseLocalDateTime parses input and returns a LocalDateTime or an error.

func (LocalDateTime) MarshalJSON added in v0.6.4

func (ldt LocalDateTime) MarshalJSON() ([]byte, error)

MarshalJSON encodes ldt as {"kind":"local_datetime","value":"YYYY-MM-DDTHH:MM:SS"}.

func (LocalDateTime) Resolve added in v0.6.4

func (ldt LocalDateTime) Resolve(z Zone) LocalResolution

Resolve projects ldt into z and classifies DST gaps and overlaps explicitly.

func (LocalDateTime) String added in v0.6.4

func (ldt LocalDateTime) String() string

String returns the ISO 8601 local date-time string "YYYY-MM-DDTHH:MM:SS[.fraction]".

func (*LocalDateTime) UnmarshalJSON added in v0.6.4

func (ldt *LocalDateTime) UnmarshalJSON(b []byte) error

UnmarshalJSON decodes ldt from {"kind":"local_datetime","value":"YYYY-MM-DDTHH:MM:SS[.fraction]"}.

type LocalResolution added in v0.6.4

type LocalResolution struct {
	// Status classifies the local time projection.
	Status LocalResolutionStatus
	// Zone is the zone used for resolution. A zero Zone resolves as UTC.
	Zone Zone
	// Local is the unresolved local date-time.
	Local LocalDateTime
	// Candidates holds the matching DateTime values in chronological order.
	Candidates []DateTime
}

LocalResolution holds the result of resolving a LocalDateTime in a Zone.

func (LocalResolution) Only added in v0.6.4

func (r LocalResolution) Only() (DateTime, error)

Only returns the single resolved DateTime, or an error if resolution did not produce exactly one candidate.

type LocalResolutionStatus added in v0.6.4

type LocalResolutionStatus string

LocalResolutionStatus describes how a LocalDateTime maps into a Zone.

const (
	// LocalInvalid means the local date or clock time is invalid.
	LocalInvalid LocalResolutionStatus = "invalid"
	// LocalResolved means the local time maps to exactly one DateTime.
	LocalResolved LocalResolutionStatus = "resolved"
	// LocalNonexistent means the local time falls in a DST gap.
	LocalNonexistent LocalResolutionStatus = "nonexistent"
	// LocalAmbiguous means the local time maps to multiple DateTime candidates.
	LocalAmbiguous LocalResolutionStatus = "ambiguous"
)

type Option

type Option func(*config)

Option is a functional option for Parse.

func WithInputLocale

func WithInputLocale(tag language.Tag) Option

WithInputLocale sets the language used to interpret natural-language input. Standard formats (RFC 3339, ISO 8601) are language-independent and ignore it.

The argument is a golang.org/x/text/language Tag — the de facto standard representation of BCP-47 language tags in Go. Construct it via language.MustParse("zh-Hans"), language.Chinese, or similar.

Only the language identity is consumed; Unicode -u- extensions (hour cycle, calendar, numbering system) are ignored because they describe display behavior, not parsing behavior.

Chinese requires script subtags: language.SimplifiedChinese (zh-Hans) or language.TraditionalChinese (zh-Hant). The bare language.Chinese ("zh") tag does not activate the Chinese natural-language parser.

For slash dates (e.g. "04/05/2026"), en-US selects month-first while en-GB and en-AU select day-first. Unsupported tags, including bare en and en-CA, use validity inference: one valid interpretation resolves, two are Ambiguous, and zero are Invalid. Unicode -u- extensions do not change the supported locale's order.

Example

WithInputLocale enables natural-language parsing in the given language. Use a golang.org/x/text/language Tag — the standard Go BCP-47 type — to avoid binding gotime to any specific i18n library.

package main

import (
	"fmt"
	"time"

	"golang.org/x/text/language"

	gotime "github.com/agentable/go-time"
)

func main() {
	ref := gotime.InstantFromTime(time.Date(2026, 3, 30, 12, 0, 0, 0, time.UTC))
	r := gotime.Parse("明天",
		gotime.WithInputLocale(language.SimplifiedChinese),
		gotime.WithZone(gotime.MustLoadZone("Asia/Shanghai")),
		gotime.WithReference(ref),
	)
	if r.Status != gotime.StatusResolved {
		return
	}
	d, _ := r.Date()
	fmt.Println(d)

}
Output:
2026-03-31

func WithReference

func WithReference(t Instant) Option

WithReference sets the reference instant for relative-time expressions. Relative natural date and datetime expressions also require WithZone.

func WithZone

func WithZone(zone Zone) Option

WithZone sets the timezone used when the input has no explicit offset. Relative natural date and datetime expressions require it so WithReference's instant can be interpreted in an explicit calendar frame.

type ParseResult

type ParseResult struct {
	// Status reports whether parsing resolved, remained ambiguous, or failed.
	Status Status
	// Kind identifies the semantic type of the parsed value when Status is Resolved or Ambiguous.
	Kind Kind
	// Input is the original input string.
	Input string
	// Zone is the zone applied when the input did not carry its own zone or offset.
	Zone Zone
	// Reference is the reference instant used for relative expressions.
	Reference Instant
	// HasZone reports whether the input explicitly included timezone or offset information.
	HasZone bool
	// Warnings describes lossy assumptions made while parsing.
	Warnings []Warning
	// Candidates holds the alternative interpretations when Status is Ambiguous.
	// Each candidate is itself a StatusResolved ParseResult.
	Candidates []ParseResult
	// Error describes the failure when Status is Invalid.
	Error *TimeError
	// contains filtered or unexported fields
}

ParseResult holds the outcome of Parse. Check Status before calling the typed accessors (DateTime, Date, …).

func Parse

func Parse(input string, opts ...Option) ParseResult

Parse is the inspection / dispatch entry point. It accepts any supported input (ISO 8601, RFC 3339, natural language, ranges) and returns a ParseResult describing the outcome. It never returns a Go error — semantic states (ambiguous / invalid) live on ParseResult.Status.

When you already know which concrete type you expect, prefer the typed helpers (ParseInstant, ParseDateTime, ParseLocalDateTime, ParseDate, ParseTime, ParseDuration, ParsePeriod, ParseInterval) — they return (T, error) directly by narrowing the same shared parse result.

Reach for Parse when you need any of:

Example

Parse accepts ISO 8601, RFC 3339, and natural language. The three-status result model (Resolved / Ambiguous / Invalid) lets callers handle any input without panicking.

package main

import (
	"fmt"

	gotime "github.com/agentable/go-time"
)

func main() {
	r := gotime.Parse("2026-03-27T13:00:00+09:00")
	if r.Status != gotime.StatusResolved {
		fmt.Println("unexpected status:", r.Status)
		return
	}
	i, _ := r.Instant()
	fmt.Println(i)

}
Output:
2026-03-27T04:00:00Z

func (ParseResult) Date

func (r ParseResult) Date() (Date, bool)

Date returns the parsed Date. ok is false unless Status is Resolved and Kind == KindDate.

func (ParseResult) DateTime

func (r ParseResult) DateTime() (DateTime, bool)

DateTime returns the parsed DateTime. ok is false unless Status is Resolved and Kind == KindDateTime.

func (ParseResult) Duration

func (r ParseResult) Duration() (Duration, bool)

Duration returns the parsed Duration. ok is false unless Status is Resolved and Kind == KindDuration.

func (ParseResult) Instant

func (r ParseResult) Instant() (Instant, bool)

Instant returns the parsed Instant. ok is false unless Status is Resolved and Kind == KindInstant.

func (ParseResult) Interval

func (r ParseResult) Interval() (Interval, bool)

Interval returns the parsed Interval. ok is false unless Status is Resolved and Kind == KindInterval.

func (ParseResult) LocalDateTime added in v0.6.4

func (r ParseResult) LocalDateTime() (LocalDateTime, bool)

LocalDateTime returns the parsed LocalDateTime. ok is false unless Status is Resolved and Kind == KindLocalDateTime.

func (ParseResult) MarshalJSON

func (r ParseResult) MarshalJSON() ([]byte, error)

MarshalJSON serializes r to the stable JSON schema defined in SPECS/20-parsing.md.

func (ParseResult) Period

func (r ParseResult) Period() (Period, bool)

Period returns the parsed Period. ok is false unless Status is Resolved and Kind == KindPeriod.

func (ParseResult) Time

func (r ParseResult) Time() (Time, bool)

Time returns the parsed Time. ok is false unless Status is Resolved and Kind == KindTime.

type Period

type Period struct {
	// Years is the calendar-year offset.
	Years int32 `json:"years,omitzero"`
	// Months is the calendar-month offset.
	Months int32 `json:"months,omitzero"`
	// Days is the calendar-day offset.
	Days int32 `json:"days,omitzero"`
}

Period represents a calendar offset in years, months, and days. It is the calendar-aware counterpart to Duration: Period operations preserve wall-clock time across DST transitions and apply end-of-month clamping for month/year arithmetic.

Use Period for "next month", "in 7 days", recurring schedules. Use Duration for exact-time math.

Fields are exported so callers can construct via struct literal:

p := gotime.Period{Years: 1, Months: 3, Days: 7}

func Days

func Days(n int32) Period

Days returns Period{Days: n}. These are calendar days (not 24-hour spans); they preserve wall-clock time across DST boundaries. For exact 24-hour math write 24 * gotime.Hour.

func Months

func Months(n int32) Period

Months returns Period{Months: n}. Sugar for the struct literal.

func NewPeriod added in v0.6.4

func NewPeriod(years, months, days int32) Period

NewPeriod creates a Period from calendar year, month, and day components.

func ParsePeriod

func ParsePeriod(input string, opts ...Option) (Period, error)

ParsePeriod parses input and returns a Period or an error. Rejects ISO 8601 inputs with clock components (PT1H) — use ParseDuration for those.

func Years

func Years(n int32) Period

Years returns Period{Years: n}. Sugar for the struct literal.

func (Period) Abs

func (p Period) Abs() (Period, error)

Abs returns p with all fields made non-negative, or ErrOverflow when a component is math.MinInt32.

func (Period) Add

func (p Period) Add(other Period) (Period, error)

Add returns p + other componentwise, or ErrOverflow when a component exceeds int32.

func (Period) ISO8601

func (p Period) ISO8601() string

ISO8601 returns the ISO 8601 representation of p, e.g. "P1Y3M7D". A zero Period returns "P0D". Negative components produce a leading "-".

func (Period) IsNegative

func (p Period) IsNegative() bool

IsNegative reports whether any field of p is negative. A Period with mixed signs is not normalized — use Negate to flip all fields.

func (Period) IsZero

func (p Period) IsZero() bool

IsZero reports whether p has no calendar offset.

func (Period) MarshalJSON

func (p Period) MarshalJSON() ([]byte, error)

MarshalJSON encodes p as {"kind":"period","iso":"<ISO8601>"}. The ISO 8601 string is the single source of truth; callers that need structured slots can read p.Years / p.Months / p.Days directly.

func (Period) Negate

func (p Period) Negate() (Period, error)

Negate returns -p (all fields flipped), or ErrOverflow when a component is math.MinInt32.

func (Period) String

func (p Period) String() string

String returns a compact human form, e.g. "1y3mo7d", "-2mo".

func (Period) Sub

func (p Period) Sub(other Period) (Period, error)

Sub returns p - other componentwise, or ErrOverflow when a component exceeds int32.

func (*Period) UnmarshalJSON

func (p *Period) UnmarshalJSON(b []byte) error

UnmarshalJSON decodes p from {"kind":"period","iso":"<ISO8601>",...}.

type Status

type Status string

Status is the outcome of a Parse call.

const (
	// StatusResolved means Parse found exactly one interpretation.
	StatusResolved Status = "resolved"
	// StatusAmbiguous means Parse found multiple plausible interpretations.
	StatusAmbiguous Status = "ambiguous"
	// StatusInvalid means Parse could not parse the input.
	StatusInvalid Status = "invalid"
)

type Time

type Time struct {
	// contains filtered or unexported fields
}

Time represents a clock time without a date or timezone.

func NewTime

func NewTime(hour, minute, second int) (Time, error)

NewTime creates a Time from hour, minute, and second.

func NewTimeNanos

func NewTimeNanos(hour, minute, second, nanosecond int) (Time, error)

NewTimeNanos creates a Time with sub-second precision.

func ParseTime

func ParseTime(input string, opts ...Option) (Time, error)

ParseTime parses input and returns a Time or an error.

func TimeFromTime

func TimeFromTime(t time.Time) Time

TimeFromTime extracts the clock time from a time.Time.

func (Time) After

func (t Time) After(other Time) bool

After reports whether t is after other.

func (Time) Before

func (t Time) Before(other Time) bool

Before reports whether t is before other.

func (Time) Equal

func (t Time) Equal(other Time) bool

Equal reports whether t and other represent the same clock time.

func (Time) Hour

func (t Time) Hour() int

Hour returns the hour component (0-23).

func (Time) IsZero

func (t Time) IsZero() bool

IsZero reports whether t is the zero value (midnight, 00:00:00.000000000).

func (Time) MarshalJSON

func (t Time) MarshalJSON() ([]byte, error)

MarshalJSON encodes t as {"kind":"time","value":"HH:MM:SS[.fraction]"}.

func (Time) Minute

func (t Time) Minute() int

Minute returns the minute component (0-59).

func (Time) Nanosecond

func (t Time) Nanosecond() int

Nanosecond returns the nanosecond component (0-999999999).

func (Time) Second

func (t Time) Second() int

Second returns the second component (0-59).

func (Time) String

func (t Time) String() string

String returns the canonical clock time as "HH:MM:SS[.fraction]".

func (*Time) UnmarshalJSON

func (t *Time) UnmarshalJSON(b []byte) error

UnmarshalJSON decodes t from {"kind":"time","value":"HH:MM:SS[.nnnnnnnnn]"}.

type TimeError

type TimeError struct {
	// Code is the stable machine-readable error code.
	Code ErrorCode `json:"code"`
	// Message is the human-readable error summary.
	Message string `json:"message,omitzero"`
	// Input is the original input that triggered the error.
	Input string `json:"input,omitzero"`
	// Hint explains how to correct the input.
	Hint string `json:"hint,omitzero"`
	// Err is the sentinel identity for errors.Is.
	Err error `json:"-"`
}

TimeError is the structured error type for every failure in this package. It composes with the standard library: errors.Is matches the Err sentinel, while errors.As extracts Input, Hint, Message, and Code for inspection.

func (*TimeError) Error

func (e *TimeError) Error() string

Error implements the error interface.

func (*TimeError) Unwrap

func (e *TimeError) Unwrap() error

Unwrap returns the sentinel identity for errors.Is.

type Warning

type Warning struct {
	// Code classifies the warning.
	Code WarningCode `json:"code"`
	// Message is a short human-readable description.
	Message string `json:"message"`
	// Hint suggests how to silence the warning.
	Hint string `json:"hint,omitempty"`
}

Warning is a non-fatal advisory about a lossy assumption made during parsing.

type WarningCode

type WarningCode string

WarningCode classifies a parse warning. Warnings are non-fatal lossy assumptions; they never change Status.

const (
	// WarnAssumedZone reports that a default Zone was applied because the
	// input did not carry an explicit zone or offset.
	WarnAssumedZone WarningCode = "assumed_zone"
	// WarnTruncatedPrecision reports that input precision exceeded what the
	// target type can represent and was truncated.
	WarnTruncatedPrecision WarningCode = "truncated_precision"
	// WarnInferredCalendar reports that a date/calendar interpretation was
	// inferred from locale or candidate ordering.
	WarnInferredCalendar WarningCode = "inferred_calendar"
	// WarnDuplicateTime reports that a DST fall-back local time candidate is
	// one of multiple valid instants.
	WarnDuplicateTime WarningCode = "duplicate_time"
)

type Zone

type Zone struct {
	// contains filtered or unexported fields
}

Zone represents an IANA timezone identity.

func LoadZone

func LoadZone(id string) (Zone, error)

LoadZone loads a Zone by IANA timezone id.

func MustLoadZone

func MustLoadZone(id string) Zone

MustLoadZone is like LoadZone but panics if id is invalid. It is intended for use with fixed IANA identifiers in variable initializations.

func ResolveZone

func ResolveZone(id string) (Zone, error)

ResolveZone resolves a timezone identifier by trying exact IANA names, case-insensitive IANA matches, and Windows timezone names. Legacy IANA aliases such as "US/Eastern" are handled by Go's time.LoadLocation.

func (Zone) Equal

func (z Zone) Equal(other Zone) bool

Equal reports whether two zones have the same identifier.

func (Zone) ID

func (z Zone) ID() string

ID returns the IANA timezone identifier.

func (Zone) IsZero

func (z Zone) IsZero() bool

IsZero reports whether z is the Go zero value.

func (Zone) Location

func (z Zone) Location() *time.Location

Location returns the underlying *time.Location for stdlib interop. The zero Zone falls back to time.UTC.

func (Zone) MarshalJSON

func (z Zone) MarshalJSON() ([]byte, error)

MarshalJSON encodes z as {"kind":"zone","id":"<IANA id>"}. The output is deterministic and never depends on time.Now().

func (Zone) String

func (z Zone) String() string

String returns the zone identifier.

func (*Zone) UnmarshalJSON

func (z *Zone) UnmarshalJSON(b []byte) error

UnmarshalJSON decodes z from {"kind":"zone","id":"<IANA id>",...}.

Directories

Path Synopsis
internal
natural
Package natural implements the internal natural-language parsing layer for gotime.
Package natural implements the internal natural-language parsing layer for gotime.
zone
Package zone provides internal IANA timezone data and DST projection utilities.
Package zone provides internal IANA timezone data and DST projection utilities.
zone/geniana command
The geniana command generates the internal IANA timezone catalog.
The geniana command generates the internal IANA timezone catalog.
zone/genwindows command
The genwindows command generates the Windows-to-IANA mapping from a pinned Unicode CLDR windowsZones.xml release.
The genwindows command generates the Windows-to-IANA mapping from a pinned Unicode CLDR windowsZones.xml release.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL