# HG changeset patch # User Henry Precheur # Date 1578028651 28800 # Thu Jan 02 21:17:31 2020 -0800 # Node ID 6eb73d3a91a64812d1126619dc922385e3d6e672 # Parent 732f18ae0e3638a4aff974b1cfb55ca8b9a80cad edits diff --git a/episode11/README.md b/episode11/README.md --- a/episode11/README.md +++ b/episode11/README.md @@ -40,34 +40,48 @@ 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 @@ } 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 @@ } } -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 @@ ... ) -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 @@ 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 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 @@ } 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 @@ } } -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 @@ ... } +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 @@ 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 @@ 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 @@ 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 @@ } } -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.