@@ 39,32 39,31 @@ one:
----
In the [previous episode][ep10] we added better floating point number support to
-our encoder. We implemented all the Go native types, now we’ll implement a
-custom stype: time.Time, a timestamp type. The CBOR encoding
-[supports](https://tools.ietf.org/html/rfc7049#section-2.4.1) 3 timestamp types
-natively:
+our encoder. We have implemented all the Go native types, now we’ll implement a
+custom stype: time.Time, a timestamp type from Go’s standard library. The CBOR
+encoding [supports](https://tools.ietf.org/html/rfc7049#section-2.4.1) 3
+timestamp types natively:
- RFC3339 string like "2019-02-01T17:45:23Z"
- floating point epoch based values
- integer value epoch based values
-[CBOR][rfc7049] has special values called tags. They are used to represent data
-with additional semantics like timestamps. A tag’s header major type 6 and
-represents an integer number used to determine the tag content’s type. Each tag
-type has a unique integer identifier, a number that corresponds to the tag’s
-type. For example URIs are represented as a tagged unicode string: first there’s
-the header with the major type 6 —indicating it’s a tagged value— encoding the
-integer 32 —the minor that corresponds to URIs—, the header is followed by the
-URI encoded as an UTF-8 CBOR string. Timestamps have two tagged data item types:
-0 for unicode strings encoded as a RFC3339 timestamp, or 1 for epoch-based
-timestamps that include floating point & integer values.
+The [CBOR format][rfc7049] has special values called tags, used to represent
+data with additional semantics like timestamps. Tag’s headers major is type 6 and
+represents an integer number used to determine the tag content’s type. Each
+tagged type has a unique integer identifier number.
-How can we detect if we have a time.Time value in the encoder? Looking at
-[time.Time’s definition](https://godoc.org/time#Time) we see that it’s a struct,
-a kind of value we already handle in the encoder. The reflect package lets us
-query and compare value types, so we will check if the value’s type is time.Time
-when we have a reflect.Struct kind and write a CBOR timestamp when that’s the
-case.
+For example URIs are represented as a tagged unicode string: first there’s the
+header with the major type 6 —indicating it’s a tagged value— encoding the
+integer 32 —the identifies that corresponds to URIs—, the header is followed by
+the URI encoded as an UTF-8 CBOR string.
+
+How can we detect if we have a time.Time value in the encoder?
+Looking at [time.Time’s definition](https://godoc.org/time#Time) we see that
+it’s a struct, a kind of value we already handle in the encoder. The reflect
+package lets us query and compare value types, so we will check if the value’s
+type is time.Time when we have a reflect.Struct kind and write a CBOR timestamp
+when that’s the case.
In the main switch block we add a if condition in the reflect.Struct case to
handle time.Time if we have a value with that type. There’s a bit of gymnastic
@@ 89,10 88,12 @@ is therefor a tiny bit more efficient:
}
return e.writeStruct(x)
-Let’s add a new function to write CBOR timestamps: writeTime. First we’ll handle
-string timestamps, and implement scalar timestamp types second. We start with
-[RFC3339][rfc3339] strings, we lookup the example from the spec, and we add our
-first test case:
+Timestamps have two tagged data item types: 0 for RFC3339 timestamps encoded as
+a unicode strings, or 1 for epoch-based timestamps that include floating point &
+integer values. Let’s add a new function to write CBOR timestamps: writeTime.
+First we’ll handle string timestamps, and implement scalar epoch-based timestamp
+types second. We start with [RFC3339][rfc3339] strings, lookup the example from
+the spec, and add our first test case:
func TestTimestamp(t *testing.T) {
var rfc3339Timestamp, _ = time.Parse(time.RFC3339, "2013-03-21T20:04:00Z")
@@ 134,8 135,8 @@ types:
)
The function writeTime writes the tag’s header with minorTimeString to indicate
-a string timestamp follows, then it converts the timestamp into a RFC3339 string
-and writes it to the output:
+a string follows, then it converts the timestamp into a RFC3339 string and
+writes it to the output:
func (e *Encoder) writeTime(v reflect.Value) error {
if err := e.writeHeader(majorTag, minorTimeString); err != nil {
@@ 154,23 155,23 @@ switch statement:
}
return e.writeStruct(x)
-We run `go test` to confirm everything works.
+A quick `go test` to confirm writing string timestamps works.
-Now that we are done with string timestamps, we’ll handle epoch-based
-timestamps: they are scalar values where 0 corresponds to the epoch (January 1,
-1970), they can either be integer or floating point values.
+We are done with string timestamps, epoch-based timestamps are next: they are
+scalar values where 0 corresponds to the Unix epoch (January 1, 1970), they can
+either be integer or floating point values.
We’ll minimize the size of our output by using the most compact type we can use
without losing precision. The timestamp can either be an integer, a floating
-point number, or a RFC3339 string. If the timestamp’s timezone isn’t UTC
-we’ll have to use the largest type: the RFC3339 string, because we need to
-encode the timezone information and we can’t do it with scalar timestamps. If
-the timestamp’s timezone is UTC we can use a scalar timestamp because it’s
-assumed to be UTC. We’ll use an integer when the timestamp can be represented as
-whole seconds and use floating point number otherwise.
+point number, or a RFC3339 string. If the timestamp’s timezone isn’t UTC we’ll
+have to use the largest type: RFC3339 strings, because we need to encode the
+timezone information and we can’t do it with scalar timestamps. If the
+timestamp’s timezone is UTC or isn’t set we can use a scalar timestamp because
+it’s assumed to be UTC. We’ll use an integer when the timestamp can be
+represented as whole seconds and use floating point number otherwise.
First we’ll add a condition to only use RFC3339 strings when the timestamp has a
-timezone different from UTC.
+timezone that’s not UTC:
func (e *Encoder) writeTime(v reflect.Value) error {
var t = v.Interface().(time.Time)
@@ 185,7 186,8 @@ timezone different from UTC.
Because we’re changing the behavior of writeTime when the timezone is UTC, we
have to fix the first test case to use a timestamp with its timezone set. To do
-so we replace the Z at the end of rfc3339Timestamp with +07:00:
+so we replace the Z —a shortcut for the UTC timezone— at the end of
+rfc3339Timestamp with +07:00:
func TestTimestamp(t *testing.T) {
var rfc3339Timestamp, _ = time.Parse(time.RFC3339, "2013-03-21T20:04:00+07:00")
@@ 203,16 205,11 @@ so we replace the Z at the end of rfc333
},
},
}
-
- for _, c := range cases {
- t.Run(fmt.Sprintf("%v", c.Value), func(t *testing.T) {
- testEncoder(t, c.Value, c.Expected)
- })
- }
+ ...
}
-We’ll now implement floating point numbers when there’s no timezone information
-to encode. As usual we start by adding a test case for this from the spec:
+Let’s implement floating point numbers when there’s no timezone information to
+encode. As usual we start by adding a test case for this from the spec:
func TestTimestamp(t *testing.T) {
...
@@ 226,25 223,24 @@ to encode. As usual we start by adding a
Expected: []byte{0xc1, 0xfb, 0x41, 0xd4, 0x52, 0xd9, 0xec, 0x20, 0x00, 0x00},
},
}
-
...
}
Note that we had to call the .UTC() method on the time.Time object return by
time.Unix, that’s because otherwise the object will have the local timezone
-associated to it. So we call the UTC method to get the same timestamp with a UTC
+associated with it. So we call the UTC method to get the timestamp set at UTC
timezone.
Because time.Time store its internal time as an integer counting the
number of nanoseconds since the Epoch, we’ll have to convert it into a floating
-point number in seconds. To do this we use the units from the time module to
-define a constant to convert from nanoseconds to seconds:
+point number in seconds before writing it. To do this we define a constant to
+convert from nanoseconds to seconds from the time’s module units:
const nanoSecondsInSecond = time.Second / time.Nanosecond
-Then we add our new code after the if block to handle string timestamps. We
-write the header with the minorTimeEpoch as the sub-type to indicate we have a
-scalar timestamp, the we write the converted value as a floating point number:
+We add our new code after the block to handle string timestamps. We write the
+header with minorTimeEpoch as its sub-type to indicate we have a scalar
+timestamp, then write the converted value as a floating point number:
func (e *Encoder) writeTime(v reflect.Value) error {
var t = v.Interface().(time.Time)
@@ 264,10 260,10 @@ scalar timestamp, the we write the conve
float64(unixTimeNano) / float64(nanoSecondsInSecond))
}
-If the timestamp in seconds can be converted into an integer without losing
-precision this means we can write it as an integer timestamp. Integers are
-usually more compact than floating point numbers, so let’s always use them when
-possible. We add another test from the spec:
+If the timestamp in seconds is an integer number we can write it as an integer
+timestamp without losing precision. Integers are usually more compact than
+floating point numbers, so we’ll always use them when possible. Another test
+case from the spec makes it into cbor_test.go:
func TestTimestamp(t *testing.T) {
...
@@ 286,9 282,9 @@ possible. We add another test from the s
}
To determine if we can write an integer timestamp we check if the fractional
-part of the timestamp in seconds would be zero. Then we convert unixTimeNano
-into seconds, set the CBOR integer’s header minor type depending on the
-timestamp’s sign, and use writeInteger to write the timestamp:
+part of the timestamp in seconds is zero, then we convert unixTimeNano into
+seconds, set the CBOR integer’s header minor type depending on the timestamp’s
+sign, and use writeInteger to write the timestamp:
const nanoSecondsInSecond = time.Second / time.Nanosecond