temporal

package
v1.0.0-alpha.8 Latest Latest
Warning

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

Go to latest
Published: Oct 9, 2026 License: MIT Imports: 11 Imported by: 0

Documentation

Overview

Package temporal is the single home of Pulse's epoch-day and calendar arithmetic. A `date` cell holds whole epoch DAYS and a `datetime` cell holds whole epoch SECONDS (naive UTC); every conversion between the two, and every calendar boundary computed from a day, goes through this package rather than open-coding the 86,400 factor at a call site.

It is a leaf over the stdlib plus the public `errors` package (for the coded PULSE_TIMEZONE_UNKNOWN refusal): it must never import the public `encoding` package (which forwards into it) or any other Pulse package. It is also the only package that embeds the tz database and resolves IANA zones (zone.go).

Index

Constants

View Source
const SecondsPerDay = 86400

SecondsPerDay is the conversion factor between the `datetime` (epoch seconds) and `date` (epoch days) on-wire representations. Untyped so it composes with int64, uint64 and float64 arithmetic alike.

View Source
const SecondsPerHour = 3600

SecondsPerHour is the conversion factor between epoch seconds and epoch HOURS — the unit GROUP_DATE's `hour` component buckets on.

View Source
const TZDataVersion = "2026c"

TZDataVersion is the IANA tz database release embedded in zoneinfo.zip. Go's zip carries no version entry, so the value is recorded when the file is refreshed (from the DATA= line of the toolchain's lib/time/update.bash) and pinned to the file's bytes by tzdataSHA256: TestTZDataVersion_MatchesEmbeddedZip fails when the zip changes without both constants being updated.

Variables

View Source
var (
	ErrLocalAmbiguous   = stderrors.New("local time is ambiguous: a DST transition repeats it")
	ErrLocalNonexistent = stderrors.New("local time does not exist: a DST transition skips it")
)

ErrLocalAmbiguous and ErrLocalNonexistent are what ResolveLocal and ParseLocal return under AmbiguityError for an overlap or a gap. They carry no row or column; the caller wraps them with its own context.

View Source
var UTC = &Zone{name: "UTC", loc: time.UTC, utc: true}

UTC is the UTC sentinel: zero offset everywhere, no table, and every lookup short-circuits. LoadZone("UTC") returns exactly this pointer.

Functions

func DateTimeLayouts

func DateTimeLayouts() []string

DateTimeLayouts returns a copy of the datetime literal layouts ParseLocal walks, in priority order.

func DateTimeToDay

func DateTimeToDay(sec int64) int64

DateTimeToDay truncates epoch seconds (naive UTC) to the epoch day that contains the instant. Truncation is toward the PAST, never toward zero and never rounding: 1969-12-31T23:59:59Z (sec -1) is day -1, not day 0.

func DayToTime

func DayToTime(day int64) time.Time

DayToTime returns UTC midnight of epoch day day.

func DayToUnix

func DayToUnix(day int64) int64

DayToUnix returns the epoch seconds of UTC midnight of epoch day day.

func DayToUnixFloat

func DayToUnixFloat(day float64) float64

DayToUnixFloat is DayToUnix over float64 for callers (SPSS) that carry day counts as doubles. The multiply is exact for every day count a `date` cell can hold.

func FormatLocal

func FormatLocal(sec int64, z *Zone) string

FormatLocal renders instant sec (epoch seconds) as an RFC 3339 / ISO 8601 literal on zone z's wall clock with its numeric UTC offset — 2026-03-29T03:30:00+02:00. The literal names exactly the instant sec: re-parsing it (encoding.ParseDateTime honours the offset) yields sec again, so an export rendered through FormatLocal re-imports losslessly.

A nil or UTC-equivalent zone (Zone.IsUTC) renders the canonical `…Z` form, byte-identical to encoding.FormatDateTime. So does an instant whose offset is not a whole number of minutes — a pre-1900 local-mean-time offset such as Europe/Berlin's +00:53:28: RFC 3339 has no spelling for a seconds offset, and truncating it to +00:53 would name a different instant, so the UTC form (still exact) is the only honest literal. One string allocation per call.

func HourToDay

func HourToDay(hour int64) int64

HourToDay returns the epoch day containing epoch hour hour, truncating toward the past as DateTimeToDay does.

func HourToTime

func HourToTime(hour int64) time.Time

HourToTime returns the UTC time.Time at the start of epoch hour hour. Fed a LOCAL epoch hour (LocalHour), its fields are the wall-clock date and hour a clock in that zone shows.

func IsWholeDay

func IsWholeDay(sec int64) bool

IsWholeDay reports whether sec (epoch seconds) falls exactly on a UTC midnight boundary, for either sign.

func LocalDay

func LocalDay(sec int64, z *Zone) int64

LocalDay returns the local epoch day of instant sec (epoch seconds) in zone z: floor((sec + z.Offset(sec)) / SecondsPerDay), truncating toward the PAST exactly as DateTimeToDay does. LocalDay(sec, UTC) == DateTimeToDay(sec). Allocation-free.

func LocalHour

func LocalHour(sec int64, z *Zone) int64

LocalHour returns the local epoch HOUR of instant sec in zone z: floor((sec + z.Offset(sec)) / SecondsPerHour), the wall-clock hour a clock in z shows, counted as hours since 1970-01-01T00 on that clock. A fall-back transition repeats a wall-clock hour, so the two instants an hour apart that read it share one local hour; a spring-forward transition skips one, which no instant reads. LocalHour(sec, UTC) is the UTC hour. Allocation-free.

func LocalMidnightUTC

func LocalMidnightUTC(day int64, z *Zone) int64

LocalMidnightUTC returns the UTC instant (epoch seconds) at which local epoch day day begins in zone z: the FIRST instant s whose wall clock in z reads day 00:00:00 or later. Under UTC it is day * SecondsPerDay.

Where a DST transition skips local midnight (America/Santiago, America/Havana and, historically, Asia/Beirut spring forward from 00:00 to 01:00), there is no instant reading 00:00, and the result is the first instant that exists on that local day — the transition instant, whose wall clock reads 01:00. Where a transition repeats the hour before midnight (a fall-back from 24:00 to 23:00), the day begins at the single instant reading 00:00, after the repeated hour.

Invariants: LocalMidnightUTC(LocalDay(s, z), z) <= s for every s, and LocalDay(LocalMidnightUTC(d, z), z) == d for every local day d that has at least one instant. A day that a zone skips ENTIRELY (Pacific/Apia, 2011-12-30) has no instant; the result is then the first instant of the next day that exists, so LocalDay of it is greater than d.

func MonthStart

func MonthStart(day int64) time.Time

MonthStart returns UTC midnight of the first day of the calendar month containing epoch day day.

func ParseWeekday

func ParseWeekday(name string) (time.Weekday, bool)

ParseWeekday maps a lowercase English day name ("monday" … "sunday") to its time.Weekday. Any other spelling (case included) is refused.

func QuarterStart

func QuarterStart(day int64) time.Time

QuarterStart returns UTC midnight of the first day of the calendar quarter (Jan / Apr / Jul / Oct) containing epoch day day.

func TimeToDay

func TimeToDay(t time.Time) int64

TimeToDay returns the epoch day containing t's instant, with the same toward-the-past truncation as DateTimeToDay. t's location is ignored; only the instant matters.

func WeekStartDay

func WeekStartDay(day int64, start time.Weekday) int64

WeekStartDay returns the epoch day on which the week containing epoch day day begins, for weeks that begin on weekday start. With start == time.Monday it is the Monday of the day's ISO week.

func YearStart

func YearStart(day int64) time.Time

YearStart returns UTC midnight of January 1 of the calendar year containing epoch day day.

Types

type Ambiguity

type Ambiguity uint8

Ambiguity is the policy for a wall-clock reading that a zone's DST transition makes ambiguous (an overlap: it occurs twice) or nonexistent (a gap: the clock skips it).

const (
	// AmbiguityError refuses both cases: ResolveLocal / ParseLocal
	// return ErrLocalAmbiguous or ErrLocalNonexistent. The default.
	AmbiguityError Ambiguity = iota
	// AmbiguityEarlier resolves with the PRE-transition offset: an
	// overlap takes its first occurrence; a gap reads the skipped wall
	// clock as if the transition had not happened yet (Europe/Berlin
	// 2026-03-29 02:30 → 01:30Z).
	AmbiguityEarlier
	// AmbiguityLater resolves with the POST-transition offset: an
	// overlap takes its second occurrence; a gap reads the skipped
	// wall clock as if the transition had already happened
	// (Europe/Berlin 2026-03-29 02:30 → 00:30Z).
	AmbiguityLater
)

func ParseAmbiguity

func ParseAmbiguity(s string) (Ambiguity, bool)

ParseAmbiguity parses a policy spelling. The empty string is the default, AmbiguityError; anything but "", "error", "earlier" and "later" (exact, lower case) reports ok=false.

func (Ambiguity) String

func (a Ambiguity) String() string

String returns the policy's spelling: "error", "earlier" or "later".

type Cache

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

Cache memoises LoadZone by name. LoadZone builds a transition table on every call, so a long-lived owner (one per pulse.Pulse instance) keeps one Cache and resolves every request-time zone through it. Only successful loads are cached: a refused name is re-validated (and re-refused) on every call, so a cache can never mask an error.

The zero value is ready to use and safe for concurrent use.

func (*Cache) Load

func (c *Cache) Load(name string) (*Zone, error)

Load returns the Zone for name, loading and caching it on first use. It returns exactly what LoadZone returns for an unknown name (a PULSE_TIMEZONE_UNKNOWN *errors.CodedError); "UTC" is the UTC sentinel. Concurrent first loads of one name may each build a table, but every caller observes the single stored pointer.

type LocalKind

type LocalKind uint8

LocalKind tells the caller how a literal resolved.

const (
	// LocalExact: a naive wall clock the zone reads exactly once.
	LocalExact LocalKind = iota
	// LocalOffset: the literal carried its own `Z` or ±HH:MM offset and
	// named that instant; the zone was not consulted.
	LocalOffset
	// LocalAmbiguous: a naive wall clock the zone reads twice (a DST
	// overlap). Resolved by the policy, or refused under AmbiguityError.
	LocalAmbiguous
	// LocalNonexistent: a naive wall clock the zone never reads (a DST
	// gap). Resolved by the policy, or refused under AmbiguityError.
	LocalNonexistent
)

func ParseLocal

func ParseLocal(s string, z *Zone, a Ambiguity) (int64, LocalKind, error)

ParseLocal parses a datetime literal and returns the instant it names as epoch seconds. It accepts exactly the literals encoding.ParseDateTime accepts, first matching layout wins, and:

  • an offset-bearing literal (`Z`, `+02:00`) names that instant; z is ignored (kind LocalOffset) — the answer ParseDateTime gives;
  • a naive literal is a wall-clock reading in z, resolved by ResolveLocal under policy a. Under UTC (or any UTC-equivalent zone) that is the answer ParseDateTime gives.

Sub-second digits are floored toward the past, as ParseDateTime does. A literal matching no layout returns the same ENCODING_INVALID coded error ParseDateTime returns.

func ResolveLocal

func ResolveLocal(wall int64, z *Zone, a Ambiguity) (int64, LocalKind, error)

ResolveLocal returns the instant (epoch seconds) at which a clock in z reads wall, a wall-clock reading counted as seconds since 1970-01-01T00:00:00 on that clock.

A reading z shows exactly once resolves to that instant (LocalExact). One a fall-back shows twice is LocalAmbiguous: AmbiguityEarlier takes the first occurrence (the pre-transition offset), AmbiguityLater the second. One a spring-forward skips is LocalNonexistent: AmbiguityEarlier reads it at the pre-transition offset, AmbiguityLater at the post-transition offset. Under AmbiguityError both return ErrLocalAmbiguous / ErrLocalNonexistent and an instant of 0.

UTC and UTC-equivalent zones return wall unchanged.

type Parts

type Parts struct {
	Year    int
	Month   time.Month
	Day     int // day of month, 1..31
	Hour    int // 0..23
	Minute  int // 0..59
	Second  int // 0..59
	Weekday time.Weekday
	YearDay int // day of year, 1..366
	ISOYear int // ISO 8601 week-numbering year
	ISOWeek int // ISO 8601 week, 1..53
}

Parts is the wall-clock decomposition of an instant in a zone.

func LocalParts

func LocalParts(sec int64, z *Zone) Parts

LocalParts returns the wall-clock fields of instant sec (epoch seconds) in zone z. Every field equals what time.Unix(sec, 0).In(z.Location()) reports. Allocation-free.

type Zone

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

Zone is a validated IANA time zone with a precomputed transition table for allocation-free offset lookup. A *Zone is immutable apart from its lookup cache, which is atomic, so one *Zone is safe to share across goroutines. A Zone must not be copied after LoadZone returns it.

func LoadImportZone

func LoadImportZone(name string) (*Zone, error)

LoadImportZone resolves a SOURCE zone for the import edge. It accepts everything LoadZone accepts and, in addition, a fixed offset spelled exactly "+HH:MM" or "-HH:MM" (|offset| <= 18:00). The fixed form is for imports only — a request-time zone stays IANA-only (LoadZone refuses it). A fixed-offset zone has no transitions, so every naive reading in it is LocalExact; "+00:00" / "-00:00" is UTC-equivalent. Anything else is the PULSE_TIMEZONE_UNKNOWN refusal.

func LoadZone

func LoadZone(name string) (*Zone, error)

LoadZone resolves name to a Zone. The validity rule is strict: name is exactly "UTC" (the UTC sentinel), or an IANA Area/Location name — it contains a '/', every '/'-separated segment opens with an ASCII upper-case letter and carries only letters, digits, '_', '-' and '+' — with an exact, case-sensitive entry in Pulse's embedded tz database (zoneinfo.zip, version TZDataVersion). $ZONEINFO, the host zoneinfo directory and time/tzdata are never consulted, so the accepted set and every offset are identical on every host. "Etc/UTC", "Etc/GMT-5" and "Europe/Berlin" are accepted. Empty, "Local", abbreviations and legacy names without a '/' ("EST", "MST", "GMT", "EST5EDT") and offset strings ("+05:00") are refused with a PULSE_TIMEZONE_UNKNOWN *errors.CodedError whose details carry the name under errors.DetailTimeZone.

func (*Zone) Fork

func (z *Zone) Fork() *Zone

Fork returns a Zone equal to z — same name, location and every answer — that shares z's read-only transition table but owns its own last-span lookup cache. Every goroutine that looks up offsets at a high rate (one parallel decode / shard worker) should hold its own fork: all of them storing into ONE shared cache keep invalidating it for each other (measured: a Europe/Berlin FILTER_DATE_RANGES overhead of ~5% single-threaded grew to ~35% across 8 decode workers). A fork is itself safe for concurrent use, so sharing one is merely slower. The UTC sentinel has no cache and returns itself. One allocation per call; never per lookup.

func (*Zone) IsUTC

func (z *Zone) IsUTC() bool

IsUTC reports whether the zone is UTC-equivalent: the UTC sentinel, or a named zone whose offset is zero at every instant ("Etc/UTC", "Etc/GMT", "Etc/Zulu", "Etc/UCT"). Local-day arithmetic in such a zone is identical to UTC's. A zone that merely sits at offset zero today (e.g. "Africa/Abidjan", which had a local-mean-time offset before 1912, or "Europe/London" in winter) is NOT UTC-equivalent.

func (*Zone) Location

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

Location returns the underlying *time.Location.

func (*Zone) Name

func (z *Zone) Name() string

Name returns the zone name as given to LoadZone.

func (*Zone) Offset

func (z *Zone) Offset(sec int64) int32

Offset returns the zone's offset from UTC, in seconds east, in effect at instant sec (epoch seconds). It equals `_, off := time.Unix(sec, 0).In(loc).Zone()` everywhere, and is allocation-free: the UTC sentinel short-circuits, an instant in the span of the previous hit returns from the cache, other in-window instants binary-search the table, and out-of-window instants defer to the stdlib.

Jump to

Keyboard shortcuts

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