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)
}
})
}