@@ 40,34 40,48 @@ 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. According the the [CBOR
-spec](https://tools.ietf.org/html/rfc7049#section-2.4.1) there are 3 timestamp
-formats supported:
+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:
- RFC3339 string like "2019-02-01T17:45:23Z"
- floating point epoch based values
- integer value epoch based values
-The [CBOR spec][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. So each tag type has a unique integer
-identifier, the number corresponds to the tag’s type. For example URI’s are
-represented as a tagged unicode string: first there’s the header with the major
-type 6 —indicating it’s a tag— with the value 32 —the minor that corresponds to
-URIs—, the header is followed by an UTF-8 CBOR string with the URI.
-
-Timestamp have two tagged data item types: 0 for unicode strings encoded as a
-RFC3339 timestamp, or 1 for epoch-based timestamps.
+[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.
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 the type of a value, so we will check if the value’s type is time.Time
-when we have a reflect.Struct kind.
+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
+needed to get time.Time’s type without creating an empty timestamp on the stack,
+we can either do:
+
-In the main switch block we add a branch in the reflect.Struct case to handle
-time.Time if we have a value with that type. There’s a bit of gymnastic needed
-to get time.Time’s type without creating an empty timestamp on the stack:
+ reflect.TypeOf(time.Time{})
+
+Or:
+
+ reflect.TypeOf((*time.Time)(nil)).Elem()
+
+In the first case we create an empty time.Time object and get its type, in the
+second case we create an empty interface to time.Time and retreive its type.
+We’ll use the second way because it doesn’t create an empty time.Time object and
+is therefor a tiny bit more efficient:
case reflect.Struct:
if x.Type() == reflect.TypeOf((*time.Time)(nil)).Elem() {
@@ 75,10 89,10 @@ to get time.Time’s type without creating an empty timestamp on the stack:
}
return e.writeStruct(x)
-Let’s add a new function to write CBOR encoded timestamps: writeTime. First
-we’ll do string timestamps, and do scalar timestamp types second. We start with
-[RFC3339][rfc3339] strings, and lookup the example from the spec and add our
-first test:
+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:
func TestTimestamp(t *testing.T) {
var rfc3339Timestamp, _ = time.Parse(time.RFC3339, "2013-03-21T20:04:00Z")
@@ 104,7 118,8 @@ first test:
}
}
-We add a few header constants required to encode the new tagged types:
+Back in cbor.go we add a few header constants required to encode the new tagged
+types:
const (
// major types
@@ 118,9 133,9 @@ We add a few header constants required t
...
)
-The function writeTime writes the tag’s header with minorTimeString
-to indicate a unicode string follows, then it converts the timestamp into a
-RFC3339 string and writes the string to the output:
+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:
func (e *Encoder) writeTime(v reflect.Value) error {
if err := e.writeHeader(majorTag, minorTimeString); err != nil {
@@ 130,7 145,8 @@ RFC3339 string and writes the string to
return e.writeUnicodeString(t.Format(time.RFC3339))
}
-We hook it up to the rest of the code by adding a call to writeTime:
+We hook it up to the rest of the code by adding a call to writeTime in our main
+switch statement:
case reflect.Struct:
if x.Type() == reflect.TypeOf((*time.Time)(nil)).Elem() {
@@ 140,21 156,21 @@ We hook it up to the rest of the code by
We run `go test` to confirm everything works.
-Now we’ll handle epoch-based timestamps: those are scalar values where 0
-corresponds to the epoch (January 1, 1970), they can either be integer or
-floating point values.
+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’ll minimize the size of our output by picking the most compact type we can
-use with our encoder. The timestamp can either be an integer, a floating point
-number, or a RFC3339 string. If there’s a timestamp’s timezone isn’t UTC we’ll
-have to use the largest type: the RFC3339 string, because we need to encode the
-timezone. If the timestamp’s timezone is UTC we can use a scalar timestamp
-because we don’t have to set the timezone 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 eitherwise.
+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.
First we’ll add a condition to only use RFC3339 strings when the timestamp has a
-timezone that’s not UTC.
+timezone different from UTC.
func (e *Encoder) writeTime(v reflect.Value) error {
var t = v.Interface().(time.Time)
@@ 168,8 184,8 @@ timezone that’s not UTC.
}
Because we’re changing the behavior of writeTime when the timezone is UTC, we
-need to fix the test to use timestamp with its timezone set. To do so we replace
-the Z at the end of rfc3339Timestamp with +07:00:
+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:
func TestTimestamp(t *testing.T) {
var rfc3339Timestamp, _ = time.Parse(time.RFC3339, "2013-03-21T20:04:00+07:00")
@@ 195,7 211,7 @@ the Z at the end of rfc3339Timestamp wit
}
}
-Now let’s implement floating point numbers when there’s no timezone information
+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:
func TestTimestamp(t *testing.T) {
@@ 214,6 230,11 @@ to encode. As usual we start by adding a
...
}
+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
+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
@@ 221,9 242,9 @@ define a constant to convert from nanose
const nanoSecondsInSecond = time.Second / time.Nanosecond
-Then we add the new code after the first if block. We write the header with the
-minorTimeEpoch as the sub-type to indicate we have a scalar timestamp, and write
-the converted value as a floating point number:
+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:
func (e *Encoder) writeTime(v reflect.Value) error {
var t = v.Interface().(time.Time)
@@ 243,10 264,10 @@ the converted value as a floating point
float64(unixTimeNano) / float64(nanoSecondsInSecond))
}
-If the timestamp in seconds doesn’t have a fractional part this means we can
-write it as integer without losing data. Integers are usually more compact than
-floating point numbers, so let’s use them when possible. We add another test
-from the spec:
+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:
func TestTimestamp(t *testing.T) {
...
@@ 266,8 287,8 @@ from the spec:
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 minor type depending on the timestamp’s sign, and use
-writeInteger to write the timestamp:
+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
@@ 293,6 314,7 @@ writeInteger to write the timestamp:
}
}
-And it’s all we needed to do to support the non-native type time.Time! We are
-done writing our CBOR encoder. It you would like to see other things covered
-feel free to reach me at henry@precheur.org.
+And it’s all we needed to do to support the non-native type time.Time!
+
+We are done writing our CBOR encoder. It you would like to see other things
+covered feel free to reach me at henry@precheur.org.