timecty

package module
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Jul 14, 2026 License: BSD-2-Clause Imports: 12 Imported by: 6

README

time-cty-funcs

cty functions and types for dealing with time; mainly used in HCL2 templates.

CI

Overview

This package provides two go-cty capsule types — time and duration — plus a comprehensive set of functions for working with them in HCL2 expression evaluation contexts.

Types

timecty.TimeCapsuleType

A cty capsule type wrapping Go's time.Time. Supports equality (==, !=) via CapsuleOps. Timezone is stored inside the value; comparison is always by absolute UTC instant regardless of stored timezone.

timecty.DurationCapsuleType

A cty capsule type wrapping Go's time.Duration (int64 nanoseconds; range ±~292 years). Supports equality (==, !=) via CapsuleOps. Use duration::lt/duration::gt (or extract via get(d, unit) and compare numerically) for ordering.

Limitation: Go's time.Duration cannot represent calendar months or years exactly. ISO 8601 durations like P1Y or P1M are rejected; use time::add_years() / time::add_months() instead.

Helper functions
timecty.NewTimeCapsule(t time.Time) cty.Value
timecty.GetTime(val cty.Value) (time.Time, error)
timecty.NewDurationCapsule(d time.Duration) cty.Value
timecty.GetDuration(val cty.Value) (time.Duration, error)

Registration

import timecty "github.com/tsarna/time-cty-funcs"

// Add all time functions to your eval context:
for name, fn := range timecty.GetTimeFunctions() {
    funcs[name] = fn
}

GetTimeFunctions() returns the functions described below, keyed by the name they are callable under. The names are namespacedtime::add, duration::truncate, dns::next_zone_serial — following HashiCorp's conventions for provider-defined functions: the leaf name does not repeat the namespace, and namespaced functions use underscores (HCL's built-in functions run words together for historical reasons).

duration keeps a bare name: it is the type constructor, and reads as one.

There is deliberately no timeadd. That name belongs to go-cty's own stdlib.TimeAddFunc, the string-in/string-out function HCL has always had; a host that wants it registers it directly. time::add carries no compatibility burden as a result, which is why every one of its forms returns a time.

functy externs — the signatures cty cannot express

Some of these functions cannot describe themselves through cty's metadata. cty can only make a function's trailing parameters optional, so an optional argument has to be faked with a variadic — which erases its name, its type, and its default: time::from_unix(n, unit = "s") reflects as from_unix(n, ...args). And a cty function has one signature, so a function whose argument shapes differ per call (time::parse(s) vs time::parse(format, s)), or whose parameters are a union (dns::parse_zone_serial takes a number or a string), cannot be described at all.

Externs() returns functy declarations stating what each really accepts, keyed by a filename to report in diagnostics. A functy host registers them so that help(), generated documentation, and editor tooling show the true signature:

for name, src := range timecty.Externs() {
    parser.RegisterExterns(src, name)
}
> help("time::parse")
time::parse(s: string) -> time
time::parse(format: string, s: string) -> time
time::parse(format: string, s: string, tz: string) -> time

Parse a timestamp.

This package does not import functy — the bytes are opaque here, and embed is stdlib. Functions absent from the declarations are absent deliberately: their cty metadata is complete, so an extern would only be a second place for the same facts to drift.

rich-cty-types integration

The time and duration capsule types implement the rich-cty-types Stringable and Gettable interfaces. To expose the generic tostring and get functions in your eval context, merge them in:

import (
    timecty "github.com/tsarna/time-cty-funcs"
    richcty "github.com/tsarna/rich-cty-types"
)

funcs := richcty.GetGenericFunctions()       // tostring, get, length, ...
for name, fn := range timecty.GetTimeFunctions() {
    funcs[name] = fn
}

With these registered:

  • tostring(t) formats a time as RFC 3339 with nanosecond precision (equivalent to time::format("@rfc3339nano", t)).
  • tostring(d) formats a duration using Go syntax (equivalent to duration::format(d)).
  • get(t, part) extracts a calendar field from a time. Valid part values: "year", "month", "day", "hour", "minute", "second", "nanosecond", "weekday" (0=Sunday), "yearday", "isoweek", "isoyear".
  • get(d, unit) extracts a duration in the given unit. "h", "m", "s" return floats; "ms", "us", "ns" return integers.

The part/unit accessors are available only through get(); the previous timepart() and durationpart() functions have been removed.

String Formats

Timestamps — ISO 8601 / RFC 3339
2024-01-15T10:30:00Z                  # UTC
2024-01-15T10:30:00+05:30             # With offset
2024-01-15T10:30:00.123456789Z        # Sub-second precision
Durations — ISO 8601 P-notation
PT5M           # 5 minutes
PT1H30M        # 1 hour 30 minutes
P1DT12H        # 1 day 12 hours (= 36h fixed)
PT0.5S         # 500 milliseconds
Durations — Go format
5m             # 5 minutes
1h30m          # 1 hour 30 minutes
500ms          # 500 milliseconds
Named format aliases (@ prefix)

time::format and time::parse accept @name shortcuts for Go's time package constants:

Name Example output
@rfc3339 2006-01-02T15:04:05Z07:00
@rfc3339nano 2006-01-02T15:04:05.999999999Z07:00
@date 2006-01-02
@time 15:04:05
@datetime 2006-01-02 15:04:05
@rfc1123 Mon, 02 Jan 2006 15:04:05 MST
@rfc822 02 Jan 06 15:04 MST
@ansic, @unixdate, @rubydate, @rfc822z, @rfc850, @rfc1123z, @kitchen, @stamp, @stampmilli, @stampmicro, @stampnano (see Go time package)

Functions

Timestamp — Creation
Function Signature Description
time::now() () → time Current time in local timezone
time::now(tz) (string) → time Current time in named IANA timezone
time::parse(s) (string) → time Parse RFC 3339 string
time::parse(format, s) (string, string) → time Parse with Go reference-time format or @name alias
time::parse(format, s, tz) (string, string, string) → time Parse with format; apply IANA timezone
time::from_unix(n) (number) → time Create time from Unix seconds (integer or fractional) in UTC
time::from_unix(n, unit) (number, string) → time Unit: "s", "ms", "us", or "ns"
time::strptime(format, s) (string, string) → time Parse with strftime-style format
time::strptime(format, s, tz) (string, string, string) → time Parse with strftime format; apply IANA timezone
Timestamp — Formatting
Function Signature Description
time::format(format, t) (string, time) → string Format with Go reference-time format or @name alias
time::strftime(format, t) (string, time) → string Format with strftime / C-style format
Timestamp — Arithmetic
Function Signature Description
time::add(t, d) (time, duration) → time Add a duration to a time. Either argument may be a string: a timestamp as RFC 3339, a duration in Go or ISO 8601 syntax. Always returns a time.
time::sub(t1, t2) (time, time) → duration Elapsed from t2 to t1; negative if t1 < t2
time::sub(t, d) (time, duration) → time Subtract duration from time
time::since(t) (time) → duration Elapsed since t
time::until(t) (time) → duration Time remaining until t
time::add_years(t, n) (time, number) → time Add n calendar years
time::add_months(t, n) (time, number) → time Add n calendar months
time::add_days(t, n) (time, number) → time Add n calendar days
Timestamp — Decomposition
Function Signature Description
time::to_unix(t) (time) → number Unix epoch as fractional seconds
time::to_unix(t, unit) (time, string) → number Unix epoch in unit: "s" (float), "ms", "us", "ns" (integers)
time::zone() () → string System local timezone name
time::zone(t) (time) → string Stored timezone name
time::in_zone(t, tz) (time, string) → time Re-express t in given IANA timezone

Calendar fields (year, month, day, hour, minute, second, nanosecond, weekday, yearday, isoweek, isoyear) are extracted via the rich-cty-types generic get(t, part) function — see rich-cty-types integration.

Timestamp — Comparison

go-cty v1.18 does not support ordering operators for capsule types. Use these functions instead:

Function Signature Description
time::before(t1, t2) (time, time) → bool True if t1 is before t2
time::after(t1, t2) (time, time) → bool True if t1 is after t2
Duration — Creation
Function Signature Description
duration(s) (string) → duration Parse ISO 8601 (PT5M) or Go format (5m30s)
duration(n, unit) (number, string) → duration n in given unit: "h", "m", "s", "ms", "us", "ns"
Duration — Formatting
Function Signature Description
duration::format(d) (duration) → string Go format (e.g. "1h30m5s")
duration::format(d, fmt) (duration, string) → string fmt is "go" (default) or "iso" (ISO 8601 P-notation)
Duration — Arithmetic

Duration in a given unit is extracted via the rich-cty-types generic get(d, unit) function — see rich-cty-types integration.

Function Signature Description
duration::abs(d) (duration) → duration Absolute value
duration::add(d1, d2) (duration, duration) → duration Sum
duration::sub(d1, d2) (duration, duration) → duration Difference
duration::mul(d, n) (duration, number) → duration Scale by factor
duration::div(d, n) (duration, number) → duration Divide by factor
duration::truncate(d, m) (duration, duration) → duration Truncate to multiple of m
duration::round(d, m) (duration, duration) → duration Round to nearest multiple of m
duration::lt(d1, d2) (duration, duration) → bool True if d1 < d2
duration::gt(d1, d2) (duration, duration) → bool True if d1 > d2
DNS Zone Serials

Functions for working with DNS zone serial numbers in YYYYMMDDNN format.

Function Signature Description
dns::next_zone_serial(s) (number|string) → number Next serial after s, using today's date
dns::next_zone_serial(s, t) (number|string, time) → number Next serial using date from t
dns::parse_zone_serial(s) (number|string) → time Parse serial back to approximate date (UTC midnight)

Examples

# Current time
time::now("UTC")
time::now("America/New_York")

# Parse
time::parse("2024-01-15T10:30:00Z")
time::parse("2006-01-02", "2024-01-15", "UTC")
time::strptime("%Y-%m-%d", "2024-01-15")

# Format
time::format("@date", time::now("UTC"))           # "2024-01-15"
time::format("2006-01-02", time::now("UTC"))      # same
time::strftime("%Y-%m-%d", time::now("UTC"))          # same

# Arithmetic
time::add(time::now("UTC"), duration("1h30m"))
time::sub(end_time, start_time)             # → duration
time::sub(deadline, duration(30, "m"))      # → time

# Duration
time::since(start_time)
get(time::since(start_time), "s")               # float seconds (requires rich-cty-types)
duration::format(time::since(start_time))         # "5m32s"
duration::format(time::since(start_time), "iso")  # "PT5M32S"
tostring(time::since(start_time))               # "5m32s" (requires rich-cty-types)

# Comparison
duration::gt(time::since(last_seen), duration(24, "h"))
time::before(expires_at, time::now("UTC"))

# Calendar field extraction (requires rich-cty-types)
get(time::now("UTC"), "year")                   # 2024
get(time::now("UTC"), "weekday")                # 0=Sun ... 6=Sat

# Unix interop
time::from_unix(epoch_seconds)
time::from_unix(epoch_ms, "ms")
time::to_unix(time::now("UTC"), "ns")

# Calendar
time::add_months(time::now("UTC"), 3)
time::add_days(time::now("UTC"), -7)

# DNS zone serials
dns::next_zone_serial(2026012300)                # → 2026012301
dns::next_zone_serial(old_serial, time::now("UTC"))    # → next serial for today
dns::parse_zone_serial(2026012307)               # → 2026-01-23 00:00:00 UTC

License

BSD 2-Clause — see LICENSE.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var AbsDurationFunc = function.New(&function.Spec{
	Description: "A duration's magnitude, discarding its sign.",
	Params: []function.Parameter{
		{
			Name:        "d",
			Type:        DurationCapsuleType,
			Description: "The duration.",
		},
	},
	Type: function.StaticReturnType(DurationCapsuleType),
	Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
		d, err := GetDuration(args[0])
		if err != nil {
			return cty.NilVal, err
		}
		if d < 0 {
			d = -d
		}
		return NewDurationCapsule(d), nil
	},
})

AbsDurationFunc returns the absolute value of a duration.

View Source
var AddDaysFunc = function.New(&function.Spec{
	Description: "A time shifted by whole days. Negative n moves backwards.",
	Params: []function.Parameter{
		{
			Name:        "t",
			Type:        TimeCapsuleType,
			Description: "The time to shift.",
		},
		{
			Name:        "n",
			Type:        cty.Number,
			Description: "Days to add; truncated to a whole number.",
		},
	},
	Type: function.StaticReturnType(TimeCapsuleType),
	Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
		t, err := GetTime(args[0])
		if err != nil {
			return cty.NilVal, err
		}
		n, _ := args[1].AsBigFloat().Int64()
		return NewTimeCapsule(t.AddDate(0, 0, int(n))), nil
	},
})

AddDaysFunc adds n calendar days to a time (calls time.Time.AddDate).

View Source
var AddMonthsFunc = function.New(&function.Spec{
	Description: "A time shifted by whole calendar months. Negative n moves backwards; a day-of-month that the target month lacks rolls forward, as Go's AddDate does.",
	Params: []function.Parameter{
		{
			Name:        "t",
			Type:        TimeCapsuleType,
			Description: "The time to shift.",
		},
		{
			Name:        "n",
			Type:        cty.Number,
			Description: "Months to add; truncated to a whole number.",
		},
	},
	Type: function.StaticReturnType(TimeCapsuleType),
	Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
		t, err := GetTime(args[0])
		if err != nil {
			return cty.NilVal, err
		}
		n, _ := args[1].AsBigFloat().Int64()
		return NewTimeCapsule(t.AddDate(0, int(n), 0)), nil
	},
})

AddMonthsFunc adds n calendar months to a time (calls time.Time.AddDate).

View Source
var AddYearsFunc = function.New(&function.Spec{
	Description: "A time shifted by whole calendar years. Negative n moves backwards.",
	Params: []function.Parameter{
		{
			Name:        "t",
			Type:        TimeCapsuleType,
			Description: "The time to shift.",
		},
		{
			Name:        "n",
			Type:        cty.Number,
			Description: "Years to add; truncated to a whole number.",
		},
	},
	Type: function.StaticReturnType(TimeCapsuleType),
	Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
		t, err := GetTime(args[0])
		if err != nil {
			return cty.NilVal, err
		}
		n, _ := args[1].AsBigFloat().Int64()
		return NewTimeCapsule(t.AddDate(int(n), 0, 0)), nil
	},
})

AddYearsFunc adds n calendar years to a time (calls time.Time.AddDate).

View Source
var DurationAddFunc = function.New(&function.Spec{
	Description: "The sum of two durations.",
	Params: []function.Parameter{
		{
			Name:        "d1",
			Type:        DurationCapsuleType,
			Description: "The first duration.",
		},
		{
			Name:        "d2",
			Type:        DurationCapsuleType,
			Description: "The duration to add.",
		},
	},
	Type: function.StaticReturnType(DurationCapsuleType),
	Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
		d1, _ := GetDuration(args[0])
		d2, _ := GetDuration(args[1])
		return NewDurationCapsule(d1 + d2), nil
	},
})

DurationAddFunc adds two durations: d1 + d2

View Source
var DurationCapsuleType = cty.CapsuleWithOps("duration", reflect.TypeOf(Duration{}), &cty.CapsuleOps{
	Equals: func(a, b any) cty.Value {
		da := a.(*Duration)
		db := b.(*Duration)
		return cty.BoolVal(da.Duration == db.Duration)
	},
	RawEquals: func(a, b any) bool {
		da := a.(*Duration)
		db := b.(*Duration)
		return da.Duration == db.Duration
	},
	GoString: func(val any) string {
		return fmt.Sprintf("duration(%q)", val.(*Duration).Duration.String())
	},
	TypeGoString: func(_ reflect.Type) string {
		return "duration"
	},
})

DurationCapsuleType is a cty capsule type wrapping Duration. Supports equality (==, !=) via Equals/RawEquals. Note: ordering operators (<, >, etc.) are not available for capsule types in go-cty — use get(d, unit) (via rich-cty-types) to extract a numeric value and compare that instead.

View Source
var DurationDivFunc = function.New(&function.Spec{
	Description: "A duration divided by a divisor. Dividing by zero is an error.",
	Params: []function.Parameter{
		{
			Name:        "d",
			Type:        DurationCapsuleType,
			Description: "The duration to divide.",
		},
		{
			Name:        "n",
			Type:        cty.Number,
			Description: "The divisor; may be fractional.",
		},
	},
	Type: function.StaticReturnType(DurationCapsuleType),
	Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
		d, _ := GetDuration(args[0])
		n, _ := args[1].AsBigFloat().Float64()
		if n == 0 {
			return cty.NilVal, fmt.Errorf("duration::div: division by zero")
		}
		return NewDurationCapsule(time.Duration(float64(d) / n)), nil
	},
})

DurationDivFunc divides a duration by a scalar: d / n (returns duration)

View Source
var DurationFunc = function.New(&function.Spec{
	Description: `A duration, from a string ("1h30m" or ISO 8601 "PT1H30M") or from a number and a unit. The first argument's type therefore decides how many arguments there are, which is why the externs declare this as two forms.`,
	Params: []function.Parameter{
		{

			Name:        "val",
			Type:        cty.DynamicPseudoType,
			Description: "A duration string, or the number of `unit`s.",
		},
	},

	VarParam: &function.Parameter{
		Name:        "unit",
		Type:        cty.String,
		Description: `One of "h", "m", "s", "ms", "us", "ns". Required when val is a number; not allowed when it is a string.`,
	},
	Type: func(args []cty.Value) (cty.Type, error) {
		switch len(args) {
		case 1:
			t := args[0].Type()
			if t != cty.String && t != cty.DynamicPseudoType {
				return cty.NilType, fmt.Errorf("duration() 1-arg form requires a string, got %s", t.FriendlyName())
			}
			return DurationCapsuleType, nil
		case 2:
			t0, t1 := args[0].Type(), args[1].Type()
			if t0 != cty.Number && t0 != cty.DynamicPseudoType {
				return cty.NilType, fmt.Errorf("duration() 2-arg form requires a number as first argument, got %s", t0.FriendlyName())
			}
			if t1 != cty.String && t1 != cty.DynamicPseudoType {
				return cty.NilType, fmt.Errorf("duration() 2-arg form requires a string unit as second argument, got %s", t1.FriendlyName())
			}
			return DurationCapsuleType, nil
		default:
			return cty.NilType, fmt.Errorf("duration() requires 1 or 2 arguments, got %d", len(args))
		}
	},
	Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
		if len(args) == 1 {
			return parseDurationString(args[0].AsString())
		}

		n, _ := args[0].AsBigFloat().Float64()
		return durationFromNumber(n, args[1].AsString())
	},
})

DurationFunc creates a duration from a string or from a number and unit. Called as duration("5m"), duration("PT5M"), or duration(5, "m").

View Source
var DurationGtFunc = function.New(&function.Spec{
	Description: "Whether d1 is longer than d2. Signed: 1s is greater than -1h.",
	Params: []function.Parameter{
		{
			Name:        "d1",
			Type:        DurationCapsuleType,
			Description: "The duration to test.",
		},
		{
			Name:        "d2",
			Type:        DurationCapsuleType,
			Description: "The duration to compare against.",
		},
	},
	Type: function.StaticReturnType(cty.Bool),
	Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
		d1, err := GetDuration(args[0])
		if err != nil {
			return cty.NilVal, err
		}
		d2, err := GetDuration(args[1])
		if err != nil {
			return cty.NilVal, err
		}
		return cty.BoolVal(d1 > d2), nil
	},
})

DurationGtFunc returns true if d1 > d2.

View Source
var DurationLtFunc = function.New(&function.Spec{
	Description: "Whether d1 is shorter than d2. Signed: -1h is less than 1s.",
	Params: []function.Parameter{
		{
			Name:        "d1",
			Type:        DurationCapsuleType,
			Description: "The duration to test.",
		},
		{
			Name:        "d2",
			Type:        DurationCapsuleType,
			Description: "The duration to compare against.",
		},
	},
	Type: function.StaticReturnType(cty.Bool),
	Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
		d1, err := GetDuration(args[0])
		if err != nil {
			return cty.NilVal, err
		}
		d2, err := GetDuration(args[1])
		if err != nil {
			return cty.NilVal, err
		}
		return cty.BoolVal(d1 < d2), nil
	},
})

DurationLtFunc returns true if d1 < d2.

View Source
var DurationMulFunc = function.New(&function.Spec{
	Description: "A duration scaled by a factor.",
	Params: []function.Parameter{
		{
			Name:        "d",
			Type:        DurationCapsuleType,
			Description: "The duration to scale.",
		},
		{
			Name:        "n",
			Type:        cty.Number,
			Description: "The factor; may be fractional.",
		},
	},
	Type: function.StaticReturnType(DurationCapsuleType),
	Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
		d, _ := GetDuration(args[0])
		n, _ := args[1].AsBigFloat().Float64()
		return NewDurationCapsule(time.Duration(float64(d) * n)), nil
	},
})

DurationMulFunc multiplies a duration by a scalar: d * n

View Source
var DurationRoundFunc = function.New(&function.Spec{
	Description: "A duration rounded to the nearest multiple of m, halves away from zero.",
	Params: []function.Parameter{
		{
			Name:        "d",
			Type:        DurationCapsuleType,
			Description: "The duration to round.",
		},
		{
			Name:        "m",
			Type:        DurationCapsuleType,
			Description: "The multiple to round to — itself a duration, e.g. duration(\"1m\").",
		},
	},
	Type: function.StaticReturnType(DurationCapsuleType),
	Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
		d, _ := GetDuration(args[0])
		m, _ := GetDuration(args[1])
		return NewDurationCapsule(d.Round(m)), nil
	},
})

DurationRoundFunc rounds d to the nearest multiple of m: d.Round(m)

View Source
var DurationSubFunc = function.New(&function.Spec{
	Description: "The difference between two durations. Negative if d2 is the longer.",
	Params: []function.Parameter{
		{
			Name:        "d1",
			Type:        DurationCapsuleType,
			Description: "The duration to subtract from.",
		},
		{
			Name:        "d2",
			Type:        DurationCapsuleType,
			Description: "The duration to subtract.",
		},
	},
	Type: function.StaticReturnType(DurationCapsuleType),
	Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
		d1, _ := GetDuration(args[0])
		d2, _ := GetDuration(args[1])
		return NewDurationCapsule(d1 - d2), nil
	},
})

DurationSubFunc subtracts durations: d1 - d2

View Source
var DurationTruncateFunc = function.New(&function.Spec{
	Description: "A duration rounded down to a multiple of m.",
	Params: []function.Parameter{
		{
			Name:        "d",
			Type:        DurationCapsuleType,
			Description: "The duration to truncate.",
		},
		{
			Name:        "m",
			Type:        DurationCapsuleType,
			Description: "The multiple to truncate to — itself a duration, e.g. duration(\"1m\").",
		},
	},
	Type: function.StaticReturnType(DurationCapsuleType),
	Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
		d, _ := GetDuration(args[0])
		m, _ := GetDuration(args[1])
		return NewDurationCapsule(d.Truncate(m)), nil
	},
})

DurationTruncateFunc truncates d to a multiple of m: d.Truncate(m)

View Source
var FormatDurationFunc = function.New(&function.Spec{
	Description: "Render a duration.",
	Params: []function.Parameter{
		{
			Name:        "d",
			Type:        DurationCapsuleType,
			Description: "The duration to render.",
		},
	},

	VarParam: &function.Parameter{
		Name:        "fmt",
		Type:        cty.String,
		Description: `"go" ("1h30m0s") or "iso" ("PT1H30M"); defaults to "go".`,
	},
	Type: boundedArity("duration::format", 2, cty.String),
	Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
		d, err := GetDuration(args[0])
		if err != nil {
			return cty.NilVal, err
		}
		format := "go"
		if len(args) > 1 {
			format = args[1].AsString()
		}
		switch format {
		case "go", "":
			return cty.StringVal(d.String()), nil
		case "iso":
			return cty.StringVal(durationToISO8601(d)), nil
		default:
			return cty.NilVal, fmt.Errorf("duration::format: unknown format %q; valid values are \"go\" and \"iso\"", format)
		}
	},
})

FormatDurationFunc formats a duration as a string. Called as duration::format(d) for Go format (default) or duration::format(d, "iso") for ISO 8601.

View Source
var FormatTimeFunc = function.New(&function.Spec{
	Description: "Render a time with a Go reference layout.",
	Params: []function.Parameter{
		{
			Name:        "format",
			Type:        cty.String,
			Description: "A Go reference layout (\"2006-01-02\"), or an @alias such as @rfc3339.",
		},
		{
			Name:        "t",
			Type:        TimeCapsuleType,
			Description: "The time to render.",
		},
	},
	Type: function.StaticReturnType(cty.String),
	Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
		layout, err := resolveFormat(args[0].AsString())
		if err != nil {
			return cty.NilVal, err
		}
		t, err := GetTime(args[1])
		if err != nil {
			return cty.NilVal, err
		}
		return cty.StringVal(t.Format(layout)), nil
	},
})

FormatTimeFunc formats a time value using Go's reference-time format or a @name alias. Called as time::format("2006-01-02", t) or time::format("@rfc3339", t).

View Source
var FromUnixFunc = function.New(&function.Spec{
	Description: "A time from a Unix epoch count. Always returns a UTC time.",
	Params: []function.Parameter{
		{
			Name:        "n",
			Type:        cty.Number,
			Description: "The epoch count; fractional seconds are honored.",
		},
	},

	VarParam: &function.Parameter{
		Name:        "unit",
		Type:        cty.String,
		Description: `One of "s", "ms", "us", "ns"; defaults to "s".`,
	},
	Type: boundedArity("time::from_unix", 2, TimeCapsuleType),
	Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
		unit := "s"
		if len(args) > 1 {
			unit = args[1].AsString()
		}
		n, _ := args[0].AsBigFloat().Float64()
		switch unit {
		case "s":
			secs := int64(n)
			nanos := int64((n - float64(secs)) * 1e9)
			return NewTimeCapsule(time.Unix(secs, nanos).UTC()), nil
		case "ms":
			return NewTimeCapsule(time.UnixMilli(int64(n)).UTC()), nil
		case "us":
			return NewTimeCapsule(time.UnixMicro(int64(n)).UTC()), nil
		case "ns":
			return NewTimeCapsule(time.Unix(0, int64(n)).UTC()), nil
		default:
			return cty.NilVal, fmt.Errorf("time::from_unix: unknown unit %q; valid units: s, ms, us, ns", unit)
		}
	},
})

FromUnixFunc creates a time from a Unix epoch value. Called as time::from_unix(n) for seconds (possibly fractional), or time::from_unix(n, unit) where unit is "s", "ms", "us", or "ns". Always returns UTC.

View Source
var InTimezoneFunc = function.New(&function.Spec{
	Description: "The same instant, displayed in another zone.",
	Params: []function.Parameter{
		{
			Name:        "t",
			Type:        TimeCapsuleType,
			Description: "The time to convert.",
		},
		{
			Name:        "tz",
			Type:        cty.String,
			Description: "IANA zone name, e.g. \"America/New_York\".",
		},
	},
	Type: function.StaticReturnType(TimeCapsuleType),
	Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
		t, err := GetTime(args[0])
		if err != nil {
			return cty.NilVal, err
		}
		loc, err := time.LoadLocation(args[1].AsString())
		if err != nil {
			return cty.NilVal, fmt.Errorf("time::in_zone: invalid timezone %q: %s", args[1].AsString(), err)
		}
		return NewTimeCapsule(t.In(loc)), nil
	},
})

InTimezoneFunc re-expresses a time in a different IANA timezone. The instant is unchanged; only the displayed timezone changes.

View Source
var NextZoneSerialFunc = function.New(&function.Spec{
	Description: "The next DNS zone serial: whichever is greater of s + 1 and today's date in the conventional YYYYMMDDNN form, so that the serial always advances.",
	Params: []function.Parameter{
		{

			Name:        "s",
			Type:        cty.DynamicPseudoType,
			Description: "The current serial, as a number or a string.",
		},
	},

	VarParam: &function.Parameter{
		Name:        "t",
		Type:        TimeCapsuleType,
		Description: "The date to advance to; defaults to now().",
	},
	Type: func(args []cty.Value) (cty.Type, error) {
		if len(args) > 2 {
			return cty.NilType, fmt.Errorf("dns::next_zone_serial() takes 1 or 2 arguments")
		}
		t0 := args[0].Type()
		if t0 != cty.Number && t0 != cty.String && t0 != cty.DynamicPseudoType {
			return cty.NilType, fmt.Errorf("dns::next_zone_serial: serial must be a number or string, got %s", t0.FriendlyName())
		}
		return cty.Number, nil
	},
	Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
		s, err := parseSerialArg(args[0], "dns::next_zone_serial")
		if err != nil {
			return cty.NilVal, err
		}
		var t time.Time
		if len(args) == 2 {
			t, err = GetTime(args[1])
			if err != nil {
				return cty.NilVal, err
			}
		} else {
			t = time.Now()
		}
		year, month, day := t.Date()
		x := int64(year)*1_000_000 + int64(month)*10_000 + int64(day)*100
		return cty.NumberIntVal(max(s+1, x)), nil
	},
})

NextZoneSerialFunc computes the next DNS zone serial number in YYYYMMDDNN format.

Called as dns::next_zone_serial(s) or dns::next_zone_serial(s, t).

s: current serial (number or string)
t: optional time capsule; defaults to time::now()

Computes x = first serial of the day for t (YYYYMMDD * 100), then returns max(s+1, x).

View Source
var NowFunc = function.New(&function.Spec{
	Description: "The current time. Without a zone, in the machine's local zone.",
	VarParam: &function.Parameter{
		Name:        "tz",
		Type:        cty.String,
		Description: `IANA zone name, e.g. "America/New_York".`,
	},
	Type: boundedArity("time::now", 1, TimeCapsuleType),
	Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
		if len(args) == 0 {
			return NewTimeCapsule(time.Now()), nil
		}
		tzName := args[0].AsString()
		loc, err := time.LoadLocation(tzName)
		if err != nil {
			return cty.NilVal, fmt.Errorf("invalid timezone %q: %s", tzName, err)
		}
		return NewTimeCapsule(time.Now().In(loc)), nil
	},
})

NowFunc returns the current time, optionally in the given IANA timezone. Called as time::now() or time::now("America/New_York").

The optional tz is a VarParam because cty has no other way to spell an optional argument — see the externs, which declare the signature this cannot.

View Source
var ParseTimeFunc = function.New(&function.Spec{
	Description: "Parse a timestamp. With one argument it reads RFC 3339; with two, the first is a Go reference layout or an @alias; a third interprets the parsed wall-clock in that zone. The meaning of the first argument therefore depends on how many there are, which is why the externs declare this as three forms.",

	VarParam: &function.Parameter{
		Name:        "args",
		Type:        cty.String,
		Description: "The timestamp; or a format and then the timestamp; or a format, the timestamp, and an IANA zone.",
	},
	Type: func(args []cty.Value) (cty.Type, error) {
		if len(args) < 1 || len(args) > 3 {
			return cty.NilType, fmt.Errorf("time::parse() takes 1 to 3 arguments")
		}
		return TimeCapsuleType, nil
	},
	Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
		switch len(args) {
		case 1:
			s := args[0].AsString()
			t, err := time.Parse(time.RFC3339Nano, s)
			if err != nil {
				return cty.NilVal, fmt.Errorf("time::parse: invalid RFC 3339 timestamp %q: %s", s, err)
			}
			return NewTimeCapsule(t), nil
		case 2:
			layout, err := resolveFormat(args[0].AsString())
			if err != nil {
				return cty.NilVal, err
			}
			t, err := time.Parse(layout, args[1].AsString())
			if err != nil {
				return cty.NilVal, fmt.Errorf("time::parse: cannot parse %q with format %q: %s", args[1].AsString(), args[0].AsString(), err)
			}
			return NewTimeCapsule(t), nil
		case 3:
			layout, err := resolveFormat(args[0].AsString())
			if err != nil {
				return cty.NilVal, err
			}
			loc, err := time.LoadLocation(args[2].AsString())
			if err != nil {
				return cty.NilVal, fmt.Errorf("time::parse: invalid timezone %q: %s", args[2].AsString(), err)
			}
			t, err := time.ParseInLocation(layout, args[1].AsString(), loc)
			if err != nil {
				return cty.NilVal, fmt.Errorf("time::parse: cannot parse %q with format %q: %s", args[1].AsString(), args[0].AsString(), err)
			}
			return NewTimeCapsule(t), nil
		default:
			return cty.NilVal, fmt.Errorf("time::parse() takes 1 to 3 arguments")
		}
	},
})

ParseTimeFunc parses a timestamp string into a time value.

Forms:

time::parse(s)              — RFC 3339 (timezone required)
time::parse(format, s)      — parse s using Go layout (or @name alias)
time::parse(format, s, tz)  — same, but interpret s in the given IANA timezone
View Source
var ParseZoneSerialFunc = function.New(&function.Spec{
	Description: "The date a DNS zone serial in YYYYMMDDNN form encodes, as a UTC midnight time. The NN revision suffix is discarded; an out-of-range month or day is snapped to the nearest valid date.",
	Params: []function.Parameter{
		{

			Name:        "s",
			Type:        cty.DynamicPseudoType,
			Description: "The serial to decode, as a number or a string.",
		},
	},
	Type: func(args []cty.Value) (cty.Type, error) {
		t := args[0].Type()
		if t != cty.Number && t != cty.String && t != cty.DynamicPseudoType {
			return cty.NilType, fmt.Errorf("dns::parse_zone_serial: serial must be a number or string, got %s", t.FriendlyName())
		}
		return TimeCapsuleType, nil
	},
	Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
		s, err := parseSerialArg(args[0], "dns::parse_zone_serial")
		if err != nil {
			return cty.NilVal, err
		}
		datepart := s / 100
		year := int(datepart / 10_000)
		month := time.Month((datepart / 100) % 100)
		day := int(datepart % 100)

		if month < 1 {
			month = 1
		}
		if month > 12 {
			month = 12
			day = 31
		}
		if day < 1 {
			day = 1
		}
		if last := time.Date(year, month+1, 0, 0, 0, 0, 0, time.UTC).Day(); day > last {
			day = last
		}
		return NewTimeCapsule(time.Date(year, month, day, 0, 0, 0, 0, time.UTC)), nil
	},
})

ParseZoneSerialFunc converts a DNS zone serial back to an approximate time value. The serial format is YYYYMMDDNN; the NN sequence number is ignored. For out-of-range date components, the nearest valid date is used: month > 12 → December 31; day > days in month → last day of month.

View Source
var SinceFunc = function.New(&function.Spec{
	Description: "How long ago a time was: the duration from it until now. Negative if it is in the future.",
	Params: []function.Parameter{
		{
			Name:        "t",
			Type:        TimeCapsuleType,
			Description: "The time to measure from.",
		},
	},
	Type: function.StaticReturnType(DurationCapsuleType),
	Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
		t, err := GetTime(args[0])
		if err != nil {
			return cty.NilVal, err
		}
		return NewDurationCapsule(time.Since(t)), nil
	},
})

SinceFunc returns the duration elapsed since the given time (equivalent to time::sub(time::now(), t)).

View Source
var StrftimeFunc = function.New(&function.Spec{
	Description: "Render a time with a C-style strftime format. Unlike formattime, @aliases are not resolved here.",
	Params: []function.Parameter{
		{
			Name:        "format",
			Type:        cty.String,
			Description: "A strftime-style layout, e.g. \"%Y-%m-%d\".",
		},
		{
			Name:        "t",
			Type:        TimeCapsuleType,
			Description: "The time to render.",
		},
	},
	Type: function.StaticReturnType(cty.String),
	Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
		t, err := GetTime(args[1])
		if err != nil {
			return cty.NilVal, err
		}
		return cty.StringVal(timefmt.Format(t, args[0].AsString())), nil
	},
})

StrftimeFunc formats a time using a strftime-style format string (via itchyny/timefmt-go). Called as time::strftime("%Y-%m-%d", t).

View Source
var StrptimeFunc = function.New(&function.Spec{
	Description: "Parse a timestamp with a C-style strftime format. Unlike parsetime and formattime, @aliases are not resolved here.",
	Params: []function.Parameter{
		{
			Name:        "format",
			Type:        cty.String,
			Description: "strftime-style layout, e.g. \"%Y-%m-%d\".",
		},
		{
			Name:        "s",
			Type:        cty.String,
			Description: "The timestamp to parse.",
		},
	},

	VarParam: &function.Parameter{
		Name:        "tz",
		Type:        cty.String,
		Description: "IANA zone; the parsed wall-clock is read as being in it. Optional.",
	},
	Type: func(args []cty.Value) (cty.Type, error) {
		if len(args) > 3 {
			return cty.NilType, fmt.Errorf("time::strptime() takes 2 or 3 arguments")
		}
		return TimeCapsuleType, nil
	},
	Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
		t, err := timefmt.Parse(args[1].AsString(), args[0].AsString())
		if err != nil {
			return cty.NilVal, fmt.Errorf("time::strptime: cannot parse %q with format %q: %s", args[1].AsString(), args[0].AsString(), err)
		}
		if len(args) == 3 {
			loc, err := time.LoadLocation(args[2].AsString())
			if err != nil {
				return cty.NilVal, fmt.Errorf("time::strptime: invalid timezone %q: %s", args[2].AsString(), err)
			}

			t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), loc)
		}
		return NewTimeCapsule(t), nil
	},
})

StrptimeFunc parses a time string using a strftime-style format (via itchyny/timefmt-go). Called as time::strptime("%Y-%m-%d", "2024-01-15") or time::strptime("%Y-%m-%d", "2024-01-15", "UTC").

View Source
var TimeAddFunc = function.New(&function.Spec{
	Description: "Add a duration to a time. Either argument may be given as a string: a timestamp as RFC 3339, a duration in Go syntax (\"1h30m\") or ISO 8601 (\"PT1H30M\").",
	Params: []function.Parameter{
		{

			Name:        "ts",
			Type:        cty.DynamicPseudoType,
			Description: "The time to add to: a time, or an RFC 3339 string.",
		},
		{
			Name:        "dur",
			Type:        cty.DynamicPseudoType,
			Description: "The duration to add: a duration, or a string in Go or ISO 8601 syntax.",
		},
	},
	Type: func(args []cty.Value) (cty.Type, error) {
		t0, t1 := args[0].Type(), args[1].Type()

		if t0 == cty.DynamicPseudoType || t1 == cty.DynamicPseudoType {
			return cty.DynamicPseudoType, nil
		}
		validTS := t0 == TimeCapsuleType || t0 == cty.String
		validDur := t1 == DurationCapsuleType || t1 == cty.String
		if validTS && validDur {
			return TimeCapsuleType, nil
		}
		return cty.NilType, fmt.Errorf("time::add: unsupported argument types %s and %s", t0.FriendlyName(), t1.FriendlyName())
	},
	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
		// The time: a capsule, or an RFC 3339 (Nano) string.
		var t time.Time
		switch args[0].Type() {
		case cty.String:
			var err error
			t, err = time.Parse(time.RFC3339Nano, args[0].AsString())
			if err != nil {
				return cty.NilVal, fmt.Errorf("time::add: invalid timestamp %q: %s", args[0].AsString(), err)
			}
		case TimeCapsuleType:
			var err error
			t, err = GetTime(args[0])
			if err != nil {
				return cty.NilVal, err
			}
		default:
			return cty.NilVal, fmt.Errorf("time::add: first argument must be a time or string, got %s", args[0].Type().FriendlyName())
		}

		// The duration: a capsule, or a string in Go or ISO 8601 syntax.
		var d time.Duration
		switch args[1].Type() {
		case cty.String:
			v, err := parseDurationString(args[1].AsString())
			if err != nil {
				return cty.NilVal, err
			}
			d, err = GetDuration(v)
			if err != nil {
				return cty.NilVal, err
			}
		case DurationCapsuleType:
			var err error
			d, err = GetDuration(args[1])
			if err != nil {
				return cty.NilVal, err
			}
		default:
			return cty.NilVal, fmt.Errorf("time::add: second argument must be a duration or string, got %s", args[1].Type().FriendlyName())
		}

		return NewTimeCapsule(t.Add(d)), nil
	},
})

TimeAddFunc adds a duration to a time, and always returns a time.

Signatures:

time::add(time, duration)   → time
time::add(time, string)     → time   (string parsed as a duration)
time::add(string, duration) → time   (string parsed as RFC 3339 Nano)
time::add(string, string)   → time

It carries no stdlib-compatibility burden: cty's own stdlib.TimeAddFunc is the string-in/string-out `timeadd`, and a host that wants that behavior registers it directly. Shedding that duty is what lets every form here return a time — so the return type is static, and a string is parsed the same way whichever form it lands in. It used to be neither: (string, string) returned a *string* and accepted only Go duration syntax, so time::add("2024-01-01T00:00:00Z", "PT5M") failed while time::add(time::parse("2024-01-01T00:00:00Z"), "PT5M") succeeded.

View Source
var TimeAfterFunc = function.New(&function.Spec{
	Description: "Whether t1 is later than t2.",
	Params: []function.Parameter{
		{
			Name:        "t1",
			Type:        TimeCapsuleType,
			Description: "The time to test.",
		},
		{
			Name:        "t2",
			Type:        TimeCapsuleType,
			Description: "The time to compare against.",
		},
	},
	Type: function.StaticReturnType(cty.Bool),
	Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
		t1, err := GetTime(args[0])
		if err != nil {
			return cty.NilVal, err
		}
		t2, err := GetTime(args[1])
		if err != nil {
			return cty.NilVal, err
		}
		return cty.BoolVal(t1.After(t2)), nil
	},
})

TimeAfterFunc returns true if t1 is after t2.

View Source
var TimeBeforeFunc = function.New(&function.Spec{
	Description: "Whether t1 is earlier than t2.",
	Params: []function.Parameter{
		{
			Name:        "t1",
			Type:        TimeCapsuleType,
			Description: "The time to test.",
		},
		{
			Name:        "t2",
			Type:        TimeCapsuleType,
			Description: "The time to compare against.",
		},
	},
	Type: function.StaticReturnType(cty.Bool),
	Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
		t1, err := GetTime(args[0])
		if err != nil {
			return cty.NilVal, err
		}
		t2, err := GetTime(args[1])
		if err != nil {
			return cty.NilVal, err
		}
		return cty.BoolVal(t1.Before(t2)), nil
	},
})

TimeBeforeFunc returns true if t1 is before t2.

View Source
var TimeCapsuleType = cty.CapsuleWithOps("time", reflect.TypeOf(Timestamp{}), &cty.CapsuleOps{

	Equals: func(a, b any) cty.Value {
		ta := a.(*Timestamp)
		tb := b.(*Timestamp)
		return cty.BoolVal(ta.Equal(tb.Time))
	},
	RawEquals: func(a, b any) bool {
		ta := a.(*Timestamp)
		tb := b.(*Timestamp)
		return ta.Equal(tb.Time)
	},
	GoString: func(val any) string {
		return fmt.Sprintf("time(%q)", val.(*Timestamp).Format(time.RFC3339Nano))
	},
	TypeGoString: func(_ reflect.Type) string {
		return "time"
	},
})

TimeCapsuleType is a cty capsule type wrapping Timestamp. Supports equality (==, !=) via Equals/RawEquals. Note: ordering operators (<, >, etc.) are not available for capsule types in go-cty — use time::sub() and compare the resulting duration instead.

View Source
var TimeSubFunc = function.New(&function.Spec{
	Description: "Subtract from a time: another time, giving the duration between them (negative if t2 is later), or a duration, giving the earlier time. The return type therefore depends on the arguments, which is why the externs declare this as two forms.",
	Params: []function.Parameter{
		{
			Name:        "t1",
			Type:        TimeCapsuleType,
			Description: "The time to subtract from.",
		},
		{

			Name:        "t2",
			Type:        cty.DynamicPseudoType,
			Description: "A time (giving the duration between) or a duration (giving the earlier time).",
		},
	},
	Type: func(args []cty.Value) (cty.Type, error) {
		t1 := args[1].Type()
		if t1 == cty.DynamicPseudoType {
			return cty.DynamicPseudoType, nil
		}
		switch t1 {
		case TimeCapsuleType:
			return DurationCapsuleType, nil
		case DurationCapsuleType:
			return TimeCapsuleType, nil
		default:
			return cty.NilType, fmt.Errorf("time::sub: second argument must be a time or duration, got %s", t1.FriendlyName())
		}
	},
	Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
		t1, err := GetTime(args[0])
		if err != nil {
			return cty.NilVal, err
		}
		switch args[1].Type() {
		case TimeCapsuleType:
			t2, err := GetTime(args[1])
			if err != nil {
				return cty.NilVal, err
			}
			return NewDurationCapsule(t1.Sub(t2)), nil
		case DurationCapsuleType:
			d, err := GetDuration(args[1])
			if err != nil {
				return cty.NilVal, err
			}
			return NewTimeCapsule(t1.Add(-d)), nil
		default:
			return cty.NilVal, fmt.Errorf("time::sub: second argument must be a time or duration, got %s", args[1].Type().FriendlyName())
		}
	},
})

TimeSubFunc subtracts a time or duration from a time.

Signatures:

time::sub(time, time)     → duration   (elapsed from t2 to t1; negative if t1 < t2)
time::sub(time, duration) → time       (time minus duration)
View Source
var TimezoneFunc = function.New(&function.Spec{
	Description: "A time's zone name. Without an argument, the machine's local zone.",

	VarParam: &function.Parameter{
		Name:        "t",
		Type:        TimeCapsuleType,
		Description: "The time whose zone to report; omit for the local zone.",
	},
	Type: boundedArity("time::zone", 1, cty.String),
	Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
		if len(args) == 0 {
			return cty.StringVal(time.Local.String()), nil
		}
		t, err := GetTime(args[0])
		if err != nil {
			return cty.NilVal, err
		}
		return cty.StringVal(t.Location().String()), nil
	},
})

TimezoneFunc returns the timezone name. Called as time::zone() for the local system timezone, or time::zone(t) for the timezone stored in a time value.

View Source
var UnixFunc = function.New(&function.Spec{
	Description: "A time as a Unix epoch count. Seconds are fractional; the other units are whole.",
	Params: []function.Parameter{
		{
			Name:        "t",
			Type:        TimeCapsuleType,
			Description: "The time to convert.",
		},
	},

	VarParam: &function.Parameter{
		Name:        "unit",
		Type:        cty.String,
		Description: `One of "s", "ms", "us", "ns"; defaults to "s".`,
	},
	Type: boundedArity("time::to_unix", 2, cty.Number),
	Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
		t, err := GetTime(args[0])
		if err != nil {
			return cty.NilVal, err
		}
		unit := "s"
		if len(args) > 1 {
			unit = args[1].AsString()
		}
		switch unit {
		case "s":
			return cty.NumberFloatVal(float64(t.UnixNano()) / 1e9), nil
		case "ms":
			return cty.NumberIntVal(t.UnixMilli()), nil
		case "us":
			return cty.NumberIntVal(t.UnixMicro()), nil
		case "ns":
			return cty.NumberIntVal(t.UnixNano()), nil
		default:
			return cty.NilVal, fmt.Errorf("time::to_unix: unknown unit %q; valid units: s, ms, us, ns", unit)
		}
	},
})

UnixFunc returns the Unix epoch value for a time. Called as time::to_unix(t) for fractional seconds, or time::to_unix(t, unit) where unit is "s" (float), "ms", "us", or "ns" (integers).

View Source
var UntilFunc = function.New(&function.Spec{
	Description: "How far off a time is: the duration from now until it. Negative if it has passed.",
	Params: []function.Parameter{
		{
			Name:        "t",
			Type:        TimeCapsuleType,
			Description: "The time to measure to.",
		},
	},
	Type: function.StaticReturnType(DurationCapsuleType),
	Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
		t, err := GetTime(args[0])
		if err != nil {
			return cty.NilVal, err
		}
		return NewDurationCapsule(time.Until(t)), nil
	},
})

UntilFunc returns the duration until the given time (equivalent to time::sub(t, time::now())).

Functions

func Externs added in v0.3.0

func Externs() map[string][]byte

Externs returns the functy `//functy:extern` declarations for the functions GetTimeFunctions provides whose real signature their cty metadata cannot express, keyed by a filename to report in diagnostics.

cty can only make a function's *trailing* parameters optional, so an optional argument has to be faked with a variadic — which erases its name, its type, and its default: `time::from_unix(n, unit = "s")` reflects as `from_unix(n, ...args)`. And a cty function has one signature, so a function whose argument shapes differ per call (`time::parse(s)` vs `time::parse(format, s)`) or whose parameters are a union (`dns::parse_zone_serial` takes a number *or* a string) cannot be described at all. These declarations say what each really accepts, so that help(), generated documentation, and editor tooling can show it.

There is one file per namespace, because a functy source declares at most one: `time`, `duration`, `dns`, and a global file for the un-namespaced `duration()` constructor. A host registers them all:

for name, src := range timecty.Externs() {
    parser.RegisterExterns(src, name)
}

The bytes are opaque to this package: it does not import functy, and nothing here parses them. The functions with no declaration are deliberately undeclared — their cty metadata is complete, so an extern would only be a second place for the same facts to drift.

func GetDuration

func GetDuration(val cty.Value) (time.Duration, error)

GetDuration extracts a time.Duration from a cty capsule value. Returns an error if the value is not a DurationCapsuleType.

func GetTime

func GetTime(val cty.Value) (time.Time, error)

GetTime extracts a time.Time from a cty capsule value. Returns an error if the value is not a TimeCapsuleType.

func GetTimeFunctions

func GetTimeFunctions() map[string]function.Function

GetTimeFunctions returns this package's cty functions, keyed by the name they are callable under.

The names are namespaced — `time::add`, `duration::truncate`, `dns::next_zone_serial`. HCL parses `a::b(x)` natively and resolves it as a single flat map key, so a namespace is a naming convention rather than a containment relationship: these are ordinary map entries whose keys happen to contain `::`.

Two rules, both HashiCorp's for provider-defined functions, and both worth stating because they are why the names look the way they do:

  • The leaf name does not repeat the namespace. `time::add`, not `time::timeadd`.
  • Namespaced functions use underscores. HCL's *built-in* functions run words together (`formatdate`, `jsonencode`) for historical reasons; once a namespace does the grouping, the leaf name gets longer and more descriptive, and run-on stops being readable — `next_zone_serial` over `nextzoneserial`.

`duration` keeps a bare, un-namespaced name: it is the type constructor, and reads as one — `duration("1h30m")`, like `tostring(x)`. A bare name and a namespace of the same name coexist without conflict, since they are simply different map keys.

There is deliberately no `timeadd`. That name belongs to cty's own stdlib.TimeAddFunc, the string-in/string-out function HCL has always had; a host that wants it registers it directly. `time::add` carries no compatibility burden as a result, which is why every one of its forms returns a time.

func NewDurationCapsule

func NewDurationCapsule(d time.Duration) cty.Value

NewDurationCapsule wraps a time.Duration in a cty capsule value.

func NewTimeCapsule

func NewTimeCapsule(t time.Time) cty.Value

NewTimeCapsule wraps a time.Time in a cty capsule value.

Types

type Duration added in v0.2.0

type Duration struct {
	time.Duration
}

Duration wraps a time.Duration so it can implement the rich-cty-types Stringable and Gettable interfaces.

func (*Duration) Get added in v0.2.0

func (d *Duration) Get(_ context.Context, args []cty.Value) (cty.Value, error)

Get extracts the duration expressed in the given unit. "h", "m", "s" return floats; "ms", "us", "ns" return integers.

func (*Duration) ToString added in v0.2.0

func (d *Duration) ToString(_ context.Context) (string, error)

ToString formats the duration using Go's default duration syntax (e.g. "1h30m5s"). This matches the default output of duration::format().

type Timestamp added in v0.2.0

type Timestamp struct {
	time.Time
}

Timestamp wraps a time.Time so it can implement the rich-cty-types Stringable and Gettable interfaces. Embedding forwards time.Time's methods (Format, Year, Equal, etc.) so callers can use them directly.

func (*Timestamp) Get added in v0.2.0

func (t *Timestamp) Get(_ context.Context, args []cty.Value) (cty.Value, error)

Get extracts a named calendar field from the timestamp in its stored timezone. Valid parts: year, month, day, hour, minute, second, nanosecond, weekday (0=Sunday), yearday, isoweek, isoyear.

func (*Timestamp) ToString added in v0.2.0

func (t *Timestamp) ToString(_ context.Context) (string, error)

ToString formats the timestamp as RFC3339Nano. This is lossless for times with sub-second precision and identical to RFC3339 for whole-second times.

Jump to

Keyboard shortcuts

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