timecty

package module
v0.2.1 Latest Latest
Warning

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

Go to latest
Published: Apr 18, 2026 License: BSD-2-Clause Imports: 10 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 durationlt/durationgt (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 addyears() / addmonths() 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. The timeadd entry supersedes the go-cty stdlib version, adding capsule-type support while remaining backward-compatible with the (string, string) → string form.

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 formattime("@rfc3339nano", t)).
  • tostring(d) formats a duration using Go syntax (equivalent to formatduration(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)

formattime and parsetime 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
now() () → time Current time in local timezone
now(tz) (string) → time Current time in named IANA timezone
parsetime(s) (string) → time Parse RFC 3339 string
parsetime(format, s) (string, string) → time Parse with Go reference-time format or @name alias
parsetime(format, s, tz) (string, string, string) → time Parse with format; apply IANA timezone
fromunix(n) (number) → time Create time from Unix seconds (integer or fractional) in UTC
fromunix(n, unit) (number, string) → time Unit: "s", "ms", "us", or "ns"
strptime(format, s) (string, string) → time Parse with strftime-style format
strptime(format, s, tz) (string, string, string) → time Parse with strftime format; apply IANA timezone
Timestamp — Formatting
Function Signature Description
formattime(format, t) (string, time) → string Format with Go reference-time format or @name alias
strftime(format, t) (string, time) → string Format with strftime/C-style format
Timestamp — Arithmetic
Function Signature Description
timeadd(t, d) (time, duration) → time Add duration to time (also accepts string forms for backward compat)
timesub(t1, t2) (time, time) → duration Elapsed from t2 to t1; negative if t1 < t2
timesub(t, d) (time, duration) → time Subtract duration from time
since(t) (time) → duration Elapsed since t
until(t) (time) → duration Time remaining until t
addyears(t, n) (time, number) → time Add n calendar years
addmonths(t, n) (time, number) → time Add n calendar months
adddays(t, n) (time, number) → time Add n calendar days
Timestamp — Decomposition
Function Signature Description
unix(t) (time) → number Unix epoch as fractional seconds
unix(t, unit) (time, string) → number Unix epoch in unit: "s" (float), "ms", "us", "ns" (integers)
timezone() () → string System local timezone name
timezone(t) (time) → string Stored timezone name
intimezone(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
timebefore(t1, t2) (time, time) → bool True if t1 is before t2
timeafter(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
formatduration(d) (duration) → string Go format (e.g. "1h30m5s")
formatduration(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
absduration(d) (duration) → duration Absolute value
durationadd(d1, d2) (duration, duration) → duration Sum
durationsub(d1, d2) (duration, duration) → duration Difference
durationmul(d, n) (duration, number) → duration Scale by factor
durationdiv(d, n) (duration, number) → duration Divide by factor
durationtruncate(d, m) (duration, duration) → duration Truncate to multiple of m
durationround(d, m) (duration, duration) → duration Round to nearest multiple of m
durationlt(d1, d2) (duration, duration) → bool True if d1 < d2
durationgt(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
nextzoneserial(s) (number|string) → number Next serial after s, using today's date
nextzoneserial(s, t) (number|string, time) → number Next serial using date from t
parsezoneserial(s) (number|string) → time Parse serial back to approximate date (UTC midnight)

Examples

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

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

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

# Arithmetic
timeadd(now("UTC"), duration("1h30m"))
timesub(end_time, start_time)             # → duration
timesub(deadline, duration(30, "m"))      # → time

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

# Comparison
durationgt(since(last_seen), duration(24, "h"))
timebefore(expires_at, now("UTC"))

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

# Unix interop
fromunix(epoch_seconds)
fromunix(epoch_ms, "ms")
unix(now("UTC"), "ns")

# Calendar
addmonths(now("UTC"), 3)
adddays(now("UTC"), -7)

# DNS zone serials
nextzoneserial(2026012300)                # → 2026012301
nextzoneserial(old_serial, now("UTC"))    # → next serial for today
parsezoneserial(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{
	Params: []function.Parameter{
		{Name: "d", Type: DurationCapsuleType},
	},
	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{
	Params: []function.Parameter{
		{Name: "t", Type: TimeCapsuleType},
		{Name: "n", Type: cty.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{
	Params: []function.Parameter{
		{Name: "t", Type: TimeCapsuleType},
		{Name: "n", Type: cty.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{
	Params: []function.Parameter{
		{Name: "t", Type: TimeCapsuleType},
		{Name: "n", Type: cty.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{
	Params: []function.Parameter{
		{Name: "d1", Type: DurationCapsuleType},
		{Name: "d2", Type: DurationCapsuleType},
	},
	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{
	Params: []function.Parameter{
		{Name: "d", Type: DurationCapsuleType},
		{Name: "n", Type: cty.Number},
	},
	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("durationdiv: 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{
	Params: []function.Parameter{
		{Name: "val", Type: cty.DynamicPseudoType},
	},
	VarParam: &function.Parameter{
		Name: "unit",
		Type: cty.DynamicPseudoType,
	},
	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{
	Params: []function.Parameter{
		{Name: "d1", Type: DurationCapsuleType},
		{Name: "d2", Type: DurationCapsuleType},
	},
	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{
	Params: []function.Parameter{
		{Name: "d1", Type: DurationCapsuleType},
		{Name: "d2", Type: DurationCapsuleType},
	},
	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{
	Params: []function.Parameter{
		{Name: "d", Type: DurationCapsuleType},
		{Name: "n", Type: cty.Number},
	},
	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{
	Params: []function.Parameter{
		{Name: "d", Type: DurationCapsuleType},
		{Name: "m", Type: DurationCapsuleType},
	},
	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{
	Params: []function.Parameter{
		{Name: "d1", Type: DurationCapsuleType},
		{Name: "d2", Type: DurationCapsuleType},
	},
	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{
	Params: []function.Parameter{
		{Name: "d", Type: DurationCapsuleType},
		{Name: "m", Type: DurationCapsuleType},
	},
	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{
	Params: []function.Parameter{
		{Name: "d", Type: DurationCapsuleType},
	},
	VarParam: &function.Parameter{
		Name: "fmt",
		Type: cty.String,
	},
	Type: function.StaticReturnType(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("formatduration: unknown format %q; valid values are \"go\" and \"iso\"", format)
		}
	},
})

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

View Source
var FormatTimeFunc = function.New(&function.Spec{
	Params: []function.Parameter{
		{Name: "format", Type: cty.String},
		{Name: "t", Type: TimeCapsuleType},
	},
	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 formattime("2006-01-02", t) or formattime("@rfc3339", t).

View Source
var FromUnixFunc = function.New(&function.Spec{
	Params: []function.Parameter{
		{Name: "n", Type: cty.Number},
	},
	VarParam: &function.Parameter{
		Name: "unit",
		Type: cty.String,
	},
	Type: function.StaticReturnType(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("fromunix: unknown unit %q; valid units: s, ms, us, ns", unit)
		}
	},
})

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

View Source
var InTimezoneFunc = function.New(&function.Spec{
	Params: []function.Parameter{
		{Name: "t", Type: TimeCapsuleType},
		{Name: "tz", Type: cty.String},
	},
	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("intimezone: 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{
	Params: []function.Parameter{
		{Name: "s", Type: cty.DynamicPseudoType},
	},
	VarParam: &function.Parameter{
		Name: "t",
		Type: cty.DynamicPseudoType,
	},
	Type: func(args []cty.Value) (cty.Type, error) {
		if len(args) > 2 {
			return cty.NilType, fmt.Errorf("nextzoneserial() takes 1 or 2 arguments")
		}
		t0 := args[0].Type()
		if t0 != cty.Number && t0 != cty.String && t0 != cty.DynamicPseudoType {
			return cty.NilType, fmt.Errorf("nextzoneserial: serial must be a number or string, got %s", t0.FriendlyName())
		}
		if len(args) == 2 {
			t1 := args[1].Type()
			if t1 != TimeCapsuleType && t1 != cty.DynamicPseudoType {
				return cty.NilType, fmt.Errorf("nextzoneserial: second argument must be a time value, got %s", t1.FriendlyName())
			}
		}
		return cty.Number, nil
	},
	Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
		s, err := parseSerialArg(args[0], "nextzoneserial")
		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 nextzoneserial(s) or nextzoneserial(s, t).

s: current serial (number or string)
t: optional time capsule; defaults to 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{
	VarParam: &function.Parameter{
		Name: "tz",
		Type: cty.String,
	},
	Type: function.StaticReturnType(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 now() or now("America/New_York").

View Source
var ParseTimeFunc = function.New(&function.Spec{
	VarParam: &function.Parameter{Name: "args", Type: cty.String},
	Type: func(args []cty.Value) (cty.Type, error) {
		if len(args) < 1 || len(args) > 3 {
			return cty.NilType, fmt.Errorf("parsetime() 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("parsetime: 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("parsetime: 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("parsetime: 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("parsetime: cannot parse %q with format %q: %s", args[1].AsString(), args[0].AsString(), err)
			}
			return NewTimeCapsule(t), nil
		default:
			return cty.NilVal, fmt.Errorf("parsetime() takes 1 to 3 arguments")
		}
	},
})

ParseTimeFunc parses a timestamp string into a time value.

Forms:

parsetime(s)              — RFC 3339 (timezone required)
parsetime(format, s)      — parse s using Go layout (or @name alias)
parsetime(format, s, tz)  — same, but interpret s in the given IANA timezone
View Source
var ParseZoneSerialFunc = function.New(&function.Spec{
	Params: []function.Parameter{
		{Name: "s", Type: cty.DynamicPseudoType},
	},
	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("parsezoneserial: 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], "parsezoneserial")
		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{
	Params: []function.Parameter{
		{Name: "t", Type: TimeCapsuleType},
	},
	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 timesub(now(), t)).

View Source
var StrftimeFunc = function.New(&function.Spec{
	Params: []function.Parameter{
		{Name: "format", Type: cty.String},
		{Name: "t", Type: TimeCapsuleType},
	},
	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 strftime("%Y-%m-%d", t).

View Source
var StrptimeFunc = function.New(&function.Spec{
	Params: []function.Parameter{
		{Name: "format", Type: cty.String},
		{Name: "s", Type: cty.String},
	},
	VarParam: &function.Parameter{Name: "tz", Type: cty.String},
	Type: func(args []cty.Value) (cty.Type, error) {
		if len(args) > 3 {
			return cty.NilType, fmt.Errorf("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("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("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 strptime("%Y-%m-%d", "2024-01-15") or strptime("%Y-%m-%d", "2024-01-15", "UTC").

View Source
var TimeAddFunc = function.New(&function.Spec{
	Params: []function.Parameter{
		{Name: "ts", Type: cty.DynamicPseudoType},
		{Name: "dur", Type: cty.DynamicPseudoType},
	},
	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
		}

		if t0 == cty.String && t1 == cty.String {
			return cty.String, nil
		}

		validTS := t0 == TimeCapsuleType || t0 == cty.String
		validDur := t1 == DurationCapsuleType || t1 == cty.String
		if validTS && validDur {
			return TimeCapsuleType, nil
		}
		return cty.NilType, fmt.Errorf("timeadd: unsupported argument types %s and %s", t0.FriendlyName(), t1.FriendlyName())
	},
	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {

		if args[0].Type() == cty.String && args[1].Type() == cty.String {
			ts, err := time.Parse(time.RFC3339, args[0].AsString())
			if err != nil {
				return cty.NilVal, fmt.Errorf("timeadd: invalid timestamp %q: %s", args[0].AsString(), err)
			}
			dur, err := time.ParseDuration(args[1].AsString())
			if err != nil {
				return cty.NilVal, fmt.Errorf("timeadd: invalid duration %q: %s", args[1].AsString(), err)
			}
			return cty.StringVal(ts.Add(dur).Format(time.RFC3339)), nil
		}

		// Get the time value
		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("timeadd: 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("timeadd: first argument must be a time or string, got %s", args[0].Type().FriendlyName())
		}

		// Get the duration value
		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("timeadd: 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. Backward-compatible with the stdlib timeadd(string, string) form; also accepts capsule types.

Signatures:

timeadd(string, string) → string   (standard hcl behavior)
timeadd(time, duration) → time
timeadd(time, string)   → time     (string auto-parsed as duration)
timeadd(string, duration) → time   (string auto-parsed as RFC 3339)
View Source
var TimeAfterFunc = function.New(&function.Spec{
	Params: []function.Parameter{
		{Name: "t1", Type: TimeCapsuleType},
		{Name: "t2", Type: TimeCapsuleType},
	},
	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{
	Params: []function.Parameter{
		{Name: "t1", Type: TimeCapsuleType},
		{Name: "t2", Type: TimeCapsuleType},
	},
	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 timesub() and compare the resulting duration instead.

View Source
var TimeSubFunc = function.New(&function.Spec{
	Params: []function.Parameter{
		{Name: "t1", Type: cty.DynamicPseudoType},
		{Name: "t2", Type: cty.DynamicPseudoType},
	},
	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
		}
		if t0 != TimeCapsuleType {
			return cty.NilType, fmt.Errorf("timesub: first argument must be a time, got %s", t0.FriendlyName())
		}
		switch t1 {
		case TimeCapsuleType:
			return DurationCapsuleType, nil
		case DurationCapsuleType:
			return TimeCapsuleType, nil
		default:
			return cty.NilType, fmt.Errorf("timesub: 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("timesub: second argument must be a time or duration, got %s", args[1].Type().FriendlyName())
		}
	},
})

TimeSubFunc subtracts a time or duration from a time.

Signatures:

timesub(time, time)     → duration   (elapsed from t2 to t1; negative if t1 < t2)
timesub(time, duration) → time       (time minus duration)
View Source
var TimezoneFunc = function.New(&function.Spec{
	VarParam: &function.Parameter{
		Name: "t",
		Type: cty.DynamicPseudoType,
	},
	Type: func(args []cty.Value) (cty.Type, error) {
		if len(args) > 1 {
			return cty.NilType, fmt.Errorf("timezone() takes 0 or 1 arguments")
		}
		if len(args) == 1 {
			t := args[0].Type()
			if t != TimeCapsuleType && t != cty.DynamicPseudoType {
				return cty.NilType, fmt.Errorf("timezone: argument must be a time value, got %s", t.FriendlyName())
			}
		}
		return cty.String, nil
	},
	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 timezone() for the local system timezone, or timezone(t) for the timezone stored in a time value.

View Source
var UnixFunc = function.New(&function.Spec{
	Params: []function.Parameter{
		{Name: "t", Type: TimeCapsuleType},
	},
	VarParam: &function.Parameter{
		Name: "unit",
		Type: cty.String,
	},
	Type: function.StaticReturnType(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("unix: unknown unit %q; valid units: s, ms, us, ns", unit)
		}
	},
})

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

View Source
var UntilFunc = function.New(&function.Spec{
	Params: []function.Parameter{
		{Name: "t", Type: TimeCapsuleType},
	},
	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 timesub(t, now())).

Functions

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 all time-related cty functions for registration in an eval context. The "timeadd" entry supersedes the stdlib version, adding capsule-type support while remaining backward-compatible with the original (string, string) form.

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 formatduration().

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