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
- Variables
- func DateTimeLayouts() []string
- func DateTimeToDay(sec int64) int64
- func DayToTime(day int64) time.Time
- func DayToUnix(day int64) int64
- func DayToUnixFloat(day float64) float64
- func FormatLocal(sec int64, z *Zone) string
- func HourToDay(hour int64) int64
- func HourToTime(hour int64) time.Time
- func IsWholeDay(sec int64) bool
- func LocalDay(sec int64, z *Zone) int64
- func LocalHour(sec int64, z *Zone) int64
- func LocalMidnightUTC(day int64, z *Zone) int64
- func MonthStart(day int64) time.Time
- func ParseWeekday(name string) (time.Weekday, bool)
- func QuarterStart(day int64) time.Time
- func TimeToDay(t time.Time) int64
- func WeekStartDay(day int64, start time.Weekday) int64
- func YearStart(day int64) time.Time
- type Ambiguity
- type Cache
- type LocalKind
- type Parts
- type Zone
Constants ¶
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.
const SecondsPerHour = 3600
SecondsPerHour is the conversion factor between epoch seconds and epoch HOURS — the unit GROUP_DATE's `hour` component buckets on.
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 ¶
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.
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 ¶
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 DayToUnixFloat ¶
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 ¶
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 ¶
HourToDay returns the epoch day containing epoch hour hour, truncating toward the past as DateTimeToDay does.
func HourToTime ¶
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 ¶
IsWholeDay reports whether sec (epoch seconds) falls exactly on a UTC midnight boundary, for either sign.
func LocalDay ¶
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 ¶
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 ¶
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 ¶
MonthStart returns UTC midnight of the first day of the calendar month containing epoch day day.
func ParseWeekday ¶
ParseWeekday maps a lowercase English day name ("monday" … "sunday") to its time.Weekday. Any other spelling (case included) is refused.
func QuarterStart ¶
QuarterStart returns UTC midnight of the first day of the calendar quarter (Jan / Apr / Jul / Oct) containing epoch day day.
func TimeToDay ¶
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 ¶
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.
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 ¶
ParseAmbiguity parses a policy spelling. The empty string is the default, AmbiguityError; anything but "", "error", "earlier" and "later" (exact, lower case) reports ok=false.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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) Offset ¶
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.