gotime

package module
v0.6.3 Latest Latest
Warning

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

Go to latest
Published: Jun 4, 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, Date, Time, Duration, Period, Interval, Zone
  • Two parse entry points: Parse returns a tagged-sum ParseResult; seven ParseInstant / ParseDateTime / … 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, DST detection, floating-time detection (ParseResult.HasZone)
  • Calendar-safe computation: Duration.Add is exact; AddPeriod is calendar-aware with end-of-month clamping and DST-stable wall-clock preservation
  • Stable JSON schemas: Each value object has a single-source-of-truth wire format that never depends on time.Now() or runtime locale
  • Zero i18n footprint: This package ships no locale data, no formatter types — display is the caller's choice (stdlib time.Format, github.com/agentable/go-intl, logging libraries, …)

Installation

go get github.com/agentable/go-time

Requires Go 1.26.3 or newer.

Quick Start

Most code knows what type it expects — reach for the typed helpers (ParseDateTime, 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+09:00")
	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; relative expressions default to time.Now(), so most callers do not need WithReference:

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

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

Pin the 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+09: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. The fastest way to dispatch is Value() + a Go type switch:

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

switch v := r.Value().(type) {
case gotime.DateTime:
	fmt.Println(v)
case gotime.Date:
	fmt.Println(v)
case gotime.Duration:
	fmt.Println(v)
case nil:
	// Status is StatusAmbiguous or StatusInvalid.
	switch r.Status {
	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 (r.DateTime(), r.Date(), …) are still available for callers that use Parse for metadata but already know the target Kind statically.

Supported inputs
Input kind Examples Routes to
ISO datetime 2026-03-27T13:00:00+09:00, 20260327T130000+0900 KindDateTime
RFC 3339 UTC 2026-03-27T04:00:00Z KindInstant
Local datetime 2026-03-27T13:00:00, 20260327T130000 KindDateTime, HasZone=false
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. Also disambiguates slash-date order (e.g. 04/05/2026 is May 4 in en-GB, April 5 in en-US).
WithZone(zone) Supply the typed zone applied when input has no explicit offset
WithReference(instant) Anchor relative expressions such as tomorrow or in 2 hours

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

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.

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
A localized formatter (e.g. go-intl)
import (
    "github.com/agentable/go-intl/datetimeformat"
    "github.com/agentable/go-intl/locale"
)

f, _ := datetimeformat.New(locale.MustParse("zh-Hans"), datetimeformat.Options{
    DateStyle: datetimeformat.LongDateTimeStyle,
    TimeStyle: datetimeformat.ShortDateTimeStyle,
    TimeZone:  "Asia/Tokyo",
})
fmt.Println(f.Format(dt.Std()))
Duration → clock slots, Period → exported fields
import "github.com/agentable/go-intl/durationformat"

// Duration: modular arithmetic is non-trivial, so a helper does it for you.
c := d.Decompose() // DurationComponents{Hours, Minutes, Seconds, …}

f, _ := durationformat.New(loc, durationformat.Options{Style: durationformat.LongStyle})
s, _ := f.Format(durationformat.Duration{
    Hours:        c.Hours,
    Minutes:      c.Minutes,
    Seconds:      c.Seconds,
    Milliseconds: c.Milliseconds,
})

// Period: fields are already exported — no Decompose, no parallel struct.
s2, _ := f.Format(durationformat.Duration{
    Years:  int64(p.Years),
    Months: int64(p.Months),
    Days:   int64(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)
Date.Std(z Zone) time.Time at 00:00 in z
Time.Std(on Date, z Zone) time.Time
Interval.StdRange() (start, end time.Time)
Duration.Std() time.Duration
Duration.Decompose() DurationComponents (clock slots)
Period.Years / .Months / .Days int32 fields (no Decompose)

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

Core Types

Type Meaning
Instant Absolute UTC moment
DateTime Zoned local date and time
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 or resolved fixed offset
DurationComponents Clock-slot decomposition of Duration (Hours…Nanoseconds) — bridge to external duration formatters

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) — exact-time arithmetic; pass -d to move back
  • dt.AddPeriod(p Period) — calendar arithmetic with end-of-month clamping; pass p.Negate() to move back
  • dt.Sub(other DateTime) Duration — difference between two DateTimes (mirrors time.Time.Sub)

Date.Add(p Period) Date and Date.Sub(other Date) Period follow the same convention. Instant.Add(d Duration) Instant and Instant.Sub(other Instant) Duration round out the 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
}

exact := dt.Add(24 * gotime.Hour)      // exact 24-hour span (Duration)
calendar := dt.AddPeriod(gotime.Days(1)) // 1 calendar day, DST-safe (Period)
shifted := dt.In(tokyo)

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
ResolveZone("UTC+8") Fixed-offset input
Zone{} Zero value, treated as UTC for stdlib interop
tokyo, err := gotime.LoadZone("Asia/Tokyo")
if err != nil {
	return err
}
fmt.Println(tokyo.ID(), tokyo.OffsetAt(gotime.Now()))
fmt.Println(len(gotime.Zones()))

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

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. Each schema has a single source of truth: derived fields (e.g. Duration.Decompose()) are computed at the call site, not duplicated on the wire.

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

Representative shapes:

{"kind":"datetime","value":"2026-03-27T09:00:00-04:00","zone":"America/New_York","calendar":"iso8601"}
{"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. Run Duration.Decompose() for clock slots; read Period.Years / .Months / .Days directly.
  • Zone JSON contains only {"kind":"zone","id":"…"}. Time-dependent display data (offset, DST, abbreviation) lives in Zone.Snapshot(at Instant) — never on the marshalled value, so the same Zone encodes byte-identical regardless of when you call Marshal.

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 a formatter (for example github.com/agentable/go-intl) via the Instant.Std, DateTime.Std, and Duration.Std adapters.

Example — typed helper (most common)

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

Example — natural language with locale

Relative expressions ("tomorrow", "in 2 hours") default to time.Now(); pass WithReference only when you need a fixed reference for testing.

dt, err := gotime.ParseDateTime("tomorrow at 3pm",
    gotime.WithInputLocale(language.English),
    gotime.WithZone(gotime.MustLoadZone("America/New_York")),
)
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",
  "value": "2026-03-27T13:00:00+09:00",
  "zone": "Asia/Tokyo",
  "calendar": "iso8601"
}

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")
	ErrAmbiguousTime     = errors.New("gotime: ambiguous time")
	ErrAmbiguousZone     = errors.New("gotime: ambiguous zone")
	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.

Functions

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

DateFromTime extracts the date from a time.Time using its location.

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, and inheriting time.Local from the runtime environment produces date drift across container/CI/server boundaries. Callers that want process-local behavior must spell it out: TodayIn(Local).

func (Date) Add

func (d Date) Add(p Period) Date

Add returns a new Date advanced by p (calendar arithmetic). Month/year arithmetic applies end-of-month clamping. To move back, pass a negated Period: d.Add(gotime.Days(3).Negate()).

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) 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","calendar":"iso8601"}.

func (Date) Month

func (d Date) Month() time.Month

Month returns the month component.

func (Date) Std

func (d Date) Std(z Zone) time.Time

Std returns the Date as a time.Time at 00:00:00 in zone z. Use this to bridge a Date to any API expecting a stdlib time.Time.

func (Date) String

func (d Date) String() string

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

func (Date) Sub

func (d Date) Sub(other Date) Period

Sub returns the calendar difference from other to d as a Period (with Years, Months, Days set; signed by direction). Mirrors time.Time.Sub. Use Add for arithmetic — there is no Sub(Period) form.

func (*Date) UnmarshalJSON

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

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

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

DateTimeFromTime creates a DateTime from a stdlib time.Time and a Zone.

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

Add returns a new DateTime advanced by d using exact (nanosecond) arithmetic. To move back, pass a negative Duration: dt.Add(-30 * gotime.Minute).

func (DateTime) AddPeriod

func (dt DateTime) AddPeriod(p Period) DateTime

AddPeriod returns a new DateTime advanced by p (calendar arithmetic), preserving the local wall-clock time across DST transitions. Month/year arithmetic applies end-of-month clamping (Jan 31 + 1 month = Feb 28/29). To move back, pass a negated Period: dt.AddPeriod(gotime.Months(1).Negate()).

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,
	)
	feb := jan31.AddPeriod(gotime.Months(1))
	fmt.Println("Jan 31 + 1 month:", feb.Date())

	exact := jan31.Add(48 * gotime.Hour)
	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

In converts dt to the same absolute moment expressed in zone z.

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","value":"<RFC3339Nano>","zone":"<IANA id>","calendar":"iso8601"}.

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","value":"<RFC3339Nano>","zone":"<IANA id>"[,"calendar":"..."]}. The zone field is preferred; if absent the offset embedded in value is used.

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) RFC5545

func (d Duration) RFC5545() string

RFC5545 formats d per RFC 5545 §3.3.6 using W/D/H/M/S designators only. Duration cannot carry months/years, so this never returns an error.

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        int64
	Minutes      int64
	Seconds      int64
	Milliseconds int64
	Microseconds int64
	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"
	// CodeAmbiguousTime reports multiple plausible time interpretations.
	CodeAmbiguousTime ErrorCode = "AMBIGUOUS_TIME"
	// CodeAmbiguousZone reports timezone input matching multiple zones.
	CodeAmbiguousZone ErrorCode = "AMBIGUOUS_ZONE"
	// 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

In projects the Instant into a timezone, returning a DateTime.

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>","epoch_ms":N}.

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 IntervalOf

func IntervalOf(start Instant, d Duration) Interval

IntervalOf creates an Interval from a start Instant and a non-negative Duration. Negative durations are treated as their absolute value.

func NewInterval

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

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

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

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"
	// 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 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"), the locale also disambiguates month-first (en-US, en-CA, en-AU) vs day-first ordering. With no locale, only-valid interpretations resolve, otherwise the result is Ambiguous with both candidates.

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.

func WithZone

func WithZone(zone Zone) Option

WithZone sets the timezone used when the input has no explicit offset.

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, ParseDate, ParseTime, ParseDuration, ParsePeriod, ParseInterval) — they return (T, error) directly and skip the Status / Kind dispatch entirely.

Reach for Parse when you need any of:

  • Polymorphic dispatch on Kind via ParseResult.Value and a Go type switch.
  • Access to [ParseResult.Candidates] for ambiguous inputs.
  • Access to [ParseResult.Warnings], [ParseResult.HasZone], or [ParseResult.Reference] metadata.
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
	}
	dt, _ := r.DateTime()
	fmt.Println(dt)

}
Output:
2026-03-27T13:00:00+09:00

func (ParseResult) Date

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

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

func (ParseResult) DateTime

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

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

func (ParseResult) Duration

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

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

func (ParseResult) Instant

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

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

func (ParseResult) Interval

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

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

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 Kind == KindPeriod.

func (ParseResult) Time

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

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

func (ParseResult) Value

func (r ParseResult) Value() any

Value returns the parsed value as an untyped any so callers can dispatch via a Go type switch. The concrete type is one of Instant, DateTime, Date, Time, Duration, Period, or Interval — matching r.Kind. Returns nil when r.Status is not StatusResolved, so a type switch with a nil case (or default) naturally handles ambiguous / invalid inputs.

switch v := result.Value().(type) {
case gotime.DateTime: handle(v)
case gotime.Date:     handle(v)
case nil:             // ambiguous or invalid — inspect result.Candidates / result.Error
}

When you already know the target type, prefer the typed helpers (ParseDateTime, ParseDate, ...). The comma-ok accessors (ParseResult.DateTime etc.) remain available for callers that use Parse for Warnings / Candidates but still know the Kind statically.

type Period

type Period struct {
	Years  int32 `json:"years,omitzero"`
	Months int32 `json:"months,omitzero"`
	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 int) 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 int) Period

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

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 int) Period

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

func (Period) Abs

func (p Period) Abs() Period

Abs returns p with all fields made non-negative.

func (Period) Add

func (p Period) Add(other Period) Period

Add returns p + other (componentwise).

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

Negate returns -p (all fields flipped).

func (Period) RFC5545

func (p Period) RFC5545() string

RFC5545 formats p per RFC 5545 §3.3.6. RFC 5545 disallows months/years in DURATION values, but Period intentionally carries calendar offsets; we still emit P{n}Y, P{n}M, P{n}D — callers responsible for validating the receiving system supports this. For weeks-aligned days, emit "P{n}W".

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

Sub returns p - other (componentwise).

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","precision":"second"} or with sub-second value and appropriate precision when nanoseconds are non-zero.

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) Std

func (t Time) Std(on Date, z Zone) time.Time

Std returns the clock time projected onto Date d in Zone z as a time.Time. Use this to bridge a clock Time to any API expecting a stdlib time.Time.

func (Time) String

func (t Time) String() string

String returns the clock time as "HH:MM:SS".

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 or fixed offset.

var Local Zone

Local is the process-local timezone reported by the host system.

var UTC Zone

UTC is the UTC timezone.

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, Windows timezone names, and fixed UTC offsets. Legacy IANA aliases such as "US/Eastern" are handled by Go's time.LoadLocation.

func (Zone) Abbreviation

func (z Zone) Abbreviation(i Instant) string

Abbreviation returns the timezone abbreviation (e.g., "JST", "EDT") at the given Instant.

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) IsDST

func (z Zone) IsDST(i Instant) bool

IsDST reports whether the zone is observing Daylight Saving Time at the given Instant.

func (Zone) IsZero

func (z Zone) IsZero() bool

IsZero reports whether z is the zero value (no zone explicitly set). Note that the zero Zone is still safe to operate on: Location falls back to time.UTC. Use IsZero only when you need to detect "was this set?".

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() — time-dependent display data lives in Zone.Snapshot(at).

func (Zone) OffsetAt

func (z Zone) OffsetAt(i Instant) string

OffsetAt returns the UTC offset of the zone at the given Instant as a "+HH:MM" or "-HH:MM" string.

func (Zone) Snapshot

func (z Zone) Snapshot(i Instant) ZoneSnapshot

Snapshot returns a point-in-time view of z (offset, DST, abbreviation) at i. The snapshot is decoupled from JSON serialization — callers requiring time-dependent fields compute and embed them explicitly.

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>",...}.

type ZoneSnapshot

type ZoneSnapshot struct {
	// ID is the IANA zone identifier.
	ID string `json:"id"`
	// Offset is the UTC offset at the snapshot time, formatted as "+HH:MM".
	Offset string `json:"offset"`
	// DST reports whether the zone is observing Daylight Saving Time.
	DST bool `json:"dst"`
	// Abbreviation is the zone abbreviation (e.g. "JST", "PDT").
	Abbreviation string `json:"abbreviation"`
}

ZoneSnapshot is a point-in-time snapshot of a Zone's display data. It is intentionally cheap and copy-safe — callers compute it on demand.

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.

Jump to

Keyboard shortcuts

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