Repository: fxamacker/cbor (go)

Issue:
feature: Add TimeRFC3339NanoUTC option for encoding to CBOR time (tag 0)
This feature was suggested by @ssanderson at:
- https://github.com/fxamacker/cbor/issues/656#issuecomment-2847684876

Currently, we have `TimeRFC3339Nano` option for encoding to CBOR time (tag 0) with nanosecond precision.  However, it retains the timezone of time.Time being encoded and it can be inconvenient to set `time.Time` to UTC before encoding it to CBOR.

### RFC 8949 and Time

RFC 8949 defines CBOR tag 0 and tag 1 for encoding time using integer, floating point, or string value.

Unfortunately, RFC 8949 does not define an integer representation of time with sub-second precision.

This library provides `TimeUnixMicro` to encode to floating point (CBOR tag 1).

Since floating point is a lossy format, adding `TimeUnixNano` can lead to flakier round-trip tests, etc.

### System Time and Time Function Accuracy

Using `TimeUnix` uses 1-second precision, saves space, and is faster to encode. Also, avoids creating unrealistic expectations about accuracy of system time.

System time is sometimes not as accurate as we imagine (due to OS, running inside vm, etc.). For example, see:

- https://learn.microsoft.com/en-us/troubleshoot/windows-server/active-directory/support-boundary-high-accuracy-time

  > Time accuracy in Windows 10 and Windows Server 2016 has been substantially improved, while maintaining full backwards NTP compatibility with older Windows versions. Under the right operating conditions, systems running Windows 10, Windows Server 2016, and newer releases can deliver 1 second (s), 50 milliseconds (ms), or 1 ms accuracy.

Also, some time-related functions might use cached time values or are affected by the OS scheduler, etc.  For example, see this [old Go issue](https://github.com/golang/go/issues/14183#issuecomment-178741222) caused by OpenBSD's `nanosleep()` & scheduler.

Pull request:
Add TimeMode encoding option TimeRFC3339NanoUTC
Closes #687

This PR adds `TimeMode` encoding option `TimeRFC3339NanoUTC` to encode time.Time to CBOR time (tag 0) with a text string content representing UTC time with nanosecond precision in RFC3339 format.

NOTE: If fractional seconds are not required, using `TimeUnix` (the default option for `TimeMode`) is recommended because it is more efficient (integer vs string).

While at it, also refactored some existing tests and added more tests.

Thanks @ssanderson for suggesting this feature at:
- [fxamacker/cbor#656#comment-2847684876](https://github.com/fxamacker/cbor/issues/656#issuecomment-2847684876)

Patch:
diff --git a/encode.go b/encode.go
index c550617..880b851 100644
--- a/encode.go
+++ b/encode.go
@@ -343,7 +343,7 @@ const (
 	// non-UTC timezone then a "localtime - UTC" numeric offset will be included as specified in RFC3339.
 	// NOTE: User applications can avoid including the RFC3339 numeric offset by:
 	// - providing a time.Time value set to UTC, or
-	// - using the TimeUnix, TimeUnixMicro, or TimeUnixDynamic option instead of TimeRFC3339.
+	// - using the TimeUnix, TimeUnixMicro, TimeUnixDynamic, or TimeRFC3339NanoUTC option.
 	TimeRFC3339
 
 	// TimeRFC3339Nano causes time.Time to encode to a CBOR time (tag 0) with a text string content
@@ -351,9 +351,13 @@ const (
 	// non-UTC timezone then a "localtime - UTC" numeric offset will be included as specified in RFC3339.
 	// NOTE: User applications can avoid including the RFC3339 numeric offset by:
 	// - providing a time.Time value set to UTC, or
-	// - using the TimeUnix, TimeUnixMicro, or TimeUnixDynamic option instead of TimeRFC3339Nano.
+	// - using the TimeUnix, TimeUnixMicro, TimeUnixDynamic, or TimeRFC3339NanoUTC option.
 	TimeRFC3339Nano
 
+	// TimeRFC3339NanoUTC causes time.Time to encode to a CBOR time (tag 0) with a text string content
+	// representing UTC time using nanosecond precision in RFC3339 format.
+	TimeRFC3339NanoUTC
+
 	maxTimeMode
 )
 
@@ -1633,7 +1637,7 @@ func encodeTime(e *bytes.Buffer, em *encMode, v reflect.Value) error {
 	}
 	if em.timeTag == EncTagRequired {
 		tagNumber := 1
-		if em.time == TimeRFC3339 || em.time == TimeRFC3339Nano {
+		if em.time == TimeRFC3339 || em.time == TimeRFC3339Nano || em.time == TimeRFC3339NanoUTC {
 			tagNumber = 0
 		}
 		encodeHead(e, byte(cborTypeTag), uint64(tagNumber))
@@ -1661,6 +1665,10 @@ func encodeTime(e *bytes.Buffer, em *encMode, v reflect.Value) error {
 		s := t.Format(time.RFC3339)
 		return encodeString(e, em, reflect.ValueOf(s))
 
+	case TimeRFC3339NanoUTC:
+		s := t.UTC().Format(time.RFC3339Nano)
+		return encodeString(e, em, reflect.ValueOf(s))
+
 	default: // TimeRFC3339Nano
 		s := t.Format(time.RFC3339Nano)
 		return encodeString(e, em, reflect.ValueOf(s))


Test patch:
diff --git a/decode_test.go b/decode_test.go
index 53753c2..7ef6f51 100644
--- a/decode_test.go
+++ b/decode_test.go
@@ -4137,25 +4137,25 @@ func TestDecodeTime(t *testing.T) {
 			name:            "time without fractional seconds", // positive integer
 			cborRFC3339Time: mustHexDecode("74323031332d30332d32315432303a30343a30305a"),
 			cborUnixTime:    mustHexDecode("1a514b67b0"),
-			wantTime:        parseTime(time.RFC3339Nano, "2013-03-21T20:04:00Z"),
+			wantTime:        mustParseTime(time.RFC3339Nano, "2013-03-21T20:04:00Z"),
 		},
 		{
 			name:            "time with fractional seconds", // float
 			cborRFC3339Time: mustHexDecode("7819313937302d30312d30315432313a34363a34302d30363a3030"),
 			cborUnixTime:    mustHexDecode("fa47c35000"),
-			wantTime:        parseTime(time.RFC3339Nano, "1970-01-01T21:46:40-06:00"),
+			wantTime:        mustParseTime(time.RFC3339Nano, "1970-01-01T21:46:40-06:00"),
 		},
 		{
 			name:            "time with fractional seconds", // float
 			cborRFC3339Time: mustHexDecode("76323031332d30332d32315432303a30343a30302e355a"),
 			cborUnixTime:    mustHexDecode("fb41d452d9ec200000"),
-			wantTime:        parseTime(time.RFC3339Nano, "2013-03-21T20:04:00.5Z"),
+			wantTime:        mustParseTime(time.RFC3339Nano, "2013-03-21T20:04:00.5Z"),
 		},
 		{
 			name:            "time before January 1, 1970 UTC without fractional seconds", // negative integer
 			cborRFC3339Time: mustHexDecode("74313936392d30332d32315432303a30343a30305a"),
 			cborUnixTime:    mustHexDecode("3a0177f2cf"),
-			wantTime:        parseTime(time.RFC3339Nano, "1969-03-21T20:04:00Z"),
+			wantTime:        mustParseTime(time.RFC3339Nano, "1969-03-21T20:04:00Z"),
 		},
 	}
 	for _, tc := range testCases {
@@ -4187,19 +4187,19 @@ func TestDecodeTimeWithTag(t *testing.T) {
 			name:            "time without fractional seconds", // positive integer
 			cborRFC3339Time: mustHexDecode("c074323031332d30332d32315432303a30343a30305a"),
 			cborUnixTime:    mustHexDecode("c11a514b67b0"),
-			wantTime:        parseTime(time.RFC3339Nano, "2013-03-21T20:04:00Z"),
+			wantTime:        mustParseTime(time.RFC3339Nano, "2013-03-21T20:04:00Z"),
 		},
 		{
 			name:            "time with fractional seconds", // float
 			cborRFC3339Time: mustHexDecode("c076323031332d30332d32315432303a30343a30302e355a"),
 			cborUnixTime:    mustHexDecode("c1fb41d452d9ec200000"),
-			wantTime:        parseTime(time.RFC3339Nano, "2013-03-21T20:04:00.5Z"),
+			wantTime:        mustParseTime(time.RFC3339Nano, "2013-03-21T20:04:00.5Z"),
 		},
 		{
 			name:            "time before January 1, 1970 UTC without fractional seconds", // negative integer
 			cborRFC3339Time: mustHexDecode("c074313936392d30332d32315432303a30343a30305a"),
 			cborUnixTime:    mustHexDecode("c13a0177f2cf"),
-			wantTime:        parseTime(time.RFC3339Nano, "1969-03-21T20:04:00Z"),
+			wantTime:        mustParseTime(time.RFC3339Nano, "1969-03-21T20:04:00Z"),
 		},
 	}
 	for _, tc := range testCases {
@@ -4570,14 +4570,14 @@ func TestDecTimeTagOption(t *testing.T) {
 			cborRFC3339Time: mustHexDecode("74323031332d30332d32315432303a30343a30305a"),
 			cborUnixTime:    mustHexDecode("1a514b67b0"),
 			decMode:         timeTagIgnoredDecMode,
-			wantTime:        parseTime(time.RFC3339Nano, "2013-03-21T20:04:00Z"),
+			wantTime:        mustParseTime(time.RFC3339Nano, "2013-03-21T20:04:00Z"),
 		},
 		{
 			name:            "not-tagged data with timeTagOptionalDecMode option",
 			cborRFC3339Time: mustHexDecode("74323031332d30332d32315432303a30343a30305a"),
 			cborUnixTime:    mustHexDecode("1a514b67b0"),
 			decMode:         timeTagOptionalDecMode,
-			wantTime:        parseTime(time.RFC3339Nano, "2013-03-21T20:04:00Z"),
+			wantTime:        mustParseTime(time.RFC3339Nano, "2013-03-21T20:04:00Z"),
 		},
 		{
 			name:            "not-tagged data with timeTagRequiredDecMode option",
@@ -4592,21 +4592,21 @@ func TestDecTimeTagOption(t *testing.T) {
 			cborRFC3339Time: mustHexDecode("c074323031332d30332d32315432303a30343a30305a"),
 			cborUnixTime:    mustHexDecode("c11a514b67b0"),
 			decMode:         timeTagIgnoredDecMode,
-			wantTime:        parseTime(time.RFC3339Nano, "2013-03-21T20:04:00Z"),
+			wantTime:        mustParseTime(time.RFC3339Nano, "2013-03-21T20:04:00Z"),
 		},
 		{
 			name:            "tagged data with timeTagOptionalDecMode option",
 			cborRFC3339Time: mustHexDecode("c074323031332d30332d32315432303a30343a30305a"),
 			cborUnixTime:    mustHexDecode("c11a514b67b0"),
 			decMode:         timeTagOptionalDecMode,
-			wantTime:        parseTime(time.RFC3339Nano, "2013-03-21T20:04:00Z"),
+			wantTime:        mustParseTime(time.RFC3339Nano, "2013-03-21T20:04:00Z"),
 		},
 		{
 			name:            "tagged data with timeTagRequiredDecMode option",
 			cborRFC3339Time: mustHexDecode("c074323031332d30332d32315432303a30343a30305a"),
 			cborUnixTime:    mustHexDecode("c11a514b67b0"),
 			decMode:         timeTagRequiredDecMode,
-			wantTime:        parseTime(time.RFC3339Nano, "2013-03-21T20:04:00Z"),
+			wantTime:        mustParseTime(time.RFC3339Nano, "2013-03-21T20:04:00Z"),
 		},
 		// mis-tagged time CBOR data
 		{
@@ -4614,7 +4614,7 @@ func TestDecTimeTagOption(t *testing.T) {
 			cborRFC3339Time: mustHexDecode("c8c974323031332d30332d32315432303a30343a30305a"),
 			cborUnixTime:    mustHexDecode("c8c91a514b67b0"),
 			decMode:         timeTagIgnoredDecMode,
-			wantTime:        parseTime(time.RFC3339Nano, "2013-03-21T20:04:00Z"),
+			wantTime:        mustParseTime(time.RFC3339Nano, "2013-03-21T20:04:00Z"),
 		},
 		{
 			name:            "mis-tagged data with timeTagOptionalDecMode option",
diff --git a/encode_test.go b/encode_test.go
index 83abfe7..f8cc7ea 100644
--- a/encode_test.go
+++ b/encode_test.go
@@ -2687,320 +2687,593 @@ func TestEncodeInterface(t *testing.T) {
 	}
 }
 
-func TestEncodeTime(t *testing.T) {
-	timeUnixOpt := EncOptions{Time: TimeUnix}
-	timeUnixMicroOpt := EncOptions{Time: TimeUnixMicro}
-	timeUnixDynamicOpt := EncOptions{Time: TimeUnixDynamic}
-	timeRFC3339Opt := EncOptions{Time: TimeRFC3339}
-	timeRFC3339NanoOpt := EncOptions{Time: TimeRFC3339Nano}
+type encTimeOption struct {
+	name           string
+	opt            EncOptions
+	hasLocalOffset bool
+}
 
-	type timeConvert struct {
-		opt          EncOptions
+var (
+	timeUnixOpt = encTimeOption{
+		name:           "TimeUnix",
+		opt:            EncOptions{Time: TimeUnix},
+		hasLocalOffset: false,
+	}
+
+	timeUnixWithTagOpt = encTimeOption{
+		name:           "TimeUnixWithTag",
+		opt:            EncOptions{Time: TimeUnix, TimeTag: EncTagRequired},
+		hasLocalOffset: false,
+	}
+
+	timeUnixMicroOpt = encTimeOption{
+		name:           "TimeUnixMicro",
+		opt:            EncOptions{Time: TimeUnixMicro},
+		hasLocalOffset: false,
+	}
+
+	timeUnixMicroWithTagOpt = encTimeOption{
+		name:           "TimeUnixMicroWithTag",
+		opt:            EncOptions{Time: TimeUnixMicro, TimeTag: EncTagRequired},
+		hasLocalOffset: false,
+	}
+
+	timeUnixDynamicOpt = encTimeOption{
+		name:           "TimeUnixDynamic",
+		opt:            EncOptions{Time: TimeUnixDynamic},
+		hasLocalOffset: false,
+	}
+
+	timeUnixDynamicWithTagOpt = encTimeOption{
+		name:           "TimeUnixDynamicWithTag",
+		opt:            EncOptions{Time: TimeUnixDynamic, TimeTag: EncTagRequired},
+		hasLocalOffset: false,
+	}
+
+	timeRFC3339Opt = encTimeOption{
+		name:           "TimeRFC3339",
+		opt:            EncOptions{Time: TimeRFC3339},
+		hasLocalOffset: true,
+	}
+
+	timeRFC3339WithTagOpt = encTimeOption{
+		name:           "TimeRFC3339WithTag",
+		opt:            EncOptions{Time: TimeRFC3339, TimeTag: EncTagRequired},
+		hasLocalOffset: true,
+	}
+
+	timeRFC3339NanoOpt = encTimeOption{
+		name:           "TimeRFC3339Nano",
+		opt:            EncOptions{Time: TimeRFC3339Nano},
+		hasLocalOffset: true,
+	}
+
+	timeRFC3339NanoWithTagOpt = encTimeOption{
+		name:           "TimeRFC3339NanoWithTag",
+		opt:            EncOptions{Time: TimeRFC3339Nano, TimeTag: EncTagRequired},
+		hasLocalOffset: true,
+	}
+
+	timeRFC3339NanoUTCOpt = encTimeOption{
+		name:           "TimeRFC3339NanoUTC",
+		opt:            EncOptions{Time: TimeRFC3339NanoUTC},
+		hasLocalOffset: false,
+	}
+
+	timeRFC3339NanoUTCWithTagOpt = encTimeOption{
+		name:           "TimeRFC3339NanoUTCWithTag",
+		opt:            EncOptions{Time: TimeRFC3339NanoUTC, TimeTag: EncTagRequired},
+		hasLocalOffset: false,
+	}
+)
+
+func TestEncodeTime(t *testing.T) {
+	type conversion struct {
+		opt          encTimeOption
 		wantCborData []byte
+		roundtrip    bool
 	}
 	testCases := []struct {
 		name    string
 		tm      time.Time
-		convert []timeConvert
+		convert []conversion
 	}{
 		{
 			name: "zero time",
 			tm:   time.Time{},
-			convert: []timeConvert{
+			convert: []conversion{
 				{
 					opt:          timeUnixOpt,
 					wantCborData: mustHexDecode("f6"), // encode as CBOR null
+					roundtrip:    true,
+				},
+				{
+					opt:          timeUnixWithTagOpt,
+					wantCborData: mustHexDecode("f6"), // encode as CBOR null
+					roundtrip:    true,
 				},
 				{
 					opt:          timeUnixMicroOpt,
 					wantCborData: mustHexDecode("f6"), // encode as CBOR null
+					roundtrip:    true,
+				},
+				{
+					opt:          timeUnixMicroWithTagOpt,
+					wantCborData: mustHexDecode("f6"), // encode as CBOR null
+					roundtrip:    true,
 				},
 				{
 					opt:          timeUnixDynamicOpt,
 					wantCborData: mustHexDecode("f6"), // encode as CBOR null
+					roundtrip:    true,
+				},
+				{
+					opt:          timeUnixDynamicWithTagOpt,
+					wantCborData: mustHexDecode("f6"), // encode as CBOR null
+					roundtrip:    true,
 				},
 				{
 					opt:          timeRFC3339Opt,
 					wantCborData: mustHexDecode("f6"), // encode as CBOR null
+					roundtrip:    true,
+				},
+				{
+					opt:          timeRFC3339WithTagOpt,
+					wantCborData: mustHexDecode("f6"), // encode as CBOR null
+					roundtrip:    true,
 				},
 				{
 					opt:          timeRFC3339NanoOpt,
 					wantCborData: mustHexDecode("f6"), // encode as CBOR null
+					roundtrip:    true,
+				},
+				{
+					opt:          timeRFC3339NanoWithTagOpt,
+					wantCborData: mustHexDecode("f6"), // encode as CBOR null
+					roundtrip:    true,
+				},
+				{
+					opt:          timeRFC3339NanoUTCOpt,
+					wantCborData: mustHexDecode("f6"), // encode as CBOR null
+					roundtrip:    true,
+				},
+				{
+					opt:          timeRFC3339NanoUTCWithTagOpt,
+					wantCborData: mustHexDecode("f6"), // encode as CBOR null
+					roundtrip:    true,
 				},
 			},
 		},
 		{
-			name: "time without fractional seconds",
-			tm:   parseTime(time.RFC3339Nano, "2013-03-21T20:04:00Z"),
-			convert: []timeConvert{
+			name: "UTC time without fractional seconds",
+			tm:   mustParseTime(time.RFC3339Nano, "2013-03-21T20:04:00Z"),
+			convert: []conversion{
 				{
 					opt:          timeUnixOpt,
 					wantCborData: mustHexDecode("1a514b67b0"), // 1363896240
+					roundtrip:    true,
+				},
+				{
+					opt:          timeUnixWithTagOpt,
+					wantCborData: mustHexDecode("c11a514b67b0"), // 1(1363896240)
+					roundtrip:    true,
 				},
 				{
 					opt:          timeUnixMicroOpt,
 					wantCborData: mustHexDecode("fb41d452d9ec000000"), // 1363896240.0
+					roundtrip:    true,
+				},
+				{
+					opt:          timeUnixMicroWithTagOpt,
+					wantCborData: mustHexDecode("c1fb41d452d9ec000000"), // 1(1363896240.0)
+					roundtrip:    true,
 				},
 				{
 					opt:          timeUnixDynamicOpt,
 					wantCborData: mustHexDecode("1a514b67b0"), // 1363896240
+					roundtrip:    true,
 				},
 				{
-					opt:          timeRFC3339Opt,
-					wantCborData: mustHexDecode("74323031332d30332d32315432303a30343a30305a"), // "2013-03-21T20:04:00Z"
+					opt:          timeUnixDynamicWithTagOpt,
+					wantCborData: mustHexDecode("c11a514b67b0"), // 1(1363896240)
+					roundtrip:    true,
 				},
 				{
-					opt:          timeRFC3339NanoOpt,
+					opt:          timeRFC3339Opt,
 					wantCborData: mustHexDecode("74323031332d30332d32315432303a30343a30305a"), // "2013-03-21T20:04:00Z"
+					roundtrip:    true,
 				},
-			},
-		},
-		{
-			name: "time with fractional seconds",
-			tm:   parseTime(time.RFC3339Nano, "2013-03-21T20:04:00.5Z"),
-			convert: []timeConvert{
 				{
-					opt:          timeUnixOpt,
-					wantCborData: mustHexDecode("1a514b67b0"), // 1363896240
+					opt:          timeRFC3339WithTagOpt,
+					wantCborData: mustHexDecode("c074323031332d30332d32315432303a30343a30305a"), // 0("2013-03-21T20:04:00Z")
+					roundtrip:    true,
 				},
 				{
-					opt:          timeUnixMicroOpt,
-					wantCborData: mustHexDecode("fb41d452d9ec200000"), // 1363896240.5
+					opt:          timeRFC3339NanoOpt,
+					wantCborData: mustHexDecode("74323031332d30332d32315432303a30343a30305a"), // "2013-03-21T20:04:00Z"
+					roundtrip:    true,
 				},
 				{
-					opt:          timeUnixDynamicOpt,
-					wantCborData: mustHexDecode("fb41d452d9ec200000"), // 1363896240.5
+					opt:          timeRFC3339NanoWithTagOpt,
+					wantCborData: mustHexDecode("c074323031332d30332d32315432303a30343a30305a"), // 0("2013-03-21T20:04:00Z")
+					roundtrip:    true,
 				},
 				{
-					opt:          timeRFC3339Opt,
+					opt:          timeRFC3339NanoUTCOpt,
 					wantCborData: mustHexDecode("74323031332d30332d32315432303a30343a30305a"), // "2013-03-21T20:04:00Z"
+					roundtrip:    true,
 				},
 				{
-					opt:          timeRFC3339NanoOpt,
-					wantCborData: mustHexDecode("76323031332d30332d32315432303a30343a30302e355a"), // "2013-03-21T20:04:00.5Z"
+					opt:          timeRFC3339NanoUTCWithTagOpt,
+					wantCborData: mustHexDecode("c074323031332d30332d32315432303a30343a30305a"), // 0("2013-03-21T20:04:00Z")
+					roundtrip:    true,
 				},
 			},
 		},
 		{
-			name: "time before January 1, 1970 UTC without fractional seconds",
-			tm:   parseTime(time.RFC3339Nano, "1969-03-21T20:04:00Z"),
-			convert: []timeConvert{
+			name: "local time without fractional seconds",
+			tm:   mustParseTime(time.RFC3339Nano, "2013-03-21T20:04:00-05:00"),
+			convert: []conversion{
 				{
 					opt:          timeUnixOpt,
-					wantCborData: mustHexDecode("3a0177f2cf"), // -24638160
+					wantCborData: mustHexDecode("1a514bae00"), // 1363914240
+					roundtrip:    true,
+				},
+				{
+					opt:          timeUnixWithTagOpt,
+					wantCborData: mustHexDecode("c11a514bae00"), // 1(1363914240)
+					roundtrip:    true,
 				},
 				{
 					opt:          timeUnixMicroOpt,
-					wantCborData: mustHexDecode("fbc1777f2d00000000"), // -24638160.0
+					wantCborData: mustHexDecode("fb41d452eb80000000"), // 1363914240.0
+					roundtrip:    true,
+				},
+				{
+					opt:          timeUnixMicroWithTagOpt,
+					wantCborData: mustHexDecode("c1fb41d452eb80000000"), // 1(1363914240.0)
+					roundtrip:    true,
 				},
 				{
 					opt:          timeUnixDynamicOpt,
-					wantCborData: mustHexDecode("3a0177f2cf"), // -24638160
+					wantCborData: mustHexDecode("1a514bae00"), // 1363914240
+					roundtrip:    true,
+				},
+				{
+					opt:          timeUnixDynamicWithTagOpt,
+					wantCborData: mustHexDecode("c11a514bae00"), // 1(1363914240)
+					roundtrip:    true,
 				},
 				{
 					opt:          timeRFC3339Opt,
-					wantCborData: mustHexDecode("74313936392d30332d32315432303a30343a30305a"), // "1969-03-21T20:04:00Z"
+					wantCborData: mustHexDecode("7819323031332d30332d32315432303a30343a30302d30353a3030"), // "2013-03-21T20:04:00-05:00"
+					roundtrip:    true,
+				},
+				{
+					opt:          timeRFC3339WithTagOpt,
+					wantCborData: mustHexDecode("c07819323031332d30332d32315432303a30343a30302d30353a3030"), // 0("2013-03-21T20:04:00-05:00")
+					roundtrip:    true,
 				},
 				{
 					opt:          timeRFC3339NanoOpt,
-					wantCborData: mustHexDecode("74313936392d30332d32315432303a30343a30305a"), // "1969-03-21T20:04:00Z"
+					wantCborData: mustHexDecode("7819323031332d30332d32315432303a30343a30302d30353a3030"), // "2013-03-21T20:04:00-05:00"
+					roundtrip:    true,
+				},
+				{
+					opt:          timeRFC3339NanoWithTagOpt,
+					wantCborData: mustHexDecode("c07819323031332d30332d32315432303a30343a30302d30353a3030"), // 0("2013-03-21T20:04:00-05:00")
+					roundtrip:    true,
+				},
+				{
+					opt:          timeRFC3339NanoUTCOpt,
+					wantCborData: mustHexDecode("74323031332d30332d32325430313a30343a30305a"), // "2013-03-22T01:04:00Z"
+					roundtrip:    true,
+				},
+				{
+					opt:          timeRFC3339NanoUTCWithTagOpt,
+					wantCborData: mustHexDecode("c074323031332d30332d32325430313a30343a30305a"), // 0("2013-03-22T01:04:00Z")
+					roundtrip:    true,
 				},
 			},
 		},
-	}
-	for _, tc := range testCases {
-		for _, convert := range tc.convert {
-			var convertName string
-			switch convert.opt.Time {
-			case TimeUnix:
-				convertName = "TimeUnix"
-			case TimeUnixMicro:
-				convertName = "TimeUnixMicro"
-			case TimeUnixDynamic:
-				convertName = "TimeUnixDynamic"
-			case TimeRFC3339:
-				convertName = "TimeRFC3339"
-			case TimeRFC3339Nano:
-				convertName = "TimeRFC3339Nano"
-			}
-			name := tc.name + " with " + convertName + " option"
-			t.Run(name, func(t *testing.T) {
-				em, err := convert.opt.EncMode()
-				if err != nil {
-					t.Errorf("EncMode() returned error %v", err)
-				}
-				b, err := em.Marshal(tc.tm)
-				if err != nil {
-					t.Errorf("Marshal(%+v) returned error %v", tc.tm, err)
-				} else if !bytes.Equal(b, convert.wantCborData) {
-					t.Errorf("Marshal(%+v) = 0x%x, want 0x%x", tc.tm, b, convert.wantCborData)
-				}
-			})
-		}
-	}
-}
-
-func TestEncodeTimeWithTag(t *testing.T) {
-	timeUnixOpt := EncOptions{Time: TimeUnix, TimeTag: EncTagRequired}
-	timeUnixMicroOpt := EncOptions{Time: TimeUnixMicro, TimeTag: EncTagRequired}
-	timeUnixDynamicOpt := EncOptions{Time: TimeUnixDynamic, TimeTag: EncTagRequired}
-	timeRFC3339Opt := EncOptions{Time: TimeRFC3339, TimeTag: EncTagRequired}
-	timeRFC3339NanoOpt := EncOptions{Time: TimeRFC3339Nano, TimeTag: EncTagRequired}
-
-	type timeConvert struct {
-		opt          EncOptions
-		wantCborData []byte
-	}
-	testCases := []struct {
-		name    string
-		tm      time.Time
-		convert []timeConvert
-	}{
 		{
-			name: "zero time",
-			tm:   time.Time{},
-			convert: []timeConvert{
+			name: "UTC time with fractional seconds",
+			tm:   mustParseTime(time.RFC3339Nano, "2013-03-21T20:04:00.5Z"),
+			convert: []conversion{
 				{
 					opt:          timeUnixOpt,
-					wantCborData: mustHexDecode("f6"), // encode as CBOR null
+					wantCborData: mustHexDecode("1a514b67b0"), // 1363896240
+					roundtrip:    false,
+				},
+				{
+					opt:          timeUnixWithTagOpt,
+					wantCborData: mustHexDecode("c11a514b67b0"), // 1(1363896240)
+					roundtrip:    false,
 				},
 				{
 					opt:          timeUnixMicroOpt,
-					wantCborData: mustHexDecode("f6"), // encode as CBOR null
+					wantCborData: mustHexDecode("fb41d452d9ec200000"), // 1363896240.5
+					roundtrip:    true,
+				},
+				{
+					opt:          timeUnixMicroWithTagOpt,
+					wantCborData: mustHexDecode("c1fb41d452d9ec200000"), // 1(1363896240.5)
+					roundtrip:    true,
 				},
 				{
 					opt:          timeUnixDynamicOpt,
-					wantCborData: mustHexDecode("f6"), // encode as CBOR null
+					wantCborData: mustHexDecode("fb41d452d9ec200000"), // 1363896240.5
+					roundtrip:    true,
+				},
+				{
+					opt:          timeUnixDynamicWithTagOpt,
+					wantCborData: mustHexDecode("c1fb41d452d9ec200000"), // 1(1363896240.5)
+					roundtrip:    true,
 				},
 				{
 					opt:          timeRFC3339Opt,
-					wantCborData: mustHexDecode("f6"), // encode as CBOR null
+					wantCborData: mustHexDecode("74323031332d30332d32315432303a30343a30305a"), // "2013-03-21T20:04:00Z"
+					roundtrip:    false,
+				},
+				{
+					opt:          timeRFC3339WithTagOpt,
+					wantCborData: mustHexDecode("c074323031332d30332d32315432303a30343a30305a"), // 0("2013-03-21T20:04:00Z")
+					roundtrip:    false,
 				},
 				{
 					opt:          timeRFC3339NanoOpt,
-					wantCborData: mustHexDecode("f6"), // encode as CBOR null
+					wantCborData: mustHexDecode("76323031332d30332d32315432303a30343a30302e355a"), // "2013-03-21T20:04:00.5Z"
+					roundtrip:    true,
+				},
+				{
+					opt:          timeRFC3339NanoWithTagOpt,
+					wantCborData: mustHexDecode("c076323031332d30332d32315432303a30343a30302e355a"), // 0("2013-03-21T20:04:00.5Z")
+					roundtrip:    true,
+				},
+				{
+					opt:          timeRFC3339NanoUTCOpt,
+					wantCborData: mustHexDecode("76323031332d30332d32315432303a30343a30302e355a"), // "2013-03-21T20:04:00.5Z"
+					roundtrip:    true,
+				},
+				{
+					opt:          timeRFC3339NanoUTCWithTagOpt,
+					wantCborData: mustHexDecode("c076323031332d30332d32315432303a30343a30302e355a"), // 0("2013-03-21T20:04:00.5Z")
+					roundtrip:    true,
 				},
 			},
 		},
 		{
-			name: "time without fractional seconds",
-			tm:   parseTime(time.RFC3339Nano, "2013-03-21T20:04:00Z"),
-			convert: []timeConvert{
+			name: "local time with fractional seconds",
+			tm:   mustParseTime(time.RFC3339Nano, "2013-03-21T20:04:00.5-05:00"),
+			convert: []conversion{
 				{
 					opt:          timeUnixOpt,
-					wantCborData: mustHexDecode("c11a514b67b0"), // 1363896240
+					wantCborData: mustHexDecode("1a514bae00"), // 1363914240
+					roundtrip:    false,
+				},
+				{
+					opt:          timeUnixWithTagOpt,
+					wantCborData: mustHexDecode("c11a514bae00"), // 1(1363914240)
+					roundtrip:    false,
 				},
 				{
 					opt:          timeUnixMicroOpt,
-					wantCborData: mustHexDecode("c1fb41d452d9ec000000"), // 1363896240.0
+					wantCborData: mustHexDecode("fb41d452eb80200000"), // 1363914240.5
+					roundtrip:    true,
+				},
+				{
+					opt:          timeUnixMicroWithTagOpt,
+					wantCborData: mustHexDecode("c1fb41d452eb80200000"), // 1(1363914240.5)
+					roundtrip:    true,
 				},
 				{
 					opt:          timeUnixDynamicOpt,
-					wantCborData: mustHexDecode("c11a514b67b0"), // 1363896240
+					wantCborData: mustHexDecode("fb41d452eb80200000"), // 1363914240.5
+					roundtrip:    true,
+				},
+				{
+					opt:          timeUnixDynamicWithTagOpt,
+					wantCborData: mustHexDecode("c1fb41d452eb80200000"), // 1(1363914240.5)
+					roundtrip:    true,
 				},
 				{
 					opt:          timeRFC3339Opt,
-					wantCborData: mustHexDecode("c074323031332d30332d32315432303a30343a30305a"), // "2013-03-21T20:04:00Z"
+					wantCborData: mustHexDecode("7819323031332d30332d32315432303a30343a30302d30353a3030"), // "2013-03-21T20:04:00-05:00"
+					roundtrip:    false,
+				},
+				{
+					opt:          timeRFC3339WithTagOpt,
+					wantCborData: mustHexDecode("c07819323031332d30332d32315432303a30343a30302d30353a3030"), // 0("2013-03-21T20:04:00-05:00")
+					roundtrip:    false,
 				},
 				{
 					opt:          timeRFC3339NanoOpt,
-					wantCborData: mustHexDecode("c074323031332d30332d32315432303a30343a30305a"), // "2013-03-21T20:04:00Z"
+					wantCborData: mustHexDecode("781b323031332d30332d32315432303a30343a30302e352d30353a3030"), // "2013-03-21T20:04:00.5-05:00"
+					roundtrip:    true,
+				},
+				{
+					opt:          timeRFC3339NanoWithTagOpt,
+					wantCborData: mustHexDecode("c0781b323031332d30332d32315432303a30343a30302e352d30353a3030"), // 0("2013-03-21T20:04:00.5-05:00")
+					roundtrip:    true,
+				},
+				{
+					opt:          timeRFC3339NanoUTCOpt,
+					wantCborData: mustHexDecode("76323031332d30332d32325430313a30343a30302e355a"), // "2013-03-22T01:04:00.5Z"
+					roundtrip:    true,
+				},
+				{
+					opt:          timeRFC3339NanoUTCWithTagOpt,
+					wantCborData: mustHexDecode("c076323031332d30332d32325430313a30343a30302e355a"), // 0("2013-03-22T01:04:00.5Z")
+					roundtrip:    true,
 				},
 			},
 		},
 		{
-			name: "time with fractional seconds",
-			tm:   parseTime(time.RFC3339Nano, "2013-03-21T20:04:00.5Z"),
-			convert: []timeConvert{
+			name: "UTC time before January 1, 1970 UTC without fractional seconds",
+			tm:   mustParseTime(time.RFC3339Nano, "1969-03-21T20:04:00Z"),
+			convert: []conversion{
 				{
 					opt:          timeUnixOpt,
-					wantCborData: mustHexDecode("c11a514b67b0"), // 1363896240
+					wantCborData: mustHexDecode("3a0177f2cf"), // -24638160
+					roundtrip:    true,
+				},
+				{
+					opt:          timeUnixWithTagOpt,
+					wantCborData: mustHexDecode("c13a0177f2cf"), // 1(-24638160)
+					roundtrip:    true,
 				},
 				{
 					opt:          timeUnixMicroOpt,
-					wantCborData: mustHexDecode("c1fb41d452d9ec200000"), // 1363896240.5
+					wantCborData: mustHexDecode("fbc1777f2d00000000"), // -24638160.0
+					roundtrip:    true,
+				},
+				{
+					opt:          timeUnixMicroWithTagOpt,
+					wantCborData: mustHexDecode("c1fbc1777f2d00000000"), // 1(-24638160.0)
+					roundtrip:    true,
 				},
 				{
 					opt:          timeUnixDynamicOpt,
-					wantCborData: mustHexDecode("c1fb41d452d9ec200000"), // 1363896240.5
+					wantCborData: mustHexDecode("3a0177f2cf"), // -24638160
+					roundtrip:    true,
+				},
+				{
+					opt:          timeUnixDynamicWithTagOpt,
+					wantCborData: mustHexDecode("c13a0177f2cf"), // 1(-24638160)
+					roundtrip:    true,
 				},
 				{
 					opt:          timeRFC3339Opt,
-					wantCborData: mustHexDecode("c074323031332d30332d32315432303a30343a30305a"), // "2013-03-21T20:04:00Z"
+					wantCborData: mustHexDecode("74313936392d30332d32315432303a30343a30305a"), // "1969-03-21T20:04:00Z"
+					roundtrip:    true,
+				},
+				{
+					opt:          timeRFC3339WithTagOpt,
+					wantCborData: mustHexDecode("c074313936392d30332d32315432303a30343a30305a"), // 0("1969-03-21T20:04:00Z")
+					roundtrip:    true,
 				},
 				{
 					opt:          timeRFC3339NanoOpt,
-					wantCborData: mustHexDecode("c076323031332d30332d32315432303a30343a30302e355a"), // "2013-03-21T20:04:00.5Z"
+					wantCborData: mustHexDecode("74313936392d30332d32315432303a30343a30305a"), // "1969-03-21T20:04:00Z"
+					roundtrip:    true,
+				},
+				{
+					opt:          timeRFC3339NanoWithTagOpt,
+					wantCborData: mustHexDecode("c074313936392d30332d32315432303a30343a30305a"), // 0("1969-03-21T20:04:00Z")
+					roundtrip:    true,
+				},
+				{
+					opt:          timeRFC3339NanoUTCOpt,
+					wantCborData: mustHexDecode("74313936392d30332d32315432303a30343a30305a"), // "1969-03-21T20:04:00Z"
+					roundtrip:    true,
+				},
+				{
+					opt:          timeRFC3339NanoUTCWithTagOpt,
+					wantCborData: mustHexDecode("c074313936392d30332d32315432303a30343a30305a"), // 0("1969-03-21T20:04:00Z")
+					roundtrip:    true,
 				},
 			},
 		},
 		{
-			name: "time before January 1, 1970 UTC without fractional seconds",
-			tm:   parseTime(time.RFC3339Nano, "1969-03-21T20:04:00Z"),
-			convert: []timeConvert{
+			name: "local time before January 1, 1970 UTC without fractional seconds",
+			tm:   mustParseTime(time.RFC3339Nano, "1969-03-21T20:04:00-05:00"),
+			convert: []conversion{
 				{
 					opt:          timeUnixOpt,
-					wantCborData: mustHexDecode("c13a0177f2cf"), // -24638160
+					wantCborData: mustHexDecode("3a0177ac7f"), // -24620160
+					roundtrip:    true,
+				},
+				{
+					opt:          timeUnixWithTagOpt,
+					wantCborData: mustHexDecode("c13a0177ac7f"), // 1(-24620160)
+					roundtrip:    true,
 				},
 				{
 					opt:          timeUnixMicroOpt,
-					wantCborData: mustHexDecode("c1fbc1777f2d00000000"), // -24638160.0
+					wantCborData: mustHexDecode("fbc1777ac800000000"), // -24620160.0
+					roundtrip:    true,
+				},
+				{
+					opt:          timeUnixMicroWithTagOpt,
+					wantCborData: mustHexDecode("c1fbc1777ac800000000"), // 1(-24620160.0)
+					roundtrip:    true,
 				},
 				{
 					opt:          timeUnixDynamicOpt,
-					wantCborData: mustHexDecode("c13a0177f2cf"), // -24638160
+					wantCborData: mustHexDecode("3a0177ac7f"), // -24620160
+					roundtrip:    true,
+				},
+				{
+					opt:          timeUnixDynamicWithTagOpt,
+					wantCborData: mustHexDecode("c13a0177ac7f"), // 1(-24620160)
+					roundtrip:    true,
 				},
 				{
 					opt:          timeRFC3339Opt,
-					wantCborData: mustHexDecode("c074313936392d30332d32315432303a30343a30305a"), // "1969-03-21T20:04:00Z"
+					wantCborData: mustHexDecode("7819313936392d30332d32315432303a30343a30302d30353a3030"), // "1969-03-21T20:04:00-05:00"
+					roundtrip:    true,
+				},
+				{
+					opt:          timeRFC3339WithTagOpt,
+					wantCborData: mustHexDecode("c07819313936392d30332d32315432303a30343a30302d30353a3030"), // 0("1969-03-21T20:04:00-05:00")
+					roundtrip:    true,
 				},
 				{
 					opt:          timeRFC3339NanoOpt,
-					wantCborData: mustHexDecode("c074313936392d30332d32315432303a30343a30305a"), // "1969-03-21T20:04:00Z"
+					wantCborData: mustHexDecode("7819313936392d30332d32315432303a30343a30302d30353a3030"), // "1969-03-21T20:04:00-05:00"
+					roundtrip:    true,
+				},
+				{
+					opt:          timeRFC3339NanoWithTagOpt,
+					wantCborData: mustHexDecode("c07819313936392d30332d32315432303a30343a30302d30353a3030"), // 0("1969-03-21T20:04:00-05:00")
+					roundtrip:    true,
+				},
+				{
+					opt:          timeRFC3339NanoUTCOpt,
+					wantCborData: mustHexDecode("74313936392d30332d32325430313a30343a30305a"), // "1969-03-22T01:04:00Z"
+					roundtrip:    true,
+				},
+				{
+					opt:          timeRFC3339NanoUTCWithTagOpt,
+					wantCborData: mustHexDecode("c074313936392d30332d32325430313a30343a30305a"), // 0("1969-03-22T01:04:00Z")
+					roundtrip:    true,
 				},
 			},
 		},
 	}
 	for _, tc := range testCases {
 		for _, convert := range tc.convert {
-			var convertName string
-			switch convert.opt.Time {
-			case TimeUnix:
-				convertName = "TimeUnix"
-			case TimeUnixMicro:
-				convertName = "TimeUnixMicro"
-			case TimeUnixDynamic:
-				convertName = "TimeUnixDynamic"
-			case TimeRFC3339:
-				convertName = "TimeRFC3339"
-			case TimeRFC3339Nano:
-				convertName = "TimeRFC3339Nano"
-			}
-			name := tc.name + " with " + convertName + " option"
+			name := tc.name + " with " + convert.opt.name + " option"
 			t.Run(name, func(t *testing.T) {
-				em, err := convert.opt.EncMode()
+				em, err := convert.opt.opt.EncMode()
 				if err != nil {
 					t.Errorf("EncMode() returned error %v", err)
 				}
+
 				b, err := em.Marshal(tc.tm)
 				if err != nil {
 					t.Errorf("Marshal(%+v) returned error %v", tc.tm, err)
 				} else if !bytes.Equal(b, convert.wantCborData) {
 					t.Errorf("Marshal(%+v) = 0x%x, want 0x%x", tc.tm, b, convert.wantCborData)
 				}
+
+				var tm time.Time
+				err = Unmarshal(b, &tm)
+				if err != nil {
+					t.Errorf("Unmarshal(0x%x) to time.Time returned error %v", b, err)
+				} else if convert.roundtrip {
+					if convert.opt.hasLocalOffset && tc.tm.Compare(tm) != 0 {
+						t.Errorf("failed to rountrip local time: want %s, got %s", tc.tm, tm)
+					}
+					if !convert.opt.hasLocalOffset && tc.tm.UTC().Compare(tm.UTC()) != 0 {
+						t.Errorf("failed to roundtrip UTC time: want %s, got %s", tc.tm, tm)
+					}
+				}
 			})
 		}
 	}
 }
 
-func parseTime(layout string, value string) time.Time {
-	tm, err := time.Parse(layout, value)
-	if err != nil {
-		panic(err)
-	}
-	return tm
-}
-
 func TestInvalidTimeMode(t *testing.T) {
 	for _, tc := range []struct {
 		name         string
@@ -6810,3 +7083,11 @@ func TestJSONMarshalerTranscoder(t *testing.T) {
 		})
 	}
 }
+
+func mustParseTime(layout string, value string) time.Time {
+	tm, err := time.Parse(layout, value)
+	if err != nil {
+		panic(err)
+	}
+	return tm
+}