time — deterministic instant + duration span
github.com/go-composites/time (Go package Time, imported from the src/
sub-directory) is a composite over an instant in time, wrapping Go's stdlib
time.Time (Ruby's Time as the reference). Its sibling
Duration (the src/duration sub-package) wraps a span of
time. Both are interface-first, their fallible constructors return a
result so failures are values rather than panics, and both ship a
Null-Object variant.
Deterministic by design — there is no Now()
Time is built only from explicit values (FromUnix, Parse); there is
deliberately no Now(). This keeps behaviour reproducible across
architectures and test runs.
Time
API
import Time "github.com/go-composites/time/src"
type Interface interface {
ToUnix() int64
Format(layout string) string
ToGoString() string
In(name string) Result.Interface
Zone() string
UTC() Interface
Before(Interface) bool
After(Interface) bool
Equal(Interface) bool
Add(Duration.Interface) Result.Interface
Sub(Interface) Duration.Interface
IsNull() bool
}
func FromUnix(sec int64) Interface
func Parse(layout, value string) Result.Interface
func Null() Interface
FromUnix(sec)builds aTimefrom a Unix timestamp (seconds since the epoch), in UTC.Parse(layout, value)returns aResult; on success its payload is theTime, otherwise it carries anError(the value did not match the layout).Null()returns the Null-Object time (see below).
| Member | Behaviour |
|---|---|
ToUnix() |
Returns the instant as a Unix timestamp (seconds). |
Format(layout) |
Renders the instant per the given layout (see time.Time.Format). |
ToGoString() |
Returns the RFC3339 representation. |
In(name) |
Returns a Result whose payload is a new Time denoting the same instant in the IANA location name (e.g. "Europe/Paris"); when name is not a known zone the Result carries an Error instead. Only the location changes, so the underlying instant — and Before/After/Equal — is preserved. Never panics, never returns nil. |
Zone() |
Returns the abbreviated name of the time zone in effect at the instant (e.g. "UTC", "CET"). |
UTC() |
Returns a new Time denoting the same instant with its location set to UTC. |
Before(other) / After(other) |
Report whether the receiver is strictly before / after other. |
Equal(other) |
Reports whether both denote the same instant (compared by Unix seconds). |
Add(duration) |
Returns a Result whose payload is a new Time shifted forward by duration. |
Sub(other) |
Returns the Duration spanning from other to the receiver. |
IsNull() |
Returns false for a real time, true for the Null() one. |
Usage
t := Time.FromUnix(0) // 1970-01-01T00:00:00Z
fmt.Println(t.ToGoString()) // 1970-01-01T00:00:00Z
r := Time.Parse(time.RFC3339, "2026-06-20T12:00:00Z")
if !r.HasError() {
parsed := r.Payload().(Time.Interface)
fmt.Println(parsed.After(t)) // true
}
if shifted := t.Add(Duration.FromSeconds(3600)); !shifted.HasError() {
fmt.Println(shifted.Payload().(Time.Interface).ToUnix()) // 3600
}
// Relocate to an IANA zone — same instant, different location:
if r := t.In("Europe/Paris"); !r.HasError() {
paris := r.Payload().(Time.Interface)
fmt.Println(paris.Zone()) // CET
fmt.Println(paris.Equal(t)) // true — the underlying instant is unchanged
}
IANA zones work on every architecture
time embeds the IANA time-zone database (time/tzdata), so In(...)
resolves zones like "Europe/Paris" deterministically on every
architecture and inside the toolchain-less CI containers — without relying
on a system tzdata install. Because relocating only changes the location, the
underlying instant is preserved, so Before/After/Equal are unaffected.
The Null-Object Time
Time.Null() denotes the zero instant: ToUnix() is 0, Format returns
"", ToGoString() is <NullTime>, In(...) carries an
Error.New("cannot relocate a null Time"), Zone() is "", UTC() yields
another null Time, comparisons are false, Equal is true only against
another null, Add yields a null Time, Sub yields a null Duration, and
IsNull() returns true.
Duration
The src/duration sub-package (Go package Duration) wraps Go's
time.Duration.
API
import Duration "github.com/go-composites/time/src/duration"
type Interface interface {
ToSeconds() int64
ToGoString() string
Add(Interface) Interface
Sub(Interface) Interface
Equal(Interface) bool
IsNull() bool
}
func FromSeconds(sec int64) Interface
func Parse(s string) Result.Interface
func Null() Interface
FromSeconds(sec)builds aDurationfrom a whole number of seconds.Parse(s)returns aResult; on success its payload is theDuration, otherwise it carries anError(the input was not a valid Go duration string — seetime.ParseDuration).Null()returns the Null-Object duration.
| Member | Behaviour |
|---|---|
ToSeconds() |
Returns the span truncated to a whole number of seconds. |
ToGoString() |
Returns the Go textual form (e.g. 1h30m0s). |
Add(other) / Sub(other) |
Return a new Duration that is the sum / difference of the receiver and other. |
Equal(other) |
Reports whether both span the same number of seconds. |
IsNull() |
Returns false for a real duration, true for the Null() one. |
Usage
d := Duration.FromSeconds(90)
fmt.Println(d.ToGoString()) // 1m30s
r := Duration.Parse("1h30m")
if !r.HasError() {
fmt.Println(r.Payload().(Duration.Interface).ToSeconds()) // 5400
}
The Null-Object Duration
Duration.Null() spans zero seconds (ToSeconds() is 0), ToGoString()
is <NullDuration>, its arithmetic returns itself, Equal is true only
against another null, and IsNull() returns true.
Dependencies
Both time and its duration sub-package depend on result and
error (and transitively on null). Time also depends
on its own duration sub-package.