pkg/time: define time types and builtins

based on Go package.

These result in more accurate parsing and make
it easier to track the time type.

Change-Id: Iff75afc905dd9d0af58d1ea76db92a9d351a5d50
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2720
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/builtin_test.go b/cue/builtin_test.go
index 3248f5d..ca22a55 100644
--- a/cue/builtin_test.go
+++ b/cue/builtin_test.go
@@ -299,6 +299,12 @@
 	}, {
 		test("text/template", `template.Execute("{{.}}-{{.}}", "foo")`),
 		`"foo-foo"`,
+	}, {
+		test("time", `time.Time & "1937-01-01T12:00:27.87+00:20"`),
+		`"1937-01-01T12:00:27.87+00:20"`,
+	}, {
+		test("time", `time.Time & "no time"`),
+		`_|_(error in call to time.Time: invalid time "no time")`,
 	}}
 	for _, tc := range testCases {
 		t.Run("", func(t *testing.T) {
diff --git a/cue/builtins.go b/cue/builtins.go
index db27401..d1d288e 100644
--- a/cue/builtins.go
+++ b/cue/builtins.go
@@ -26,6 +26,7 @@
 	"strings"
 	"text/tabwriter"
 	"text/template"
+	"time"
 	"unicode"
 	"unicode/utf8"
 
@@ -118,6 +119,15 @@
 
 var pathDir = path.Dir
 
+func timeFormat(value, layout string) (bool, error) {
+	_, err := time.Parse(layout, value)
+	if err != nil {
+
+		return false, fmt.Errorf("invalid time %q", value)
+	}
+	return true, nil
+}
+
 var builtinPackages = map[string]*builtinPkg{
 	"": &builtinPkg{
 		native: []*builtin{{}},
@@ -2362,10 +2372,193 @@
 		}},
 	},
 	"time": &builtinPkg{
-		native: []*builtin{{}},
-		cue: `{
-	Time: null | =~"^\("\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2]\\d|3[0-1])")T\("([0-1]\\d|2[0-3]):[0-5]\\d:[0-5]\\d")\("(.\\d{1,10})?")\("(Z|(-|\\+)\\d\\d:\\d\\d)")$"
-}`,
+		native: []*builtin{{
+			Name:  "Nanosecond",
+			Const: "1",
+		}, {
+			Name:  "Microsecond",
+			Const: "1000",
+		}, {
+			Name:  "Millisecond",
+			Const: "1000000",
+		}, {
+			Name:  "Second",
+			Const: "1000000000",
+		}, {
+			Name:  "Minute",
+			Const: "60000000000",
+		}, {
+			Name:  "Hour",
+			Const: "3600000000000",
+		}, {
+			Name:   "Duration",
+			Params: []kind{stringKind},
+			Result: boolKind,
+			Func: func(c *callCtxt) {
+				s := c.string(0)
+				c.ret, c.err = func() (interface{}, error) {
+					if _, err := time.ParseDuration(s); err != nil {
+						return false, err
+					}
+					return true, nil
+				}()
+			},
+		}, {
+			Name:   "ParseDuration",
+			Params: []kind{stringKind},
+			Result: intKind,
+			Func: func(c *callCtxt) {
+				s := c.string(0)
+				c.ret, c.err = func() (interface{}, error) {
+					d, err := time.ParseDuration(s)
+					if err != nil {
+						return 0, err
+					}
+					return int64(d), nil
+				}()
+			},
+		}, {
+			Name:  "ANSIC",
+			Const: "\"Mon Jan _2 15:04:05 2006\"",
+		}, {
+			Name:  "UnixDate",
+			Const: "\"Mon Jan _2 15:04:05 MST 2006\"",
+		}, {
+			Name:  "RubyDate",
+			Const: "\"Mon Jan 02 15:04:05 -0700 2006\"",
+		}, {
+			Name:  "RFC822",
+			Const: "\"02 Jan 06 15:04 MST\"",
+		}, {
+			Name:  "RFC822Z",
+			Const: "\"02 Jan 06 15:04 -0700\"",
+		}, {
+			Name:  "RFC850",
+			Const: "\"Monday, 02-Jan-06 15:04:05 MST\"",
+		}, {
+			Name:  "RFC1123",
+			Const: "\"Mon, 02 Jan 2006 15:04:05 MST\"",
+		}, {
+			Name:  "RFC1123Z",
+			Const: "\"Mon, 02 Jan 2006 15:04:05 -0700\"",
+		}, {
+			Name:  "RFC3339",
+			Const: "\"2006-01-02T15:04:05Z07:00\"",
+		}, {
+			Name:  "RFC3339Nano",
+			Const: "\"2006-01-02T15:04:05.999999999Z07:00\"",
+		}, {
+			Name:  "RFC3339Date",
+			Const: "\"2006-01-02\"",
+		}, {
+			Name:  "Kitchen",
+			Const: "\"3:04PM\"",
+		}, {
+			Name:  "Kitchen24",
+			Const: "\"15:04\"",
+		}, {
+			Name:  "January",
+			Const: "1",
+		}, {
+			Name:  "February",
+			Const: "2",
+		}, {
+			Name:  "March",
+			Const: "3",
+		}, {
+			Name:  "April",
+			Const: "4",
+		}, {
+			Name:  "May",
+			Const: "5",
+		}, {
+			Name:  "June",
+			Const: "6",
+		}, {
+			Name:  "July",
+			Const: "7",
+		}, {
+			Name:  "August",
+			Const: "8",
+		}, {
+			Name:  "September",
+			Const: "9",
+		}, {
+			Name:  "October",
+			Const: "10",
+		}, {
+			Name:  "November",
+			Const: "11",
+		}, {
+			Name:  "December",
+			Const: "12",
+		}, {
+			Name:  "Sunday",
+			Const: "0",
+		}, {
+			Name:  "Monday",
+			Const: "1",
+		}, {
+			Name:  "Tuesday",
+			Const: "2",
+		}, {
+			Name:  "Wednesday",
+			Const: "3",
+		}, {
+			Name:  "Thursday",
+			Const: "4",
+		}, {
+			Name:  "Friday",
+			Const: "5",
+		}, {
+			Name:  "Saturday",
+			Const: "6",
+		}, {
+			Name:   "Time",
+			Params: []kind{stringKind},
+			Result: boolKind,
+			Func: func(c *callCtxt) {
+				s := c.string(0)
+				c.ret, c.err = func() (interface{}, error) {
+					return timeFormat(s, time.RFC3339Nano)
+				}()
+			},
+		}, {
+			Name:   "Format",
+			Params: []kind{stringKind, stringKind},
+			Result: boolKind,
+			Func: func(c *callCtxt) {
+				value, layout := c.string(0), c.string(1)
+				c.ret, c.err = func() (interface{}, error) {
+					return timeFormat(value, layout)
+				}()
+			},
+		}, {
+			Name:   "Parse",
+			Params: []kind{stringKind, stringKind},
+			Result: stringKind,
+			Func: func(c *callCtxt) {
+				layout, value := c.string(0), c.string(1)
+				c.ret, c.err = func() (interface{}, error) {
+					t, err := time.Parse(layout, value)
+					if err != nil {
+						return "", err
+					}
+					return t.Format(time.RFC3339Nano), nil
+				}()
+			},
+		}, {
+			Name:   "Unix",
+			Params: []kind{intKind, intKind},
+			Result: stringKind,
+			Func: func(c *callCtxt) {
+				sec, nsec := c.int64(0), c.int64(1)
+				c.ret = func() interface{} {
+					t := time.Unix(sec, nsec)
+					return t.Format(time.RFC3339Nano)
+				}()
+			},
+		}},
 	},
 	"tool": &builtinPkg{
 		native: []*builtin{{}},
diff --git a/pkg/time/duration.go b/pkg/time/duration.go
new file mode 100644
index 0000000..3146937
--- /dev/null
+++ b/pkg/time/duration.go
@@ -0,0 +1,65 @@
+// Copyright 2019 CUE Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package time
+
+import (
+	"time"
+)
+
+// Common durations. There is no definition for units of Day or larger
+// to avoid confusion across daylight savings time zone transitions.
+//
+// To count the number of units in a Duration, divide:
+//	second := time.Second
+//	fmt.Print(int64(second/time.Millisecond)) // prints 1000
+//
+// To convert an integer number of units to a Duration, multiply:
+//	seconds := 10
+//	fmt.Print(time.Duration(seconds)*time.Second) // prints 10s
+//
+const (
+	Nanosecond  = 1
+	Microsecond = 1000
+	Millisecond = 1000000
+	Second      = 1000000000
+	Minute      = 60000000000
+	Hour        = 3600000000000
+)
+
+// Duration validates a duration string.
+//
+// Note: this format also accepts strings of the form '1h3m', '2ms', etc.
+// To limit this to seconds only, as often used in JSON, add the !~"hmuµn"
+// constraint.
+func Duration(s string) (bool, error) {
+	if _, err := time.ParseDuration(s); err != nil {
+		return false, err
+	}
+	return true, nil
+}
+
+// ParseDuration reports the nanoseconds represented by a duration string.
+//
+// A duration string is a possibly signed sequence of
+// decimal numbers, each with optional fraction and a unit suffix,
+// such as "300ms", "-1.5h" or "2h45m".
+// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
+func ParseDuration(s string) (int64, error) {
+	d, err := time.ParseDuration(s)
+	if err != nil {
+		return 0, err
+	}
+	return int64(d), nil
+}
diff --git a/pkg/time/duration_test.go b/pkg/time/duration_test.go
new file mode 100644
index 0000000..7b0d5f1
--- /dev/null
+++ b/pkg/time/duration_test.go
@@ -0,0 +1,73 @@
+// Copyright 2019 CUE Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package time
+
+import (
+	"testing"
+)
+
+func TestDuration(t *testing.T) {
+	valid := []string{
+		"1.0s",
+		"1000.0s",
+		"1000.000001s",
+		".000001s",
+		"4h2m",
+	}
+
+	for _, tc := range valid {
+		t.Run(tc, func(t *testing.T) {
+			if b, err := Duration(tc); !b || err != nil {
+				t.Errorf("CUE eval failed unexpectedly: %v", err)
+			}
+		})
+	}
+
+	invalid := []string{
+		"5d2h",
+	}
+
+	for _, tc := range invalid {
+		t.Run(tc, func(t *testing.T) {
+			if _, err := Duration(tc); err == nil {
+				t.Errorf("CUE eval succeeded unexpectedly")
+			}
+		})
+	}
+}
+
+func TestParseDuration(t *testing.T) {
+	valid := []struct {
+		in  string
+		out int64
+		err bool
+	}{
+		{"3h2m", 3*Hour + 2*Minute, false},
+		{"5s", 5 * Second, false},
+		{"5d", 0, true},
+	}
+
+	for _, tc := range valid {
+		t.Run(tc.in, func(t *testing.T) {
+			i, err := ParseDuration(tc.in)
+			if got := err != nil; got != tc.err {
+				t.Fatalf("error: got %v; want %v", i, tc.out)
+			}
+			if i != tc.out {
+				t.Errorf("got %v; want %v", i, tc.out)
+			}
+		})
+	}
+}
diff --git a/pkg/time/time.cue b/pkg/time/time.cue
deleted file mode 100644
index 0c69123..0000000
--- a/pkg/time/time.cue
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright 2019 CUE Authors
-// 
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// 
-//     http://www.apache.org/licenses/LICENSE-2.0
-// 
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-// Package time provides functionality for representing and displaying time.
-//
-// The calendrical calculations always assume a Gregorian calendar, with no leap seconds.
-package time
-
-// A Time represents an instant in time with nanosecond precision as
-// an RFC 3339 string.
-//
-// A time represented in this format can be marshaled into and from
-// a Go time.Time. This means it does not allow the representation of leap
-// seconds.
-Time: null | =~"^\(date)T\(time)\(nano)\(zone)$"
-
-date = #"\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2]\d|3[0-1])"#
-time = #"([0-1]\d|2[0-3]):[0-5]\d:[0-5]\d"#
-nano = #"(.\d{1,10})?"# // Go parses up to 10 digits.
-zone = #"(Z|(-|\+)\d\d:\d\d)"#
-
-// TODO: correctly constrain days and then leap years/centuries.
diff --git a/pkg/time/time.go b/pkg/time/time.go
new file mode 100644
index 0000000..eadcaa8
--- /dev/null
+++ b/pkg/time/time.go
@@ -0,0 +1,203 @@
+// Copyright 2019 CUE Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package time defines time-related types.
+package time
+
+import (
+	"fmt"
+	"time"
+)
+
+// These are predefined layouts for use in Time.Format and time.Parse.
+// The reference time used in the layouts is the specific time:
+//	Mon Jan 2 15:04:05 MST 2006
+// which is Unix time 1136239445. Since MST is GMT-0700,
+// the reference time can be thought of as
+//	01/02 03:04:05PM '06 -0700
+// To define your own format, write down what the reference time would look
+// like formatted your way; see the values of constants like ANSIC,
+// StampMicro or Kitchen for examples. The model is to demonstrate what the
+// reference time looks like so that the Format and Parse methods can apply
+// the same transformation to a general time value.
+//
+// Some valid layouts are invalid time values for time.Parse, due to formats
+// such as _ for space padding and Z for zone information.
+//
+// Within the format string, an underscore _ represents a space that may be
+// replaced by a digit if the following number (a day) has two digits; for
+// compatibility with fixed-width Unix time formats.
+//
+// A decimal point followed by one or more zeros represents a fractional
+// second, printed to the given number of decimal places. A decimal point
+// followed by one or more nines represents a fractional second, printed to
+// the given number of decimal places, with trailing zeros removed.
+// When parsing (only), the input may contain a fractional second
+// field immediately after the seconds field, even if the layout does not
+// signify its presence. In that case a decimal point followed by a maximal
+// series of digits is parsed as a fractional second.
+//
+// Numeric time zone offsets format as follows:
+//	-0700  ±hhmm
+//	-07:00 ±hh:mm
+//	-07    ±hh
+// Replacing the sign in the format with a Z triggers
+// the ISO 8601 behavior of printing Z instead of an
+// offset for the UTC zone. Thus:
+//	Z0700  Z or ±hhmm
+//	Z07:00 Z or ±hh:mm
+//	Z07    Z or ±hh
+//
+// The recognized day of week formats are "Mon" and "Monday".
+// The recognized month formats are "Jan" and "January".
+//
+// Text in the format string that is not recognized as part of the reference
+// time is echoed verbatim during Format and expected to appear verbatim
+// in the input to Parse.
+//
+// The executable example for Time.Format demonstrates the working
+// of the layout string in detail and is a good reference.
+//
+// Note that the RFC822, RFC850, and RFC1123 formats should be applied
+// only to local times. Applying them to UTC times will use "UTC" as the
+// time zone abbreviation, while strictly speaking those RFCs require the
+// use of "GMT" in that case.
+// In general RFC1123Z should be used instead of RFC1123 for servers
+// that insist on that format, and RFC3339 should be preferred for new protocols.
+// RFC3339, RFC822, RFC822Z, RFC1123, and RFC1123Z are useful for formatting;
+// when used with time.Parse they do not accept all the time formats
+// permitted by the RFCs.
+// The RFC3339Nano format removes trailing zeros from the seconds field
+// and thus may not sort correctly once formatted.
+const (
+	ANSIC       = "Mon Jan _2 15:04:05 2006"
+	UnixDate    = "Mon Jan _2 15:04:05 MST 2006"
+	RubyDate    = "Mon Jan 02 15:04:05 -0700 2006"
+	RFC822      = "02 Jan 06 15:04 MST"
+	RFC822Z     = "02 Jan 06 15:04 -0700" // RFC822 with numeric zone
+	RFC850      = "Monday, 02-Jan-06 15:04:05 MST"
+	RFC1123     = "Mon, 02 Jan 2006 15:04:05 MST"
+	RFC1123Z    = "Mon, 02 Jan 2006 15:04:05 -0700" // RFC1123 with numeric zone
+	RFC3339     = "2006-01-02T15:04:05Z07:00"
+	RFC3339Nano = "2006-01-02T15:04:05.999999999Z07:00"
+	RFC3339Date = "2006-01-02"
+	Kitchen     = "3:04PM"
+	Kitchen24   = "15:04"
+)
+
+const (
+	January   = 1
+	February  = 2
+	March     = 3
+	April     = 4
+	May       = 5
+	June      = 6
+	July      = 7
+	August    = 8
+	September = 9
+	October   = 10
+	November  = 11
+	December  = 12
+)
+
+const (
+	Sunday    = 0
+	Monday    = 1
+	Tuesday   = 2
+	Wednesday = 3
+	Thursday  = 4
+	Friday    = 5
+	Saturday  = 6
+)
+
+// Time validates a RFC3339 date-time.
+//
+// Caveat: this implementation uses the Go implementation, which does not
+// accept leap seconds.
+func Time(s string) (bool, error) {
+	return timeFormat(s, time.RFC3339Nano)
+}
+
+func timeFormat(value, layout string) (bool, error) {
+	_, err := time.Parse(layout, value)
+	if err != nil {
+		// Use our own error, the time package's error as the Go error is too
+		// confusing within this context.
+		return false, fmt.Errorf("invalid time %q", value)
+	}
+	return true, nil
+}
+
+// Format defines a type string that must adhere to a certain layout.
+//
+// See Parse for a description on layout strings.
+func Format(value, layout string) (bool, error) {
+	return timeFormat(value, layout)
+}
+
+// Parse parses a formatted string and returns the time value it represents.
+// The layout defines the format by showing how the reference time,
+// defined to be
+//	Mon Jan 2 15:04:05 -0700 MST 2006
+// would be interpreted if it were the value; it serves as an example of
+// the input format. The same interpretation will then be made to the
+// input string.
+//
+// Predefined layouts ANSIC, UnixDate, RFC3339 and others describe standard
+// and convenient representations of the reference time. For more information
+// about the formats and the definition of the reference time, see the
+// documentation for ANSIC and the other constants defined by this package.
+// Also, the executable example for Time.Format demonstrates the working
+// of the layout string in detail and is a good reference.
+//
+// Elements omitted from the value are assumed to be zero or, when
+// zero is impossible, one, so parsing "3:04pm" returns the time
+// corresponding to Jan 1, year 0, 15:04:00 UTC (note that because the year is
+// 0, this time is before the zero Time).
+// Years must be in the range 0000..9999. The day of the week is checked
+// for syntax but it is otherwise ignored.
+//
+// In the absence of a time zone indicator, Parse returns a time in UTC.
+//
+// When parsing a time with a zone offset like -0700, if the offset corresponds
+// to a time zone used by the current location (Local), then Parse uses that
+// location and zone in the returned time. Otherwise it records the time as
+// being in a fabricated location with time fixed at the given zone offset.
+//
+// When parsing a time with a zone abbreviation like MST, if the zone abbreviation
+// has a defined offset in the current location, then that offset is used.
+// The zone abbreviation "UTC" is recognized as UTC regardless of location.
+// If the zone abbreviation is unknown, Parse records the time as being
+// in a fabricated location with the given zone abbreviation and a zero offset.
+// This choice means that such a time can be parsed and reformatted with the
+// same layout losslessly, but the exact instant used in the representation will
+// differ by the actual zone offset. To avoid such problems, prefer time layouts
+// that use a numeric zone offset, or use ParseInLocation.
+func Parse(layout, value string) (string, error) {
+	t, err := time.Parse(layout, value)
+	if err != nil {
+		return "", err
+	}
+	return t.Format(time.RFC3339Nano), nil
+}
+
+// Unix returns the local Time corresponding to the given Unix time,
+// sec seconds and nsec nanoseconds since January 1, 1970 UTC.
+// It is valid to pass nsec outside the range [0, 999999999].
+// Not all sec values have a corresponding time value. One such
+// value is 1<<63-1 (the largest int64 value).
+func Unix(sec int64, nsec int64) string {
+	t := time.Unix(sec, nsec)
+	return t.Format(time.RFC3339Nano)
+}
diff --git a/pkg/time/time_test.go b/pkg/time/time_test.go
index 5955446..0386c66 100644
--- a/pkg/time/time_test.go
+++ b/pkg/time/time_test.go
@@ -16,29 +16,12 @@
 
 import (
 	"encoding/json"
+	"strconv"
 	"testing"
 	"time"
-
-	"cuelang.org/go/cue"
-	"cuelang.org/go/cue/load"
-	"cuelang.org/go/cue/parser"
 )
 
-func TestTime(t *testing.T) {
-	inst := cue.Build(load.Instances([]string{"."}, nil))[0]
-	if inst.Err != nil {
-		t.Fatal(inst.Err)
-	}
-
-	parseCUE := func(t *testing.T, time string) error {
-		expr, err := parser.ParseExpr("test", "Time&"+time)
-		if err != nil {
-			t.Fatal(err)
-		}
-		v := inst.Eval(expr)
-		return v.Err()
-	}
-
+func TestTimestamp(t *testing.T) {
 	// Valid go times (for JSON marshaling) are represented as is
 	validTimes := []string{
 		// valid Go times
@@ -47,7 +30,7 @@
 		`"2019-01-02T15:04:05-08:00"`,
 		`"2019-01-02T15:04:05.0-08:00"`,
 		`"2019-01-02T15:04:05.01-08:00"`,
-		`"2019-01-02T15:04:05.0123456789-08:00"`, // Is this a Go bug?
+		`"2019-01-02T15:04:05.012345678-08:00"`,
 		`"2019-02-28T15:04:59Z"`,
 
 		// TODO: allow leap seconds? This is allowed by the RFC 3339 spec.
@@ -63,8 +46,16 @@
 				t.Errorf("unmarshal JSON failed unexpectedly: %v", err)
 			}
 
-			if err := parseCUE(t, tc); err != nil {
-				t.Errorf("CUE eval failed unexpectedly: %v", err)
+			if tc == "null" {
+				return
+			}
+			str, _ := strconv.Unquote(tc)
+
+			if b, err := Time(str); !b || err != nil {
+				t.Errorf("Time failed unexpectedly: %v", err)
+			}
+			if _, err := Parse(RFC3339Nano, str); err != nil {
+				t.Errorf("Parse failed unexpectedly")
 			}
 		})
 	}
@@ -92,8 +83,34 @@
 				t.Errorf("unmarshal JSON succeeded unexpectedly: %v", err)
 			}
 
-			if err := parseCUE(t, tc); err == nil {
-				t.Errorf("CUE eval succeeded unexpectedly: %v", err)
+			str, _ := strconv.Unquote(tc)
+
+			if _, err := Time(str); err == nil {
+				t.Errorf("CUE eval succeeded unexpectedly")
+			}
+
+			if _, err := Parse(RFC3339Nano, str); err == nil {
+				t.Errorf("CUE eval succeeded unexpectedly")
+			}
+		})
+	}
+}
+
+func TestUnix(t *testing.T) {
+	valid := []struct {
+		sec  int64
+		nano int64
+		want string
+	}{
+		{0, 0, "1970-01-01T01:00:00+01:00"},
+		{1500000000, 123456, "2017-07-14T04:40:00.000123456+02:00"},
+	}
+
+	for _, tc := range valid {
+		t.Run(tc.want, func(t *testing.T) {
+			got := Unix(tc.sec, tc.nano)
+			if got != tc.want {
+				t.Errorf("got %v; want %s", got, tc.want)
 			}
 		})
 	}