Repository: amzn/ion-go
Pull request #98 (2020-07-17 23:46:15)

Use Timestamp struct instead of time.Time
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.



codecov-commenter: # [Codecov](https://codecov.io/gh/amzn/ion-go/pull/98?src=pr&el=h1) Report
> Merging [#98](https://codecov.io/gh/amzn/ion-go/pull/98?src=pr&el=desc) into [master](https://codecov.io/gh/amzn/ion-go/commit/0c30b5d01a9d6790add04dae077b4398ed11d28e&el=desc) will **decrease** coverage by `0.02%`.
> The diff coverage is `83.44%`.

[![Impacted file tree graph](https://codecov.io/gh/amzn/ion-go/pull/98/graphs/tree.svg?width=650&height=150&src=pr&token=OBDT1TNoNE)](https://codecov.io/gh/amzn/ion-go/pull/98?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master      #98      +/-   ##
==========================================
- Coverage   77.11%   77.09%   -0.03%     
==========================================
  Files          22       23       +1     
  Lines        4720     4806      +86     
==========================================
+ Hits         3640     3705      +65     
- Misses        664      685      +21     
  Partials      416      416              
```


| [Impacted Files](https://codecov.io/gh/amzn/ion-go/pull/98?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [ion/consts.go](https://codecov.io/gh/amzn/ion-go/pull/98/diff?src=pr&el=tree#diff-aW9uL2NvbnN0cy5nbw==) | `100.00% <ø> (ø)` | |
| [ion/writer.go](https://codecov.io/gh/amzn/ion-go/pull/98/diff?src=pr&el=tree#diff-aW9uL3dyaXRlci5nbw==) | `90.47% <ø> (ø)` | |
| [ion/reader.go](https://codecov.io/gh/amzn/ion-go/pull/98/diff?src=pr&el=tree#diff-aW9uL3JlYWRlci5nbw==) | `52.94% <50.00%> (ø)` | |
| [ion/timestamp.go](https://codecov.io/gh/amzn/ion-go/pull/98/diff?src=pr&el=tree#diff-aW9uL3RpbWVzdGFtcC5nbw==) | `60.00% <60.00%> (ø)` | |
| [ion/bitstream.go](https://codecov.io/gh/amzn/ion-go/pull/98/diff?src=pr&el=tree#diff-aW9uL2JpdHN0cmVhbS5nbw==) | `82.27% <94.73%> (+0.24%)` | :arrow_up: |
| [ion/binarywriter.go](https://codecov.io/gh/amzn/ion-go/pull/98/diff?src=pr&el=tree#diff-aW9uL2JpbmFyeXdyaXRlci5nbw==) | `77.89% <100.00%> (ø)` | |
| [ion/bits.go](https://codecov.io/gh/amzn/ion-go/pull/98/diff?src=pr&el=tree#diff-aW9uL2JpdHMuZ28=) | `100.00% <100.00%> (ø)` | |
| [ion/marshal.go](https://codecov.io/gh/amzn/ion-go/pull/98/diff?src=pr&el=tree#diff-aW9uL21hcnNoYWwuZ28=) | `78.80% <100.00%> (ø)` | |
| [ion/textutils.go](https://codecov.io/gh/amzn/ion-go/pull/98/diff?src=pr&el=tree#diff-aW9uL3RleHR1dGlscy5nbw==) | `79.77% <100.00%> (+1.09%)` | :arrow_up: |
| [ion/textwriter.go](https://codecov.io/gh/amzn/ion-go/pull/98/diff?src=pr&el=tree#diff-aW9uL3RleHR3cml0ZXIuZ28=) | `64.81% <100.00%> (ø)` | |
| ... and [2 more](https://codecov.io/gh/amzn/ion-go/pull/98/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/amzn/ion-go/pull/98?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/amzn/ion-go/pull/98?src=pr&el=footer). Last update [0c30b5d...52aaa4f](https://codecov.io/gh/amzn/ion-go/pull/98?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/cmd/ion-go/eventwriter.go b/cmd/ion-go/eventwriter.go
index 55717c5..e8a236e 100644
--- a/cmd/ion-go/eventwriter.go
+++ b/cmd/ion-go/eventwriter.go
@@ -4,7 +4,6 @@ import (
 	"io"
 	"math/big"
 	"strings"
-	"time"
 
 	"github.com/amzn/ion-go/ion"
 )
@@ -106,7 +105,7 @@ func (e *eventwriter) WriteDecimal(val *ion.Decimal) error {
 	})
 }
 
-func (e *eventwriter) WriteTimestamp(val time.Time) error {
+func (e *eventwriter) WriteTimestamp(val ion.Timestamp) error {
 	return e.write(event{
 		EventType: scalar,
 		IonType:   iontype(ion.TimestampType),
diff --git a/cmd/ion-go/main.go b/cmd/ion-go/main.go
index 2423a1f..c582b56 100644
--- a/cmd/ion-go/main.go
+++ b/cmd/ion-go/main.go
@@ -4,7 +4,6 @@ import (
 	"errors"
 	"fmt"
 	"os"
-	"time"
 
 	"github.com/amzn/ion-go/internal"
 	"github.com/amzn/ion-go/ion"
@@ -65,7 +64,7 @@ func printVersion() {
 		w.FieldName("version")
 		w.WriteString(internal.GitCommit)
 
-		buildtime, err := time.Parse(time.RFC3339, internal.BuildTime)
+		buildtime, err := ion.NewTimestampFromStr(internal.BuildTime, ion.TimestampPrecisionSecond, ion.TimezoneUTC)
 		if err == nil {
 			w.FieldName("build_time")
 			w.WriteTimestamp(buildtime)
diff --git a/cmd/ion-go/nopwriter.go b/cmd/ion-go/nopwriter.go
index 50e1a91..d31d5f9 100644
--- a/cmd/ion-go/nopwriter.go
+++ b/cmd/ion-go/nopwriter.go
@@ -2,7 +2,6 @@ package main
 
 import (
 	"math/big"
-	"time"
 
 	"github.com/amzn/ion-go/ion"
 )
@@ -58,7 +57,7 @@ func (nopwriter) WriteDecimal(*ion.Decimal) error {
 	return nil
 }
 
-func (nopwriter) WriteTimestamp(time.Time) error {
+func (nopwriter) WriteTimestamp(ion.Timestamp) error {
 	return nil
 }
 
diff --git a/cmd/ion-go/process.go b/cmd/ion-go/process.go
index 2ccc821..7f25cba 100644
--- a/cmd/ion-go/process.go
+++ b/cmd/ion-go/process.go
@@ -254,7 +254,7 @@ func (p *processor) process(in ion.Reader) error {
 			err = p.out.WriteDecimal(val)
 
 		case ion.TimestampType:
-			val, err := in.TimeValue()
+			val, err := in.TimestampValue()
 			if err != nil {
 				return p.error(read, err)
 			}
diff --git a/ion/binarywriter.go b/ion/binarywriter.go
index e0eea59..f03b5de 100644
--- a/ion/binarywriter.go
+++ b/ion/binarywriter.go
@@ -207,18 +207,18 @@ func (w *binaryWriter) WriteDecimal(val *Decimal) error {
 }
 
 // WriteTimestamp writes a timestamp value.
-func (w *binaryWriter) WriteTimestamp(val time.Time) error {
-	_, offset := val.Zone()
+func (w *binaryWriter) WriteTimestamp(val Timestamp) error {
+	_, offset := val.dateTime.Zone()
 	offset /= 60
-	utc := val.In(time.UTC)
+	val.dateTime = val.dateTime.In(time.UTC)
 
-	vlen := timeLen(offset, utc)
+	vlen := timestampLen(offset, val)
 	buflen := vlen + tagLen(vlen)
 
 	buf := make([]byte, 0, buflen)
 
 	buf = appendTag(buf, 0x60, vlen)
-	buf = appendTime(buf, offset, utc)
+	buf = appendTimestamp(buf, offset, val)
 
 	return w.writeValue("Writer.WriteTimestamp", buf)
 }
diff --git a/ion/bits.go b/ion/bits.go
index f377270..e44c5df 100644
--- a/ion/bits.go
+++ b/ion/bits.go
@@ -2,7 +2,6 @@ package ion
 
 import (
 	"math/big"
-	"time"
 )
 
 // uintLen pre-calculates the length, in bytes, of the given uint value.
@@ -244,41 +243,86 @@ func appendTag(b []byte, code byte, len uint64) []byte {
 	return appendVarUint(b, len)
 }
 
-// timeLen pre-calculates the length, in bytes, of the given time value.
-func timeLen(offset int, utc time.Time) uint64 {
-	ret := varIntLen(int64(offset))
+// timestampLen pre-calculates the length, in bytes, of the given timestamp value.
+func timestampLen(offset int, utc Timestamp) uint64 {
+	var ret uint64
 
-	// Almost certainly two but let's be safe.
-	ret += varUintLen(uint64(utc.Year()))
+	if utc.kind == TimezoneUnspecified {
+		ret = 1
+	} else {
+		ret = varIntLen(int64(offset))
+	}
+
+	// We expect at least Year precision.
+	ret += varUintLen(uint64(utc.dateTime.Year()))
 
 	// Month, day, hour, minute, and second are all guaranteed to be one byte.
-	ret += 5
+	switch utc.precision {
+	case TimestampPrecisionMonth:
+		ret++
+	case TimestampPrecisionDay:
+		ret += 2
+	case TimestampPrecisionMinute:
+		// Hour and Minute combined
+		ret += 4
+	case TimestampPrecisionSecond, TimestampPrecisionNanosecond:
+		ret += 5
+	}
+
+	if utc.precision == TimestampPrecisionNanosecond && utc.numFractionalSeconds > 0 {
+		ret++ // For fractional seconds precision indicator
 
-	ns := utc.Nanosecond()
-	if ns > 0 {
-		ret++ // varIntLen(-9)
-		ret += intLen(int64(ns))
+		ns := utc.TruncatedNanoseconds()
+		if ns > 0 {
+			ret += intLen(int64(ns))
+		}
 	}
 
 	return ret
 }
 
-// appendTime appends a timestamp value
-func appendTime(b []byte, offset int, utc time.Time) []byte {
-	b = appendVarInt(b, int64(offset))
+// appendTimestamp appends a timestamp value
+func appendTimestamp(b []byte, offset int, utc Timestamp) []byte {
+	if utc.kind == TimezoneUnspecified {
+		// Unknown offset
+		b = append(b, 0xC0)
+	} else {
+		b = appendVarInt(b, int64(offset))
+	}
 
-	b = appendVarUint(b, uint64(utc.Year()))
-	b = appendVarUint(b, uint64(utc.Month()))
-	b = appendVarUint(b, uint64(utc.Day()))
+	// We expect at least Year precision.
+	b = appendVarUint(b, uint64(utc.dateTime.Year()))
+
+	switch utc.precision {
+	case TimestampPrecisionMonth:
+		b = appendVarUint(b, uint64(utc.dateTime.Month()))
+	case TimestampPrecisionDay:
+		b = appendVarUint(b, uint64(utc.dateTime.Month()))
+		b = appendVarUint(b, uint64(utc.dateTime.Day()))
+	case TimestampPrecisionMinute:
+		b = appendVarUint(b, uint64(utc.dateTime.Month()))
+		b = appendVarUint(b, uint64(utc.dateTime.Day()))
+
+		// The hour and minute is considered as a single component.
+		b = appendVarUint(b, uint64(utc.dateTime.Hour()))
+		b = appendVarUint(b, uint64(utc.dateTime.Minute()))
+	case TimestampPrecisionSecond, TimestampPrecisionNanosecond:
+		b = appendVarUint(b, uint64(utc.dateTime.Month()))
+		b = appendVarUint(b, uint64(utc.dateTime.Day()))
+
+		// The hour and minute is considered as a single component.
+		b = appendVarUint(b, uint64(utc.dateTime.Hour()))
+		b = appendVarUint(b, uint64(utc.dateTime.Minute()))
+		b = appendVarUint(b, uint64(utc.dateTime.Second()))
+	}
 
-	b = appendVarUint(b, uint64(utc.Hour()))
-	b = appendVarUint(b, uint64(utc.Minute()))
-	b = appendVarUint(b, uint64(utc.Second()))
+	if utc.precision == TimestampPrecisionNanosecond && utc.numFractionalSeconds > 0 {
+		b = append(b, utc.numFractionalSeconds|0xC0)
 
-	ns := utc.Nanosecond()
-	if ns > 0 {
-		b = appendVarInt(b, -9)
-		b = appendInt(b, int64(ns))
+		ns := utc.TruncatedNanoseconds()
+		if ns > 0 {
+			b = appendInt(b, int64(ns))
+		}
 	}
 
 	return b
diff --git a/ion/bitstream.go b/ion/bitstream.go
index d6c0243..583cb58 100644
--- a/ion/bitstream.go
+++ b/ion/bitstream.go
@@ -8,7 +8,6 @@ import (
 	"io"
 	"math"
 	"math/big"
-	"time"
 	"unicode/utf8"
 )
 
@@ -515,58 +514,85 @@ func (b *bitstream) ReadDecimal() (*Decimal, error) {
 }
 
 // ReadTimestamp reads a timestamp value.
-func (b *bitstream) ReadTimestamp() (time.Time, error) {
+func (b *bitstream) ReadTimestamp() (Timestamp, error) {
 	if b.code != bitcodeTimestamp {
 		panic("not a timestamp")
 	}
 
-	len := b.len
+	length := b.len
 
-	offset, olen, err := b.readVarIntLen(len)
+	offset, osign, olen, err := b.readVarIntLen(length)
 	if err != nil {
-		return time.Time{}, err
+		return Timestamp{}, err
 	}
-	len -= olen
+	length -= olen
 
 	ts := []int{1, 1, 1, 0, 0, 0}
-	for i := 0; len > 0 && i < 6; i++ {
-		val, vlen, err := b.readVarUintLen(len)
+	precision := TimestampNoPrecision
+	for i := 0; length > 0 && i < 6 && precision < TimestampPrecisionSecond; i++ {
+		val, vlen, err := b.readVarUintLen(length)
 		if err != nil {
-			return time.Time{}, err
+			return Timestamp{}, err
 		}
-		len -= vlen
+		length -= vlen
 		ts[i] = int(val)
 
-		// When i is 3, it means we are setting hour component. A timestamp with
-		// hour, must follow by minute. Hence, len cannot be zero at this point.
-		if i == 3 && len == 0 {
-			return time.Time{},
-				&SyntaxError{"Invalid timestamp - Hour cannot be present without minute", b.pos}
+		// When i is 3, it means we are setting the hour component. A timestamp with an hour
+		// component must also have a minute component. Hence, length cannot be zero at this point.
+		if i == 3 {
+			if length == 0 {
+				return Timestamp{}, &SyntaxError{"Invalid timestamp - Hour cannot be present without minute", b.pos}
+			}
+		} else {
+			// Update precision as we read the timestamp.
+			// We don't update precision when i is 3 because there is no Hour precision.
+			precision++
 		}
 	}
 
-	nsecs, overflow, err := b.readNsecs(len)
-	if err != nil {
-		return time.Time{}, err
-	}
+	nsecs := 0
+	overflow := false
+	fractionPrecision := uint8(0)
 
-	b.state = b.stateAfterValue()
-	b.clear()
+	// Check the fractional seconds part of the timestamp.
+	if length > 0 {
+		// First byte indicates number of precision units in fractional seconds.
+		fracSecsBytes, err := b.in.Peek(1)
+		if err != nil {
+			return Timestamp{}, err
+		}
 
-	return tryCreateTimeWithNSecAndOffset(ts, nsecs, overflow, offset)
-}
+		nsecs, overflow, err = b.readNsecs(length)
+		if err != nil {
+			return Timestamp{}, err
+		}
 
-func tryCreateTimeWithNSecAndOffset(ts []int, nsecs int, overflow bool, offset int64) (time.Time, error) {
-	date := time.Date(ts[0], time.Month(ts[1]), ts[2], ts[3], ts[4], ts[5], nsecs, time.UTC)
-	// time.Date converts 2000-01-32 input to 2000-02-01
-	if ts[0] != date.Year() || time.Month(ts[1]) != date.Month() || ts[2] != date.Day() {
-		return time.Time{}, fmt.Errorf("ion: invalid timestamp")
+		if nsecs > 0 {
+			fractionPrecision = 9
+
+			// Adjust fractionPrecision for each trailing zero.
+			// ie. .123456000 should have 6 fractionPrecision instead of 9
+			ns := nsecs
+			for ns > 0 && (ns%10) == 0 {
+				ns /= 10
+				fractionPrecision--
+			}
+			precision = TimestampPrecisionNanosecond
+		} else if len(fracSecsBytes) > 0 && fracSecsBytes[0] > 0xC0 && (fracSecsBytes[0]^0xC0) > 0 {
+			fractionPrecision = fracSecsBytes[0] ^ 0xC0
+			precision = TimestampPrecisionNanosecond
+		}
 	}
 
-	if overflow {
-		date = date.Add(time.Second)
+	timestamp, err := tryCreateTimestamp(ts, nsecs, overflow, offset, osign, precision, fractionPrecision)
+	if err != nil {
+		return Timestamp{}, err
 	}
-	return date.In(time.FixedZone("fixed", int(offset)*60)), nil
+
+	b.state = b.stateAfterValue()
+	b.clear()
+
+	return timestamp, nil
 }
 
 // ReadNsecs reads the fraction part of a timestamp and rounds to nanoseconds.
@@ -605,7 +631,7 @@ func (b *bitstream) readDecimal(len uint64) (*Decimal, error) {
 	coef := new(big.Int)
 
 	if len > 0 {
-		val, vlen, err := b.readVarIntLen(len)
+		val, _, vlen, err := b.readVarIntLen(len)
 		if err != nil {
 			return nil, err
 		}
@@ -809,10 +835,10 @@ func (b *bitstream) remaining() uint64 {
 }
 
 // ReadVarIntLen reads a variable-length-encoded int of at most max bytes,
-// returning the value and its actual length in bytes
-func (b *bitstream) readVarIntLen(max uint64) (int64, uint64, error) {
+// returning the value, the sign, and its actual length in bytes
+func (b *bitstream) readVarIntLen(max uint64) (int64, int64, uint64, error) {
 	if max == 0 {
-		return 0, 0, &SyntaxError{"varint too large", b.pos}
+		return 0, 0, 0, &SyntaxError{"varint too large", b.pos}
 	}
 	if max > 10 {
 		max = 10
@@ -821,7 +847,7 @@ func (b *bitstream) readVarIntLen(max uint64) (int64, uint64, error) {
 	// Read the first byte, which contains the sign bit.
 	c, err := b.read1()
 	if err != nil {
-		return 0, 0, err
+		return 0, 0, 0, err
 	}
 
 	sign := int64(1)
@@ -834,17 +860,17 @@ func (b *bitstream) readVarIntLen(max uint64) (int64, uint64, error) {
 
 	// Check if that was the last (only) byte.
 	if c&0x80 != 0 {
-		return val * sign, len, nil
+		return val * sign, sign, len, nil
 	}
 
 	for {
 		if len >= max {
-			return 0, 0, &SyntaxError{"varint too large", b.pos - len}
+			return 0, 0, 0, &SyntaxError{"varint too large", b.pos - len}
 		}
 
 		c, err := b.read1()
 		if err != nil {
-			return 0, 0, err
+			return 0, 0, 0, err
 		}
 
 		val <<= 7
@@ -852,7 +878,7 @@ func (b *bitstream) readVarIntLen(max uint64) (int64, uint64, error) {
 		len++
 
 		if c&0x80 != 0 {
-			return val * sign, len, nil
+			return val * sign, sign, len, nil
 		}
 	}
 }
diff --git a/ion/consts.go b/ion/consts.go
index d4029f1..7965e79 100644
--- a/ion/consts.go
+++ b/ion/consts.go
@@ -3,7 +3,6 @@ package ion
 import (
 	"math/big"
 	"reflect"
-	"time"
 )
 
 var binaryNulls = func() []byte {
@@ -49,6 +48,6 @@ var hexChars = []byte{
 	'8', '9', 'A', 'B', 'C', 'D', 'E', 'F',
 }
 
-var timeType = reflect.TypeOf(time.Time{})
+var timestampType = reflect.TypeOf(Timestamp{})
 var decimalType = reflect.TypeOf(Decimal{})
 var bigIntType = reflect.TypeOf(big.Int{})
diff --git a/ion/marshal.go b/ion/marshal.go
index f870180..6cace3e 100644
--- a/ion/marshal.go
+++ b/ion/marshal.go
@@ -7,7 +7,6 @@ import (
 	"math/big"
 	"reflect"
 	"sort"
-	"time"
 )
 
 // EncoderOpts holds bit-flag options for an Encoder.
@@ -308,8 +307,8 @@ func (m *Encoder) encodeStruct(v reflect.Value) error {
 	}
 
 	t := v.Type()
-	if t == timeType {
-		return m.encodeTime(v)
+	if t == timestampType {
+		return m.encodeTimestamp(v)
 	}
 	if t == decimalType {
 		return m.encodeDecimal(v)
@@ -347,9 +346,9 @@ FieldLoop:
 	return m.w.EndStruct()
 }
 
-// EncodeTime encodes a time.Time to the output writer as an Ion timestamp.
-func (m *Encoder) encodeTime(v reflect.Value) error {
-	t := v.Interface().(time.Time)
+// encodeTimestamp encodes a timestamp to the output writer as an Ion timestamp.
+func (m *Encoder) encodeTimestamp(v reflect.Value) error {
+	t := v.Interface().(Timestamp)
 	return m.w.WriteTimestamp(t)
 }
 
diff --git a/ion/reader.go b/ion/reader.go
index eefec7d..fbd270a 100644
--- a/ion/reader.go
+++ b/ion/reader.go
@@ -7,7 +7,6 @@ import (
 	"math"
 	"math/big"
 	"strings"
-	"time"
 )
 
 // A Reader reads a stream of Ion values.
@@ -136,9 +135,9 @@ type Reader interface {
 	// makes sense). It returns an error if the current value is not an Ion decimal.
 	DecimalValue() (*Decimal, error)
 
-	// TimeValue returns the current value as a timestamp (if that makes sense). It returns
+	// TimestampValue returns the current value as a timestamp (if that makes sense). It returns
 	// an error if the current value is not an Ion timestamp.
-	TimeValue() (time.Time, error)
+	TimestampValue() (Timestamp, error)
 
 	// StringValue returns the current value as a string (if that makes sense). It returns
 	// an error if the current value is not an Ion symbol or an Ion string.
@@ -348,15 +347,15 @@ func (r *reader) DecimalValue() (*Decimal, error) {
 	return r.value.(*Decimal), nil
 }
 
-// TimeValue returns the current value as a time.
-func (r *reader) TimeValue() (time.Time, error) {
+// TimestampValue returns the current value as a Timestamp.
+func (r *reader) TimestampValue() (Timestamp, error) {
 	if r.valueType != TimestampType {
-		return time.Time{}, &UsageError{"Reader.TimestampValue", "value is not a timestamp"}
+		return Timestamp{}, &UsageError{"Reader.TimestampValue", "value is not a timestamp"}
 	}
 	if r.value == nil {
-		return time.Time{}, nil
+		return Timestamp{}, nil
 	}
-	return r.value.(time.Time), nil
+	return r.value.(Timestamp), nil
 }
 
 // StringValue returns the current value as a string.
diff --git a/ion/textutils.go b/ion/textutils.go
index 206bc41..30880e8 100644
--- a/ion/textutils.go
+++ b/ion/textutils.go
@@ -1,30 +1,10 @@
 package ion
 
 import (
-	"fmt"
 	"io"
 	"math/big"
 	"strconv"
 	"strings"
-	"time"
-)
-
-const (
-	// layoutMinutesAndOffset layout for time.date with yyyy-mm-ddThh:mm±hh:mm format. time.Parse()
-	// uses "2006-01-02T15:04Z07:00" explicitly as a string value to identify this format.
-	layoutMinutesAndOffset = "2006-01-02T15:04Z07:00"
-
-	// layoutMinutesZulu layout for time.date with yyyy-mm-ddThh:mmZ format. time.Parse()
-	// uses "2006-01-02T15:04Z07:00" explicitly as a string value to identify this format.
-	layoutMinutesZulu = "2006-01-02T15:04Z"
-
-	// layoutNanosecondsAndOffset layout for time.date of RFC3339Nano format.
-	// Such as 2006-01-02T15:04:05.999999999Z07:00.
-	layoutNanosecondsAndOffset = time.RFC3339Nano
-
-	// layoutSecondsAndOffset layout for time.date of RFC3339 format.
-	// Such as: 2006-01-02T15:04:05Z07:00.
-	layoutSecondsAndOffset = time.RFC3339
 )
 
 // Does this symbol need to be quoted in text form?
@@ -321,160 +301,6 @@ func parseInt(str string, radix int) (interface{}, error) {
 	return bi, nil
 }
 
-func parseTimestamp(val string) (time.Time, error) {
-	if len(val) < 5 {
-		return invalidTimestamp(val)
-	}
-
-	year, err := strconv.ParseInt(val[:4], 10, 32)
-	if err != nil || year < 1 {
-		return invalidTimestamp(val)
-	}
-	if len(val) == 5 && (val[4] == 't' || val[4] == 'T') {
-		// yyyyT
-		return tryCreateTimeDate(val, year, 1, 1)
-	}
-	if val[4] != '-' {
-		return invalidTimestamp(val)
-	}
-
-	if len(val) < 8 {
-		return invalidTimestamp(val)
-	}
-
-	month, err := strconv.ParseInt(val[5:7], 10, 32)
-	if err != nil {
-		return invalidTimestamp(val)
-	}
-
-	if len(val) == 8 && (val[7] == 't' || val[7] == 'T') {
-		// yyyy-mmT
-		return tryCreateTimeDate(val, year, month, 1)
-	}
-	if val[7] != '-' {
-		return invalidTimestamp(val)
-	}
-
-	if len(val) < 10 {
-		return invalidTimestamp(val)
-	}
-
-	day, err := strconv.ParseInt(val[8:10], 10, 32)
-	if err != nil {
-		return invalidTimestamp(val)
-	}
-
-	if len(val) == 10 || (len(val) == 11 && (val[10] == 't' || val[10] == 'T')) {
-		// yyyy-mm-dd or yyyy-mm-ddT
-		return tryCreateTimeDate(val, year, month, day)
-	}
-	if val[10] != 't' && val[10] != 'T' {
-		return invalidTimestamp(val)
-	}
-
-	// At this point timestamp must have hour:minute
-	if len(val) < 17 {
-		return invalidTimestamp(val)
-	}
-	if val[16] == 'z' || val[16] == 'Z' {
-		return time.Parse(layoutMinutesZulu, val)
-	}
-	if val[16] == '+' || val[16] == '-' {
-		if isValidOffset(val, 16) {
-			return time.Parse(layoutMinutesAndOffset, val)
-		}
-		return invalidTimestamp(val)
-	}
-	if val[16] == ':' {
-		//yyyy-mm-ddThh:mm:ss
-		if len(val) < 20 {
-			return invalidTimestamp(val)
-		}
-
-		idx := 19
-		if val[idx] == '.' {
-			idx++
-			for idx < len(val) && isDigit(int(val[idx])) {
-				idx++
-			}
-		}
-
-		if val[idx] == 'z' || val[idx] == 'Z' {
-			if idx >= 29 {
-				// Greater than 9 fractional seconds.
-				return roundFractionalSeconds(val, idx)
-			}
-			return time.Parse(layoutSecondsAndOffset, val)
-		}
-
-		if val[idx] == '+' || val[idx] == '-' {
-			if isValidOffset(val, idx) {
-				if idx >= 29 {
-					// Greater than 9 fractional seconds.
-					return roundFractionalSeconds(val, idx)
-				}
-				return time.Parse(layoutSecondsAndOffset, val)
-			}
-			return invalidTimestamp(val)
-		}
-	}
-	return invalidTimestamp(val)
-}
-
-func tryCreateTimeDate(val string, year int64, month int64, day int64) (time.Time, error) {
-	date := time.Date(int(year), time.Month(month), int(day), 0, 0, 0, 0, time.UTC)
-	// time.Date converts 2000-01-32 input to 2000-02-01
-	if int(year) != date.Year() || time.Month(month) != date.Month() || int(day) != date.Day() {
-		return invalidTimestamp(val)
-	}
-	return date, nil
-}
-
-func isValidOffset(val string, idx int) bool {
-	// +hh:mm
-	if idx+5 > len(val) || val[idx+3] != ':' {
-		return false
-	}
-
-	hourOffset, errH := strconv.ParseInt(val[idx+1:idx+3], 10, 32)
-	minuteOffset, errM := strconv.ParseInt(val[idx+4:], 10, 32)
-	if errH != nil || errM != nil {
-		return false
-	}
-
-	return hourOffset < 24 && minuteOffset < 60
-}
-
-func invalidTimestamp(val string) (time.Time, error) {
-	return time.Time{}, fmt.Errorf("ion: invalid timestamp: %v", val)
-}
-
-func roundFractionalSeconds(val string, idx int) (time.Time, error) {
-	// Convert to float to perform rounding.
-	floatValue, err := strconv.ParseFloat(val[18:idx], 64)
-	if err != nil {
-		return invalidTimestamp(val)
-	}
-	roundedStringValue := fmt.Sprintf("%.9f", floatValue)
-
-	roundedFloatValue, err := strconv.ParseFloat(roundedStringValue, 64)
-	if err != nil {
-		return invalidTimestamp(val)
-	}
-
-	// Microsecond overflow 9.9999999999 -> 10.00000000.
-	if roundedFloatValue == 10 {
-		roundedStringValue := "9.000000000"
-		val = val[:18] + roundedStringValue + val[idx:]
-		timeValue, err := time.Parse(layoutNanosecondsAndOffset, val)
-		if err != nil {
-			return invalidTimestamp(val)
-		}
-
-		timeValue = timeValue.Add(time.Second)
-		return timeValue, err
-	}
-
-	val = val[:18] + roundedStringValue + val[idx:]
-	return time.Parse(layoutNanosecondsAndOffset, val)
+func parseTimestamp(val string) (Timestamp, error) {
+	return ParseTimestampFromStr(val)
 }
diff --git a/ion/textwriter.go b/ion/textwriter.go
index 47bebaa..c362149 100644
--- a/ion/textwriter.go
+++ b/ion/textwriter.go
@@ -5,7 +5,6 @@ import (
 	"fmt"
 	"io"
 	"math/big"
-	"time"
 )
 
 // TextWriterOpts defines a set of bit flag options for text writers.
@@ -90,8 +89,8 @@ func (w *textWriter) WriteDecimal(val *Decimal) error {
 }
 
 // WriteTimestamp writes a timestamp.
-func (w *textWriter) WriteTimestamp(val time.Time) error {
-	return w.writeValue("Writer.WriteTimestamp", val.Format(time.RFC3339Nano))
+func (w *textWriter) WriteTimestamp(val Timestamp) error {
+	return w.writeValue("Writer.WriteTimestamp", val.String())
 }
 
 // WriteSymbol writes a symbol.
diff --git a/ion/timestamp.go b/ion/timestamp.go
new file mode 100644
index 0000000..d0f0224
--- /dev/null
+++ b/ion/timestamp.go
@@ -0,0 +1,501 @@
+package ion
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+	"time"
+)
+
+// TimestampPrecision is for tracking the precision of a timestamp
+type TimestampPrecision uint8
+
+// Possible TimestampPrecision values
+const (
+	TimestampNoPrecision TimestampPrecision = iota
+	TimestampPrecisionYear
+	TimestampPrecisionMonth
+	TimestampPrecisionDay
+	TimestampPrecisionMinute
+	TimestampPrecisionSecond
+	TimestampPrecisionNanosecond
+)
+
+func (tp TimestampPrecision) String() string {
+	switch tp {
+	case TimestampNoPrecision:
+		return "<no precision>"
+	case TimestampPrecisionYear:
+		return "Year"
+	case TimestampPrecisionMonth:
+		return "Month"
+	case TimestampPrecisionDay:
+		return "Day"
+	case TimestampPrecisionMinute:
+		return "Minute"
+	case TimestampPrecisionSecond:
+		return "Second"
+	case TimestampPrecisionNanosecond:
+		return "Nanosecond"
+	default:
+		return fmt.Sprintf("<unknown precision %v>", uint8(tp))
+	}
+}
+
+// Layout returns a suitable format string to be used in time.Parse() or time.Format().
+// The idea of the format string is to format the particular date Mon Jan 2 15:04:05 MST 2006 (Unix time 1136239445)
+// in the desired layout we want to use to format other dates.
+func (tp TimestampPrecision) Layout(kind TimezoneKind, precisionUnits uint8) string {
+	switch tp {
+	case TimestampPrecisionYear:
+		return "2006T"
+	case TimestampPrecisionMonth:
+		return "2006-01T"
+	case TimestampPrecisionDay:
+		return "2006-01-02T"
+	case TimestampPrecisionMinute:
+		if kind == TimezoneUnspecified {
+			return "2006-01-02T15:04-07:00"
+		}
+		return "2006-01-02T15:04Z07:00"
+	case TimestampPrecisionSecond:
+		if kind == TimezoneUnspecified {
+			return "2006-01-02T15:04:05-07:00"
+		}
+		return "2006-01-02T15:04:05Z07:00"
+	case TimestampPrecisionNanosecond:
+		layout := strings.Builder{}
+		layout.WriteString("2006-01-02T15:04:05")
+
+		if precisionUnits > 9 {
+			precisionUnits = 9
+		}
+
+		if precisionUnits > 0 {
+			layout.WriteByte('.')
+			for i := uint8(0); i < precisionUnits; i++ {
+				layout.WriteByte('9')
+			}
+		}
+
+		if kind == TimezoneUnspecified {
+			layout.WriteString("-07:00")
+		} else {
+			layout.WriteString("Z07:00")
+		}
+
+		return layout.String()
+	}
+
+	return time.RFC3339Nano
+}
+
+// TimezoneKind tracks the type of timezone.
+type TimezoneKind uint8
+
+const (
+	// TimezoneUnspecified is for timestamps without a timezone such as dates with no time component (ie. Year/Month/Day precision).
+	// Negative zero offsets (ie. yyyy-mm-ddThh:mm-00:00) are also considered Unspecified.
+	TimezoneUnspecified TimezoneKind = iota
+
+	// TimezoneUTC is for UTC timestamps and they are usually denoted with a trailing 'Z' (ie. yyyy-mm-ddThh:mmZ).
+	// Timestamps with a positive zero offset (ie. yyyy-mm-ddThh:mm+00:00) are also considered UTC.
+	TimezoneUTC
+
+	// TimezoneLocal is for timestamps that have a non-zero offset from UTC (ie. 2001-02-03T04:05+08:30, 2009-05-18T16:20-04:00).
+	TimezoneLocal
+)
+
+// Timestamp struct
+type Timestamp struct {
+	dateTime             time.Time
+	precision            TimestampPrecision
+	kind                 TimezoneKind
+	numFractionalSeconds uint8
+}
+
+// NewDateTimestamp constructor meant for timestamps that only have a date portion (ie. no time portion).
+func NewDateTimestamp(dateTime time.Time, precision TimestampPrecision) Timestamp {
+	return Timestamp{dateTime, precision, TimezoneUnspecified, 0}
+}
+
+// NewTimestamp constructor
+func NewTimestamp(dateTime time.Time, precision TimestampPrecision, kind TimezoneKind) Timestamp {
+	if precision <= TimestampPrecisionDay {
+		// Timestamps with Year, Month, or Day precision necessarily have TimezoneUnspecified timezone.
+		kind = TimezoneUnspecified
+	}
+	return Timestamp{dateTime, precision, kind, 0}
+}
+
+// NewTimestampWithFractionalSeconds constructor
+func NewTimestampWithFractionalSeconds(dateTime time.Time, precision TimestampPrecision, kind TimezoneKind, fractionPrecision uint8) Timestamp {
+	if fractionPrecision > 9 {
+		// 9 is the max precision supported
+		fractionPrecision = 9
+	}
+	return Timestamp{dateTime, precision, kind, fractionPrecision}
+}
+
+// NewTimestampFromStr constructor
+func NewTimestampFromStr(dateStr string, precision TimestampPrecision, kind TimezoneKind) (Timestamp, error) {
+	// Count number of fractional seconds units.
+	fractionUnits := uint8(0)
+	if precision >= TimestampPrecisionNanosecond {
+		pointIdx := strings.LastIndex(dateStr, ".")
+		if pointIdx != -1 {
+			nonZeroFraction := false
+
+			idx := pointIdx + 1
+			for idx < len(dateStr) && isDigit(int(dateStr[idx])) {
+				if dateStr[idx] != '0' {
+					nonZeroFraction = true
+				}
+				fractionUnits++
+				idx++
+			}
+
+			if idx == len(dateStr) {
+				return Timestamp{}, fmt.Errorf("ion: invalid date string '%v'", dateStr)
+			}
+
+			// We do not want to include trailing zeros for a non-zero fraction (ie. .1234000 -> .1234)
+			// So we adjust fractionUnits accordingly.
+			if nonZeroFraction {
+				idx--
+				for idx > pointIdx && dateStr[idx] == '0' {
+					fractionUnits--
+					idx--
+				}
+			}
+		}
+	}
+
+	dateTime, err := time.Parse(precision.Layout(kind, fractionUnits), dateStr)
+	if err != nil {
+		return Timestamp{}, err
+	}
+
+	return NewTimestampWithFractionalSeconds(dateTime, precision, kind, fractionUnits), nil
+}
+
+func invalidTimestamp(val string) (Timestamp, error) {
+	return Timestamp{}, fmt.Errorf("ion: invalid timestamp: %v", val)
+}
+
+func tryCreateDateTimestamp(year, month, day int, precision TimestampPrecision) (Timestamp, error) {
+	date := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC)
+
+	// time.Date converts 2000-01-32 input to 2000-02-01
+	if year != date.Year() || time.Month(month) != date.Month() || day != date.Day() {
+		return Timestamp{}, fmt.Errorf("ion: invalid timestamp")
+	}
+
+	return NewDateTimestamp(date, precision), nil
+}
+
+func tryCreateTimestamp(ts []int, nsecs int, overflow bool, offset, sign int64, precision TimestampPrecision, fractionPrecision uint8) (Timestamp, error) {
+	date := time.Date(ts[0], time.Month(ts[1]), ts[2], ts[3], ts[4], ts[5], nsecs, time.UTC)
+	// time.Date converts 2000-01-32 input to 2000-02-01
+	if ts[0] != date.Year() || time.Month(ts[1]) != date.Month() || ts[2] != date.Day() {
+		return Timestamp{}, fmt.Errorf("ion: invalid timestamp")
+	}
+
+	if precision <= TimestampPrecisionDay {
+		return NewDateTimestamp(date, precision), nil
+	}
+
+	if overflow {
+		date = date.Add(time.Second)
+	}
+
+	if offset == 0 {
+		if sign == -1 {
+			// Negative zero timezone offset is Unspecified
+			return NewTimestampWithFractionalSeconds(date, precision, TimezoneUnspecified, fractionPrecision), nil
+		}
+
+		// Positive zero timezone offset is UTC
+		return NewTimestampWithFractionalSeconds(date, precision, TimezoneUTC, fractionPrecision), nil
+	}
+
+	date = date.In(time.FixedZone("fixed", int(offset)*60))
+
+	// Non-zero offset is Local
+	return NewTimestampWithFractionalSeconds(date, precision, TimezoneLocal, fractionPrecision), nil
+}
+
+// ParseTimestampFromStr parses a timestamp string and returns an ion timestamp.
+func ParseTimestampFromStr(dateStr string) (Timestamp, error) {
+	if len(dateStr) < 5 {
+		return invalidTimestamp(dateStr)
+	}
+
+	year, err := strconv.ParseInt(dateStr[:4], 10, 32)
+	if err != nil || year < 1 {
+		return invalidTimestamp(dateStr)
+	}
+
+	if len(dateStr) == 5 && (dateStr[4] == 't' || dateStr[4] == 'T') {
+		// yyyyT
+		return tryCreateDateTimestamp(int(year), 1, 1, TimestampPrecisionYear)
+	}
+
+	if dateStr[4] != '-' {
+		return invalidTimestamp(dateStr)
+	}
+
+	if len(dateStr) < 8 {
+		return invalidTimestamp(dateStr)
+	}
+
+	month, err := strconv.ParseInt(dateStr[5:7], 10, 32)
+	if err != nil {
+		return invalidTimestamp(dateStr)
+	}
+
+	if len(dateStr) == 8 && (dateStr[7] == 't' || dateStr[7] == 'T') {
+		// yyyy-mmT
+		return tryCreateDateTimestamp(int(year), int(month), 1, TimestampPrecisionMonth)
+	}
+
+	if dateStr[7] != '-' {
+		return invalidTimestamp(dateStr)
+	}
+
+	if len(dateStr) < 10 {
+		return invalidTimestamp(dateStr)
+	}
+
+	day, err := strconv.ParseInt(dateStr[8:10], 10, 32)
+	if err != nil {
+		return invalidTimestamp(dateStr)
+	}
+
+	if len(dateStr) == 10 || (len(dateStr) == 11 && (dateStr[10] == 't' || dateStr[10] == 'T')) {
+		// yyyy-mm-dd or yyyy-mm-ddT
+		return tryCreateDateTimestamp(int(year), int(month), int(day), TimestampPrecisionDay)
+	}
+
+	if dateStr[10] != 't' && dateStr[10] != 'T' {
+		return invalidTimestamp(dateStr)
+	}
+
+	// At this point timestamp must have hour:minute
+	if len(dateStr) < 17 {
+		return invalidTimestamp(dateStr)
+	}
+
+	switch dateStr[16] {
+	case 'z', 'Z', '+', '-':
+		// yyyy-mm-ddThh:mm
+		kind, err := computeTimezoneKind(dateStr, 16)
+		if err != nil {
+			return Timestamp{}, err
+		}
+
+		return NewTimestampFromStr(dateStr, TimestampPrecisionMinute, kind)
+	case ':':
+		// yyyy-mm-ddThh:mm:ss
+		if len(dateStr) < 20 {
+			break
+		}
+
+		idx := 19
+		if dateStr[idx] == '.' {
+			idx++
+			for idx < len(dateStr) && isDigit(int(dateStr[idx])) {
+				idx++
+			}
+		}
+
+		kind, err := computeTimezoneKind(dateStr, idx)
+		if err != nil {
+			return Timestamp{}, err
+		}
+
+		if idx <= 20 {
+			return NewTimestampFromStr(dateStr, TimestampPrecisionSecond, kind)
+		} else if idx <= 28 {
+			return NewTimestampFromStr(dateStr, TimestampPrecisionNanosecond, kind)
+		}
+
+		// Greater than 9 fractional seconds.
+		return roundFractionalSeconds(dateStr, idx, kind)
+	}
+
+	return invalidTimestamp(dateStr)
+}
+
+func computeOffset(val string, idx int) (int64, int64, error) {
+	// +hh:mm
+	if idx+5 > len(val) || val[idx+3] != ':' {
+		return 0, 0, fmt.Errorf("ion: invalid offset: '%v'", val)
+	}
+
+	hourOffset, err := strconv.ParseInt(val[idx+1:idx+3], 10, 32)
+	if err != nil {
+		return 0, 0, err
+	}
+
+	minuteOffset, err := strconv.ParseInt(val[idx+4:], 10, 32)
+	if err != nil {
+		return 0, 0, err
+	}
+
+	return hourOffset, minuteOffset, nil
+}
+
+func computeTimezoneKind(val string, idx int) (TimezoneKind, error) {
+	switch val[idx] {
+	case 'z', 'Z':
+		// 'Z' zulu time means UTC timezone.
+		return TimezoneUTC, nil
+	case '+', '-':
+		hourOffset, minuteOffset, err := computeOffset(val, idx)
+		if err != nil {
+			return TimezoneUnspecified, err
+		}
+
+		if hourOffset >= 24 || minuteOffset >= 60 {
+			return TimezoneUnspecified, fmt.Errorf("ion: invalid offset %v:%v", hourOffset, minuteOffset)
+		} else if hourOffset == 0 && minuteOffset == 0 {
+			// Negative zero offset is Unspecified timezone.
+			if val[idx] == '-' {
+				return TimezoneUnspecified, nil
+			}
+
+			// Positive zero offset is UTC.
+			return TimezoneUTC, nil
+		}
+
+		// Valid non-zero offset is Local timezone.
+		return TimezoneLocal, nil
+	}
+
+	return TimezoneUnspecified, fmt.Errorf("ion: invalid character: '%v' at position %v in %v", val[idx], idx, val)
+}
+
+func roundFractionalSeconds(val string, idx int, kind TimezoneKind) (Timestamp, error) {
+	// Convert to float to perform rounding.
+	floatValue, err := strconv.ParseFloat(val[18:idx], 64)
+	if err != nil {
+		return invalidTimestamp(val)
+	}
+
+	roundedStringValue := fmt.Sprintf("%.9f", floatValue)
+	roundedFloatValue, err := strconv.ParseFloat(roundedStringValue, 64)
+	if err != nil {
+		return invalidTimestamp(val)
+	}
+
+	// Microsecond overflow 9.9999999999 -> 10.00000000.
+	if roundedFloatValue == 10 {
+		roundedStringValue := "9.000000000"
+		val = val[:18] + roundedStringValue + val[idx:]
+		timeValue, err := time.Parse(TimestampPrecisionNanosecond.Layout(kind, 9), val)
+		if err != nil {
+			return invalidTimestamp(val)
+		}
+
+		timeValue = timeValue.Add(time.Second)
+		return NewTimestampWithFractionalSeconds(timeValue, TimestampPrecisionNanosecond, kind, 9), err
+	}
+
+	val = val[:18] + roundedStringValue + val[idx:]
+
+	return NewTimestampFromStr(val, TimestampPrecisionNanosecond, kind)
+}
+
+// GetDateTime returns the timestamps date time.
+func (ts Timestamp) GetDateTime() time.Time {
+	return ts.dateTime
+}
+
+// GetPrecision returns the timestamp's precision.
+func (ts Timestamp) GetPrecision() TimestampPrecision {
+	return ts.precision
+}
+
+// GetTimezoneKind returns the kind of timezone.
+func (ts Timestamp) GetTimezoneKind() TimezoneKind {
+	return ts.kind
+}
+
+// GetNumberOfFractionalSeconds returns the number of precision units in the timestamp's fractional seconds.
+func (ts Timestamp) GetNumberOfFractionalSeconds() uint8 {
+	return ts.numFractionalSeconds
+}
+
+// String returns a formatted Timestamp string.
+func (ts Timestamp) String() string {
+	layout := ts.precision.Layout(ts.kind, ts.numFractionalSeconds)
+	format := ts.dateTime.Format(layout)
+
+	// The above time.Format() does not produce the format we want in some scenarios.
+	// So we may need to make some adjustments.
+
+	// Add back removed trailing zeros from fractional seconds (ie. ".000")
+	if ts.precision >= TimestampPrecisionNanosecond && ts.dateTime.Nanosecond() == 0 && ts.numFractionalSeconds > 0 {
+		// Find the position of 'T'
+		tIndex := strings.Index(format, "T")
+		if tIndex == -1 {
+			tIndex = strings.Index(format, "t")
+			if tIndex == -1 {
+				return format
+			}
+		}
+
+		index := strings.LastIndex(format, "Z")
+		if index == -1 || index < tIndex {
+			index = strings.LastIndex(format, "+")
+			if index == -1 || index < tIndex {
+				index = strings.LastIndex(format, "-")
+			}
+		}
+
+		// This position better be right of 'T'
+		if index != -1 && tIndex < index {
+			zeros := strings.Builder{}
+			zeros.WriteByte('.')
+			for i := uint8(0); i < ts.numFractionalSeconds; i++ {
+				zeros.WriteByte('0')
+			}
+
+			format = format[0:index] + zeros.String() + format[index:]
+		}
+	}
+
+	// A timestamp with time precision (ie. Minute/Second/Nanosecond) and Unspecified timezone
+	// should have a "-00:00" offset but time.String() is returning a "+00:00" offset.
+	if ts.precision >= TimestampPrecisionMinute && ts.kind == TimezoneUnspecified {
+		index := strings.LastIndex(format, "+00:00")
+		if index != -1 {
+			format = format[0:index] + "-00:00"
+		}
+	}
+
+	return format
+}
+
+// Equal figures out if two timestamps are equal for each component.
+func (ts Timestamp) Equal(ts1 Timestamp) bool {
+	_, offset := ts.dateTime.Zone()
+	_, offset1 := ts1.dateTime.Zone()
+
+	return ts.dateTime.Equal(ts1.dateTime) &&
+		offset == offset1 &&
+		ts.precision == ts1.precision &&
+		ts.kind == ts1.kind &&
+		ts.numFractionalSeconds == ts1.numFractionalSeconds
+}
+
+// TruncatedNanoseconds returns nanoseconds with trailing zeros removed (ie. 123456000 gets truncated to 123456).
+func (ts Timestamp) TruncatedNanoseconds() int {
+	nsecs := ts.dateTime.Nanosecond()
+	for i := uint8(0); i < (9-ts.numFractionalSeconds) && nsecs > 0 && (nsecs%10) == 0; i++ {
+		nsecs /= 10
+	}
+
+	return nsecs
+}
diff --git a/ion/unmarshal.go b/ion/unmarshal.go
index d52fc51..66e62ba 100644
--- a/ion/unmarshal.go
+++ b/ion/unmarshal.go
@@ -86,7 +86,7 @@ func (d *Decoder) decode() (interface{}, error) {
 		return d.r.DecimalValue()
 
 	case TimestampType:
-		return d.r.TimeValue()
+		return d.r.TimestampValue()
 
 	case StringType, SymbolType:
 		return d.r.StringValue()
@@ -389,18 +389,18 @@ func (d *Decoder) decodeDecimalTo(v reflect.Value) error {
 }
 
 func (d *Decoder) decodeTimestampTo(v reflect.Value) error {
-	val, err := d.r.TimeValue()
+	val, err := d.r.TimestampValue()
 	if err != nil {
 		return err
 	}
 
 	switch v.Kind() {
 	case reflect.Struct:
-		if v.Type() == timeType {
+		if v.Type() == timestampType {
 			v.Set(reflect.ValueOf(val))
 			return d.attachAnnotations(v)
 		}
-		return d.decodeToStructWithAnnotation(v, timeType.Kind())
+		return d.decodeToStructWithAnnotation(v, timestampType.Kind())
 
 	case reflect.Interface:
 		if v.NumMethod() == 0 {
diff --git a/ion/writer.go b/ion/writer.go
index e4e6639..0afddb4 100644
--- a/ion/writer.go
+++ b/ion/writer.go
@@ -4,7 +4,6 @@ import (
 	"errors"
 	"io"
 	"math/big"
-	"time"
 )
 
 // A Writer writes a stream of Ion values.
@@ -81,7 +80,7 @@ type Writer interface {
 	WriteDecimal(val *Decimal) error
 
 	// WriteTimestamp writes a timestamp value.
-	WriteTimestamp(val time.Time) error
+	WriteTimestamp(val Timestamp) error
 
 	// WriteSymbol writes a symbol value.
 	WriteSymbol(val string) error


Test patch:
diff --git a/ion/binaryreader_test.go b/ion/binaryreader_test.go
index e46f452..578a23d 100644
--- a/ion/binaryreader_test.go
+++ b/ion/binaryreader_test.go
@@ -329,11 +329,13 @@ func TestReadBinaryTimestamps(t *testing.T) {
 
 	_null(t, r, TimestampType)
 
-	for i := 0; i < 5; i++ {
-		_timestamp(t, r, time.Time{})
-	}
+	_timestamp(t, r, NewDateTimestamp(time.Time{}, TimestampPrecisionYear))
+	_timestamp(t, r, NewDateTimestamp(time.Time{}, TimestampPrecisionMonth))
+	_timestamp(t, r, NewDateTimestamp(time.Time{}, TimestampPrecisionDay))
+	_timestamp(t, r, NewTimestamp(time.Time{}, TimestampPrecisionMinute, TimezoneUTC))
+	_timestamp(t, r, NewTimestamp(time.Time{}, TimestampPrecisionSecond, TimezoneUTC))
 
-	nowish, _ := time.Parse(time.RFC3339Nano, "2019-08-04T18:15:43.863494+10:00")
+	nowish, _ := NewTimestampFromStr("2019-08-04T18:15:43.863494000+10:00", TimestampPrecisionNanosecond, TimezoneLocal)
 	_timestamp(t, r, nowish)
 	_eof(t, r)
 }
diff --git a/ion/binarywriter_test.go b/ion/binarywriter_test.go
index 656fe7d..bea3017 100644
--- a/ion/binarywriter_test.go
+++ b/ion/binarywriter_test.go
@@ -153,22 +153,22 @@ func TestWriteBinarySymbol(t *testing.T) {
 func TestWriteBinaryTimestamp(t *testing.T) {
 	eval := []byte{
 		0x67, 0x80, 0x81, 0x81, 0x81, 0x80, 0x80, 0x80, // 0001-01-01T00:00:00Z
-		0x6E, 0x8E, // 0x0E-bit timestamp
+		0x6D,       // 0x0D-byte timestamp
 		0x04, 0xD8, // offset: +600 minutes (+10:00)
 		0x0F, 0xE3, // year:   2019
-		0x88,                   // month:  8
-		0x84,                   // day:    4
-		0x88,                   // hour:   8 utc (18 local)
-		0x8F,                   // minute: 15
-		0xAB,                   // second: 43
-		0xC9,                   // exp:    -9
-		0x33, 0x77, 0xDF, 0x70, // nsec:   863494000
+		0x88,             // month:  8
+		0x84,             // day:    4
+		0x88,             // hour:   8 utc (18 local)
+		0x8F,             // minute: 15
+		0xAB,             // second: 43
+		0xC6,             // exp:    6 precision units
+		0x0D, 0x2D, 0x06, // nsec:   863494
 	}
 
-	nowish, _ := time.Parse(time.RFC3339Nano, "2019-08-04T18:15:43.863494+10:00")
+	nowish, _ := NewTimestampFromStr("2019-08-04T18:15:43.863494+10:00", TimestampPrecisionNanosecond, TimezoneLocal)
 
 	testBinaryWriter(t, eval, func(w Writer) {
-		w.WriteTimestamp(time.Time{})
+		w.WriteTimestamp(NewTimestamp(time.Time{}, TimestampPrecisionNanosecond, TimezoneUTC))
 		w.WriteTimestamp(nowish)
 	})
 }
diff --git a/ion/bits_test.go b/ion/bits_test.go
index db2c15c..96fddf0 100644
--- a/ion/bits_test.go
+++ b/ion/bits_test.go
@@ -169,37 +169,37 @@ func TestAppendTag(t *testing.T) {
 	test(0x50, math.MaxInt64, 10, []byte{0x5E, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0xFF})
 }
 
-func TestAppendTime(t *testing.T) {
-	test := func(val time.Time, elen uint64, ebits []byte) {
-		t.Run(fmt.Sprintf("%x", val), func(t *testing.T) {
-			_, offset := val.Zone()
+func TestAppendTimestamp(t *testing.T) {
+	test := func(val Timestamp, elen uint64, ebits []byte) {
+		t.Run(fmt.Sprintf("%x", val.dateTime), func(t *testing.T) {
+			_, offset := val.dateTime.Zone()
 			offset /= 60
-			utc := val.In(time.UTC)
+			val.dateTime = val.dateTime.In(time.UTC)
 
-			len := timeLen(offset, utc)
+			len := timestampLen(offset, val)
 			if len != elen {
 				t.Errorf("expected len=%v, got len=%v", elen, len)
 			}
 
-			bits := appendTime(nil, offset, utc)
+			bits := appendTimestamp(nil, offset, val)
 			if !bytes.Equal(bits, ebits) {
 				t.Errorf("expected %v, got %v", fmtbytes(ebits), fmtbytes(bits))
 			}
 		})
 	}
 
-	nowish, _ := time.Parse(time.RFC3339Nano, "2019-08-04T18:15:43.863494+10:00")
+	nowish, _ := NewTimestampFromStr("2019-08-04T18:15:43.863494+10:00", TimestampPrecisionNanosecond, TimezoneLocal)
 
-	test(time.Time{}, 7, []byte{0x80, 0x81, 0x81, 0x81, 0x80, 0x80, 0x80})
-	test(nowish, 14, []byte{
+	test(NewDateTimestamp(time.Time{}, TimestampPrecisionSecond), 7, []byte{0xC0, 0x81, 0x81, 0x81, 0x80, 0x80, 0x80})
+	test(nowish, 13, []byte{
 		0x04, 0xD8, // offset: +600 minutes (+10:00)
 		0x0F, 0xE3, // year:   2019
-		0x88,                   // month:  8
-		0x84,                   // day:    4
-		0x88,                   // hour:   8 utc (18 local)
-		0x8F,                   // minute: 15
-		0xAB,                   // second: 43
-		0xC9,                   // exp:    -9
-		0x33, 0x77, 0xDF, 0x70, // nsec:   863494000
+		0x88,             // month:  8
+		0x84,             // day:    4
+		0x88,             // hour:   8 utc (18 local)
+		0x8F,             // minute: 15
+		0xAB,             // second: 43
+		0xC6,             // exp:    6 precision units
+		0x0D, 0x2D, 0x06, // nsec:   863494
 	})
 }
diff --git a/ion/bitstream_test.go b/ion/bitstream_test.go
index 603f327..84f7199 100644
--- a/ion/bitstream_test.go
+++ b/ion/bitstream_test.go
@@ -2,7 +2,6 @@ package ion
 
 import (
 	"testing"
-	"time"
 )
 
 func TestBitstream(t *testing.T) {
@@ -114,9 +113,8 @@ func TestBitcodeString(t *testing.T) {
 }
 
 func TestBinaryReadTimestamp(t *testing.T) {
-	test := func(ion []byte, expectedValue string) {
+	test := func(ion []byte, expectedValue string, expectedPrecision TimestampPrecision, expectedKind TimezoneKind) {
 		t.Run(expectedValue, func(t *testing.T) {
-
 			b := bitstream{}
 			b.InitBytes(ion)
 			b.Next()
@@ -126,13 +124,13 @@ func TestBinaryReadTimestamp(t *testing.T) {
 				t.Fatal(err)
 			}
 
-			expectedTime, err := time.Parse(time.RFC3339Nano, expectedValue)
+			expectedTimestamp, err := NewTimestampFromStr(expectedValue, expectedPrecision, expectedKind)
 			if err != nil {
 				t.Fatal(err)
 			}
 
-			if !val.Equal(expectedTime) {
-				t.Errorf("expected %v, got %v", expectedValue, val)
+			if !val.Equal(expectedTimestamp) {
+				t.Errorf("expected %v, got %v", expectedTimestamp, val)
 			}
 		})
 	}
@@ -141,14 +139,14 @@ func TestBinaryReadTimestamp(t *testing.T) {
 		0x63,
 		0x80,       // offset 0
 		0x0F, 0xD0, // year: 2000
-	}, "2000-01-01T00:00:00Z")
+	}, "2000T", TimestampPrecisionYear, TimezoneUnspecified)
 
 	test([]byte{
 		0x64,
 		0x80,       // offset 0
 		0x0F, 0xD0, // year: 2000
 		0x85, // month: 5
-	}, "2000-05-01T00:00:00Z")
+	}, "2000-05T", TimestampPrecisionMonth, TimezoneUnspecified)
 
 	test([]byte{
 		0x65,
@@ -156,7 +154,7 @@ func TestBinaryReadTimestamp(t *testing.T) {
 		0x0F, 0xD0, // year: 2000
 		0x85, // month: 5
 		0x86, // day: 6
-	}, "2000-05-06T00:00:00Z")
+	}, "2000-05-06T", TimestampPrecisionDay, TimezoneUnspecified)
 
 	test([]byte{
 		0x67,
@@ -166,7 +164,7 @@ func TestBinaryReadTimestamp(t *testing.T) {
 		0x86, // day: 6
 		0x87, // hour: 7
 		0x88, // minute: 8
-	}, "2000-05-06T07:08:00Z")
+	}, "2000-05-06T07:08Z", TimestampPrecisionMinute, TimezoneUTC)
 
 	test([]byte{
 		0x68,
@@ -177,7 +175,7 @@ func TestBinaryReadTimestamp(t *testing.T) {
 		0x87, // hour: 7
 		0x88, // minute: 8
 		0x89, // second: 9
-	}, "2000-05-06T07:08:09Z")
+	}, "2000-05-06T07:08:09Z", TimestampPrecisionSecond, TimezoneUTC)
 
 	test([]byte{
 		0x6A,
@@ -190,7 +188,7 @@ func TestBinaryReadTimestamp(t *testing.T) {
 		0x80, // second: 0
 		0x80, // 0 precision units
 		0x00, // 0
-	}, "2000-01-01T00:00:00Z")
+	}, "2000-01-01T00:00:00Z", TimestampPrecisionSecond, TimezoneUTC)
 
 	test([]byte{
 		0x69,
@@ -202,7 +200,7 @@ func TestBinaryReadTimestamp(t *testing.T) {
 		0x80, // minute: 0
 		0x80, // second: 0
 		0xC2, // 2 precision units
-	}, "2000-01-01T00:00:00.00Z")
+	}, "2000-01-01T00:00:00.00Z", TimestampPrecisionNanosecond, TimezoneUTC)
 
 	test([]byte{
 		0x6A,
@@ -215,7 +213,7 @@ func TestBinaryReadTimestamp(t *testing.T) {
 		0x89, // second: 9
 		0xC3, // 3 precision units
 		0x64, // 100
-	}, "2000-05-06T07:08:09.100Z")
+	}, "2000-05-06T07:08:09.100Z", TimestampPrecisionNanosecond, TimezoneUTC)
 
 	test([]byte{
 		0x6C,
@@ -228,7 +226,7 @@ func TestBinaryReadTimestamp(t *testing.T) {
 		0x89,             // second: 9
 		0xC6,             // 6 precision units
 		0x01, 0x87, 0x04, // 100100
-	}, "2000-05-06T07:08:09.100100Z")
+	}, "2000-05-06T07:08:09.100100Z", TimestampPrecisionNanosecond, TimezoneUTC)
 
 	test([]byte{
 		0x6C,
@@ -237,11 +235,11 @@ func TestBinaryReadTimestamp(t *testing.T) {
 		0x85,             // month: 5
 		0x86,             // day: 6
 		0x87,             // hour: 7
-		0x88,             // minute: 8
+		0x88,             // minute: 8 utc (16 local)
 		0x89,             // second: 9
 		0xC6,             // 6 precision units
 		0x01, 0x87, 0x04, // 100100
-	}, "2000-05-06T07:08:09.100100Z")
+	}, "2000-05-06T07:16:09.100100+00:08", TimestampPrecisionNanosecond, TimezoneLocal)
 
 	// Test >9 fractional seconds.
 	test([]byte{
@@ -255,7 +253,7 @@ func TestBinaryReadTimestamp(t *testing.T) {
 		0x80, // second: 0
 		0xCA, // 10 precision units
 		0x2C, // 44
-	}, "2000-01-01T00:00:00.000000004Z")
+	}, "2000-01-01T00:00:00.000000004Z", TimestampPrecisionNanosecond, TimezoneUTC)
 
 	test([]byte{
 		0x6A,
@@ -268,7 +266,7 @@ func TestBinaryReadTimestamp(t *testing.T) {
 		0x80, // second: 0
 		0xCA, // 10 precision units
 		0x2D, // 45
-	}, "2000-01-01T00:00:00.000000005Z")
+	}, "2000-01-01T00:00:00.000000005Z", TimestampPrecisionNanosecond, TimezoneUTC)
 
 	test([]byte{
 		0x6A,
@@ -281,7 +279,7 @@ func TestBinaryReadTimestamp(t *testing.T) {
 		0x80, // second: 0
 		0xCA, // 10 precision units
 		0x2E, // 46
-	}, "2000-01-01T00:00:00.000000005Z")
+	}, "2000-01-01T00:00:00.000000005Z", TimestampPrecisionNanosecond, TimezoneUTC)
 
 	test([]byte{
 		0x6E,
@@ -295,5 +293,5 @@ func TestBinaryReadTimestamp(t *testing.T) {
 		0xBB,                         // second: 59
 		0xCA,                         // 10 precision units
 		0x02, 0x54, 0x0B, 0xE3, 0xFF, // 9999999999
-	}, "2001-01-01T00:00:00.00000000Z")
+	}, "2001-01-01T00:00:00.000000000Z", TimestampPrecisionNanosecond, TimezoneUTC)
 }
diff --git a/ion/cmp_test.go b/ion/cmp_test.go
index 55e242d..2188c4a 100644
--- a/ion/cmp_test.go
+++ b/ion/cmp_test.go
@@ -2,7 +2,6 @@ package ion
 
 import (
 	"reflect"
-	"time"
 
 	"github.com/google/go-cmp/cmp"
 	"github.com/google/go-cmp/cmp/cmpopts"
@@ -14,7 +13,7 @@ type ionEqual interface {
 
 type ionFloat struct{ float64 }
 type ionDecimal struct{ *Decimal }
-type ionTimestamp struct{ time.Time }
+type ionTimestamp struct{ Timestamp }
 
 func (thisFloat ionFloat) eq(other ionEqual) bool {
 	return cmp.Equal(thisFloat.float64, other.(ionFloat).float64, cmpopts.EquateNaNs())
@@ -32,7 +31,7 @@ func (thisDecimal ionDecimal) eq(other ionEqual) bool {
 
 func (thisTimestamp ionTimestamp) eq(other ionEqual) bool {
 	if val, ok := other.(ionTimestamp); ok {
-		return thisTimestamp.Time.Equal(val.Time)
+		return thisTimestamp.Equal(val.Timestamp)
 	}
 	return false
 }
@@ -81,9 +80,9 @@ func cmpTimestamps(thisValue, otherValue interface{}) bool {
 	switch val := thisValue.(type) {
 	case string: // null.timestamp
 		return strNullTypeCmp(val, otherValue)
-	case time.Time:
+	case Timestamp:
 		thisTimestamp := ionTimestamp{val}
-		return thisTimestamp.eq(ionTimestamp{otherValue.(time.Time)})
+		return thisTimestamp.eq(ionTimestamp{otherValue.(Timestamp)})
 	default:
 		return false
 	}
diff --git a/ion/integration_test.go b/ion/integration_test.go
index fdb323f..9f59dbc 100644
--- a/ion/integration_test.go
+++ b/ion/integration_test.go
@@ -176,7 +176,6 @@ var nonEquivsSkipList = []string{
 	"symbolTables.ion",
 	"symbolTablesUnknownText.ion",
 	"symbols.ion",
-	"timestamps.ion",
 }
 
 func TestLoadGood(t *testing.T) {
@@ -589,7 +588,7 @@ func writeFromReaderToWriter(t *testing.T, reader Reader, writer Writer) {
 			}
 
 		case TimestampType:
-			val, err := reader.TimeValue()
+			val, err := reader.TimestampValue()
 			if err != nil {
 				t.Errorf("Something went wrong while reading a Timestamp value: " + err.Error())
 			}
@@ -758,7 +757,7 @@ func readCurrentValue(t *testing.T, reader Reader) ionItem {
 		ionItem.ionType = DecimalType
 
 	case TimestampType:
-		val, err := reader.TimeValue()
+		val, err := reader.TimestampValue()
 		if err != nil {
 			t.Errorf("Something went wrong when reading Timestamp value. " + err.Error())
 		}
diff --git a/ion/marshal_test.go b/ion/marshal_test.go
index 328f833..5a73cf7 100644
--- a/ion/marshal_test.go
+++ b/ion/marshal_test.go
@@ -36,7 +36,7 @@ func TestMarshalText(t *testing.T) {
 	test(math.NaN(), "nan")
 
 	test(MustParseDecimal("1.20"), "1.20")
-	test(time.Date(2010, 1, 1, 0, 0, 0, 0, time.UTC), "2010-01-01T00:00:00Z")
+	test(NewTimestamp(time.Date(2010, 1, 1, 0, 0, 0, 0, time.UTC), TimestampPrecisionSecond, TimezoneUTC), "2010-01-01T00:00:00Z")
 
 	test("hello\tworld", "\"hello\\tworld\"")
 
@@ -335,7 +335,7 @@ func TestMarshalValuesWithAnnotation(t *testing.T) {
 	test(buildValue(5), "int", "'symbols or string'::annotations::5")
 	test(buildValue(float32(math.MaxFloat32)), "float", "'symbols or string'::annotations::3.4028234663852886e+38")
 	test(buildValue(MustParseDecimal("1.2")), "decimal", "'symbols or string'::annotations::1.2")
-	test(buildValue(time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC)),
+	test(buildValue(NewTimestamp(time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC), TimestampPrecisionSecond, TimezoneUTC)),
 		"timestamp", "'symbols or string'::annotations::2000-01-02T03:04:05Z")
 	test(buildValue("stringValue"), "string", "'symbols or string'::annotations::\"stringValue\"")
 	test(buildValue([]byte{4, 2}), "blob", "'symbols or string'::annotations::{{BAI=}}")
diff --git a/ion/textreader_test.go b/ion/textreader_test.go
index 9851462..86da175 100644
--- a/ion/textreader_test.go
+++ b/ion/textreader_test.go
@@ -193,15 +193,16 @@ func TestBlobs(t *testing.T) {
 }
 
 func TestTimestamps(t *testing.T) {
-	testA := func(str string, etas []string, eval time.Time) {
+	testA := func(str string, etas []string, eval Timestamp) {
 		t.Run(str, func(t *testing.T) {
 			r := NewReaderStr(str)
 			_nextAF(t, r, TimestampType, nil, etas)
 
-			val, err := r.TimeValue()
+			val, err := r.TimestampValue()
 			if err != nil {
 				t.Fatal(err)
 			}
+
 			if !val.Equal(eval) {
 				t.Errorf("expected %v, got %v", eval, val)
 			}
@@ -210,26 +211,26 @@ func TestTimestamps(t *testing.T) {
 		})
 	}
 
-	test := func(str string, eval time.Time) {
+	test := func(str string, eval Timestamp) {
 		testA(str, nil, eval)
 	}
 
 	et := time.Date(2001, time.January, 1, 0, 0, 0, 0, time.UTC)
-	test("2001T", et)
-	test("2001-01T", et)
-	test("2001-01-01", et)
-	test("2001-01-01T", et)
-	test("2001-01-01T00:00Z", et)
-	test("2001-01-01T00:00:00Z", et)
-	test("2001-01-01T00:00:00.000Z", et)
-	test("2001-01-01T00:00:00.000+00:00", et)
-	test("2001-01-01T00:00:00.000000Z", et)
-	test("2001-01-01T00:00:00.000000000Z", et)
+	test("2001T", NewDateTimestamp(et, TimestampPrecisionYear))
+	test("2001-01T", NewDateTimestamp(et, TimestampPrecisionMonth))
+	test("2001-01-01", NewDateTimestamp(et, TimestampPrecisionDay))
+	test("2001-01-01T", NewDateTimestamp(et, TimestampPrecisionDay))
+	test("2001-01-01T00:00Z", NewTimestamp(et, TimestampPrecisionMinute, TimezoneUTC))
+	test("2001-01-01T00:00:00Z", NewTimestamp(et, TimestampPrecisionSecond, TimezoneUTC))
+	test("2001-01-01T00:00:00.000Z", NewTimestampWithFractionalSeconds(et, TimestampPrecisionNanosecond, TimezoneUTC, 3))
+	test("2001-01-01T00:00:00.000+00:00", NewTimestampWithFractionalSeconds(et, TimestampPrecisionNanosecond, TimezoneUTC, 3))
+	test("2001-01-01T00:00:00.000000Z", NewTimestampWithFractionalSeconds(et, TimestampPrecisionNanosecond, TimezoneUTC, 6))
+	test("2001-01-01T00:00:00.000000000Z", NewTimestampWithFractionalSeconds(et, TimestampPrecisionNanosecond, TimezoneUTC, 9))
 
 	et2 := time.Date(2001, time.January, 1, 0, 0, 0, 1, time.UTC)
-	test("2001-01-01T00:00:00.000000000999Z", et2)
+	test("2001-01-01T00:00:00.000000000999Z", NewTimestampWithFractionalSeconds(et2, TimestampPrecisionNanosecond, TimezoneUTC, 12))
 
-	testA("foo::'bar'::2001-01-01T00:00:00.000Z", []string{"foo", "bar"}, et)
+	testA("foo::'bar'::2001-01-01T00:00:00.000Z", []string{"foo", "bar"}, NewTimestampWithFractionalSeconds(et, TimestampPrecisionNanosecond, TimezoneUTC, 3))
 }
 
 func TestDecimals(t *testing.T) {
@@ -659,17 +660,17 @@ func _decimalAF(t *testing.T, r Reader, efn *string, etas []string, eval *Decima
 	}
 }
 
-func _timestamp(t *testing.T, r Reader, eval time.Time) {
+func _timestamp(t *testing.T, r Reader, eval Timestamp) {
 	_timestampAF(t, r, nil, nil, eval)
 }
 
-func _timestampAF(t *testing.T, r Reader, efn *string, etas []string, eval time.Time) {
+func _timestampAF(t *testing.T, r Reader, efn *string, etas []string, eval Timestamp) {
 	_nextAF(t, r, TimestampType, efn, etas)
 	if r.IsNull() {
 		t.Fatalf("expected %v, got null.timestamp", eval)
 	}
 
-	val, err := r.TimeValue()
+	val, err := r.TimestampValue()
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/ion/textutils_test.go b/ion/textutils_test.go
index 6f8b1f5..e66c9e9 100644
--- a/ion/textutils_test.go
+++ b/ion/textutils_test.go
@@ -7,7 +7,7 @@ import (
 )
 
 func TestParseTimestamp(t *testing.T) {
-	test := func(str string, eval string) {
+	test := func(str string, eval string, expectedPrecision TimestampPrecision, expectedKind TimezoneKind, expectedFractionSeconds uint8) {
 		t.Run(str, func(t *testing.T) {
 			val, err := parseTimestamp(str)
 			if err != nil {
@@ -19,40 +19,42 @@ func TestParseTimestamp(t *testing.T) {
 				t.Fatal(err)
 			}
 
-			if !val.Equal(et) {
-				t.Errorf("expected %v, got %v", eval, val)
+			expectedTimestamp := NewTimestampWithFractionalSeconds(et, expectedPrecision, expectedKind, expectedFractionSeconds)
+
+			if !val.Equal(expectedTimestamp) {
+				t.Errorf("expected %v, got %v", expectedTimestamp, val)
 			}
 		})
 	}
 
-	test("1234T", "1234-01-01T00:00:00Z")
-	test("1234-05T", "1234-05-01T00:00:00Z")
-	test("1234-05-06", "1234-05-06T00:00:00Z")
-	test("1234-05-06T", "1234-05-06T00:00:00Z")
-	test("1234-05-06T07:08Z", "1234-05-06T07:08:00Z")
-	test("1234-05-06T07:08:09Z", "1234-05-06T07:08:09Z")
-	test("1234-05-06T07:08:09.100Z", "1234-05-06T07:08:09.100Z")
-	test("1234-05-06T07:08:09.100100Z", "1234-05-06T07:08:09.100100Z")
+	test("1234T", "1234-01-01T00:00:00Z", TimestampPrecisionYear, TimezoneUnspecified, 0)
+	test("1234-05T", "1234-05-01T00:00:00Z", TimestampPrecisionMonth, TimezoneUnspecified, 0)
+	test("1234-05-06", "1234-05-06T00:00:00Z", TimestampPrecisionDay, TimezoneUnspecified, 0)
+	test("1234-05-06T", "1234-05-06T00:00:00Z", TimestampPrecisionDay, TimezoneUnspecified, 0)
+	test("1234-05-06T07:08Z", "1234-05-06T07:08:00Z", TimestampPrecisionMinute, TimezoneUTC, 0)
+	test("1234-05-06T07:08:09Z", "1234-05-06T07:08:09Z", TimestampPrecisionSecond, TimezoneUTC, 0)
+	test("1234-05-06T07:08:09.100Z", "1234-05-06T07:08:09.100Z", TimestampPrecisionNanosecond, TimezoneUTC, 1)
+	test("1234-05-06T07:08:09.100100Z", "1234-05-06T07:08:09.100100Z", TimestampPrecisionNanosecond, TimezoneUTC, 4)
 
 	// Test rounding of >=9 fractional seconds.
-	test("1234-05-06T07:08:09.000100100Z", "1234-05-06T07:08:09.000100100Z")
-	test("1234-05-06T07:08:09.100100100Z", "1234-05-06T07:08:09.100100100Z")
-	test("1234-05-06T07:08:09.00010010044Z", "1234-05-06T07:08:09.000100100Z")
-	test("1234-05-06T07:08:09.00010010044Z", "1234-05-06T07:08:09.000100100Z")
-	test("1234-05-06T07:08:09.00010010055Z", "1234-05-06T07:08:09.000100101Z")
-	test("1234-05-06T07:08:09.00010010099Z", "1234-05-06T07:08:09.000100101Z")
-	test("1234-05-06T07:08:09.99999999999Z", "1234-05-06T07:08:10.000000000Z")
-	test("1234-12-31T23:59:59.99999999999Z", "1235-01-01T00:00:00.000000000Z")
-	test("1234-05-06T07:08:09.000100100+09:10", "1234-05-06T07:08:09.000100100+09:10")
-	test("1234-05-06T07:08:09.100100100-10:11", "1234-05-06T07:08:09.100100100-10:11")
-	test("1234-05-06T07:08:09.00010010044+09:10", "1234-05-06T07:08:09.000100100+09:10")
-	test("1234-05-06T07:08:09.00010010055-10:11", "1234-05-06T07:08:09.000100101-10:11")
-	test("1234-05-06T07:08:09.00010010099+09:10", "1234-05-06T07:08:09.000100101+09:10")
-	test("1234-05-06T07:08:09.99999999999-10:11", "1234-05-06T07:08:10.000000000-10:11")
-	test("1234-12-31T23:59:59.99999999999+09:10", "1235-01-01T00:00:00.000000000+09:10")
-
-	test("1234-05-06T07:08+09:10", "1234-05-06T07:08:00+09:10")
-	test("1234-05-06T07:08:09-10:11", "1234-05-06T07:08:09-10:11")
+	test("1234-05-06T07:08:09.000100100Z", "1234-05-06T07:08:09.000100100Z", TimestampPrecisionNanosecond, TimezoneUTC, 7)
+	test("1234-05-06T07:08:09.100100100Z", "1234-05-06T07:08:09.100100100Z", TimestampPrecisionNanosecond, TimezoneUTC, 7)
+	test("1234-05-06T07:08:09.00010010044Z", "1234-05-06T07:08:09.000100100Z", TimestampPrecisionNanosecond, TimezoneUTC, 7)
+	test("1234-05-06T07:08:09.00010010044Z", "1234-05-06T07:08:09.000100100Z", TimestampPrecisionNanosecond, TimezoneUTC, 7)
+	test("1234-05-06T07:08:09.00010010055Z", "1234-05-06T07:08:09.000100101Z", TimestampPrecisionNanosecond, TimezoneUTC, 9)
+	test("1234-05-06T07:08:09.00010010099Z", "1234-05-06T07:08:09.000100101Z", TimestampPrecisionNanosecond, TimezoneUTC, 9)
+	test("1234-05-06T07:08:09.99999999999Z", "1234-05-06T07:08:10.000000000Z", TimestampPrecisionNanosecond, TimezoneUTC, 9)
+	test("1234-12-31T23:59:59.99999999999Z", "1235-01-01T00:00:00.000000000Z", TimestampPrecisionNanosecond, TimezoneUTC, 9)
+	test("1234-05-06T07:08:09.000100100+09:10", "1234-05-06T07:08:09.000100100+09:10", TimestampPrecisionNanosecond, TimezoneLocal, 7)
+	test("1234-05-06T07:08:09.100100100-10:11", "1234-05-06T07:08:09.100100100-10:11", TimestampPrecisionNanosecond, TimezoneLocal, 7)
+	test("1234-05-06T07:08:09.00010010044+09:10", "1234-05-06T07:08:09.000100100+09:10", TimestampPrecisionNanosecond, TimezoneLocal, 7)
+	test("1234-05-06T07:08:09.00010010055-10:11", "1234-05-06T07:08:09.000100101-10:11", TimestampPrecisionNanosecond, TimezoneLocal, 9)
+	test("1234-05-06T07:08:09.00010010099+09:10", "1234-05-06T07:08:09.000100101+09:10", TimestampPrecisionNanosecond, TimezoneLocal, 9)
+	test("1234-05-06T07:08:09.99999999999-10:11", "1234-05-06T07:08:10.000000000-10:11", TimestampPrecisionNanosecond, TimezoneLocal, 9)
+	test("1234-12-31T23:59:59.99999999999+09:10", "1235-01-01T00:00:00.000000000+09:10", TimestampPrecisionNanosecond, TimezoneLocal, 9)
+
+	test("1234-05-06T07:08+09:10", "1234-05-06T07:08:00+09:10", TimestampPrecisionMinute, TimezoneLocal, 0)
+	test("1234-05-06T07:08:09-10:11", "1234-05-06T07:08:09-10:11", TimestampPrecisionSecond, TimezoneLocal, 0)
 }
 
 func TestWriteSymbol(t *testing.T) {
diff --git a/ion/textwriter_test.go b/ion/textwriter_test.go
index 2aa7671..46fcd94 100644
--- a/ion/textwriter_test.go
+++ b/ion/textwriter_test.go
@@ -239,8 +239,10 @@ func TestWriteTextDecimal(t *testing.T) {
 func TestWriteTextTimestamp(t *testing.T) {
 	expected := "1970-01-01T00:00:00.001Z\n1970-01-01T01:23:00+01:23"
 	testTextWriter(t, expected, func(w Writer) {
-		w.WriteTimestamp(time.Unix(0, 1000000).In(time.UTC))
-		w.WriteTimestamp(time.Unix(0, 0).In(time.FixedZone("wtf", 4980)))
+		dateTime := time.Unix(0, 1000000).In(time.UTC)
+		w.WriteTimestamp(NewTimestampWithFractionalSeconds(dateTime, TimestampPrecisionNanosecond, TimezoneUTC, 3))
+		dateTime = time.Unix(0, 0).In(time.FixedZone("foo", 4980))
+		w.WriteTimestamp(NewTimestamp(dateTime, TimestampPrecisionSecond, TimezoneLocal))
 	})
 }
 
diff --git a/ion/timestamp_test.go b/ion/timestamp_test.go
new file mode 100644
index 0000000..9cf0cb7
--- /dev/null
+++ b/ion/timestamp_test.go
@@ -0,0 +1,198 @@
+package ion
+
+import (
+	"testing"
+	"time"
+)
+
+func TestNewTimestampFromStr(t *testing.T) {
+	type args struct {
+		dateStr   string
+		precision TimestampPrecision
+		kind      TimezoneKind
+	}
+	tests := []struct {
+		testCase string
+		args     args
+		expected Timestamp
+	}{
+		{
+			testCase: "2000T",
+			args:     args{"2000T", TimestampPrecisionYear, TimezoneUnspecified},
+			expected: Timestamp{dateTime: time.Date(2000, time.Month(1), 1, 0, 0, 0, 0, time.UTC), precision: TimestampPrecisionYear},
+		},
+		{
+			testCase: "2000-01T",
+			args:     args{"2000-01T", TimestampPrecisionMonth, TimezoneUnspecified},
+			expected: Timestamp{dateTime: time.Date(2000, time.Month(1), 1, 0, 0, 0, 0, time.UTC), precision: TimestampPrecisionMonth},
+		},
+		{
+			testCase: "2000-01-02T",
+			args:     args{"2000-01-02T", TimestampPrecisionDay, TimezoneUnspecified},
+			expected: Timestamp{dateTime: time.Date(2000, time.Month(1), 2, 0, 0, 0, 0, time.UTC), precision: TimestampPrecisionDay},
+		},
+		{
+			testCase: "2000-01-02T03:04Z",
+			args:     args{"2000-01-02T03:04Z", TimestampPrecisionMinute, TimezoneUTC},
+			expected: Timestamp{dateTime: time.Date(2000, time.Month(1), 2, 3, 4, 0, 0, time.UTC), precision: TimestampPrecisionMinute, kind: TimezoneUTC},
+		},
+		{
+			testCase: "2000-01-02T03:04:05Z",
+			args:     args{"2000-01-02T03:04:05Z", TimestampPrecisionSecond, TimezoneUTC},
+			expected: Timestamp{dateTime: time.Date(2000, time.Month(1), 2, 3, 4, 5, 0, time.UTC), precision: TimestampPrecisionSecond, kind: TimezoneUTC},
+		},
+		{
+			testCase: "2000-01-02T03:04:05.1Z",
+			args:     args{"2000-01-02T03:04:05.1Z", TimestampPrecisionSecond, TimezoneUTC},
+			expected: Timestamp{dateTime: time.Date(2000, time.Month(1), 2, 3, 4, 5, 100000000, time.UTC), precision: TimestampPrecisionSecond, kind: TimezoneUTC},
+		},
+		{
+			testCase: "2000-01-02T03:04:05.12Z",
+			args:     args{"2000-01-02T03:04:05.12Z", TimestampPrecisionSecond, TimezoneUTC},
+			expected: Timestamp{dateTime: time.Date(2000, time.Month(1), 2, 3, 4, 5, 120000000, time.UTC), precision: TimestampPrecisionSecond, kind: TimezoneUTC},
+		},
+		{
+			testCase: "2000-01-02T03:04:05.123Z",
+			args:     args{"2000-01-02T03:04:05.123Z", TimestampPrecisionSecond, TimezoneUTC},
+			expected: Timestamp{dateTime: time.Date(2000, time.Month(1), 2, 3, 4, 5, 123000000, time.UTC), precision: TimestampPrecisionSecond, kind: TimezoneUTC},
+		},
+		{
+			testCase: "2000-01-02T03:04:05.1234Z",
+			args:     args{"2000-01-02T03:04:05.1234Z", TimestampPrecisionSecond, TimezoneUTC},
+			expected: Timestamp{dateTime: time.Date(2000, time.Month(1), 2, 3, 4, 5, 123400000, time.UTC), precision: TimestampPrecisionSecond, kind: TimezoneUTC},
+		},
+		{
+			testCase: "2000-01-02T03:04:05.12345Z",
+			args:     args{"2000-01-02T03:04:05.12345Z", TimestampPrecisionSecond, TimezoneUTC},
+			expected: Timestamp{dateTime: time.Date(2000, time.Month(1), 2, 3, 4, 5, 123450000, time.UTC), precision: TimestampPrecisionSecond, kind: TimezoneUTC},
+		},
+		{
+			testCase: "2000-01-02T03:04:05.123456Z",
+			args:     args{"2000-01-02T03:04:05.123456Z", TimestampPrecisionSecond, TimezoneUTC},
+			expected: Timestamp{dateTime: time.Date(2000, time.Month(1), 2, 3, 4, 5, 123456000, time.UTC), precision: TimestampPrecisionSecond, kind: TimezoneUTC},
+		},
+		{
+			testCase: "2000-01-02T03:04:05.1234567Z",
+			args:     args{"2000-01-02T03:04:05.1234567Z", TimestampPrecisionSecond, TimezoneUTC},
+			expected: Timestamp{dateTime: time.Date(2000, time.Month(1), 2, 3, 4, 5, 123456700, time.UTC), precision: TimestampPrecisionSecond, kind: TimezoneUTC},
+		},
+		{
+			testCase: "2000-01-02T03:04:05.12345678Z",
+			args:     args{"2000-01-02T03:04:05.12345678Z", TimestampPrecisionSecond, TimezoneUTC},
+			expected: Timestamp{dateTime: time.Date(2000, time.Month(1), 2, 3, 4, 5, 123456780, time.UTC), precision: TimestampPrecisionSecond, kind: TimezoneUTC},
+		},
+		{
+			testCase: "2000-01-02T03:04:05.123456789Z",
+			args:     args{"2000-01-02T03:04:05.123456789Z", TimestampPrecisionSecond, TimezoneUTC},
+			expected: Timestamp{dateTime: time.Date(2000, time.Month(1), 2, 3, 4, 5, 123456789, time.UTC), precision: TimestampPrecisionSecond, kind: TimezoneUTC},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.testCase, func(t *testing.T) {
+			actual, err := NewTimestampFromStr(tt.args.dateStr, tt.args.precision, tt.args.kind)
+			if err != nil {
+				t.Fatal(err)
+			}
+			if !actual.Equal(tt.expected) {
+				t.Fatalf("expected %v, got %v", tt.expected, actual)
+			}
+		})
+	}
+}
+
+func TestTimestampString(t *testing.T) {
+	type fields struct {
+		year                 int
+		month                int
+		day                  int
+		hour                 int
+		minute               int
+		second               int
+		nanosecond           int
+		precision            TimestampPrecision
+		numFractionalSeconds uint8
+	}
+
+	tests := []struct {
+		fields   fields
+		expected string
+	}{
+		{
+			fields:   fields{2000, 1, 1, 1, 0, 0, 0, TimestampPrecisionYear, 0},
+			expected: "2000T",
+		},
+		{
+			fields:   fields{2000, 1, 1, 1, 0, 0, 0, TimestampPrecisionMonth, 0},
+			expected: "2000-01T",
+		},
+		{
+			fields:   fields{2000, 1, 2, 1, 0, 0, 0, TimestampPrecisionDay, 0},
+			expected: "2000-01-02T",
+		},
+		{
+			fields:   fields{2000, 1, 2, 3, 4, 0, 0, TimestampPrecisionMinute, 0},
+			expected: "2000-01-02T03:04Z",
+		},
+		{
+			fields:   fields{2000, 1, 2, 3, 4, 5, 0, TimestampPrecisionSecond, 0},
+			expected: "2000-01-02T03:04:05Z",
+		},
+		{
+			fields:   fields{2000, 1, 2, 3, 4, 5, 100000000, TimestampPrecisionNanosecond, 1},
+			expected: "2000-01-02T03:04:05.1Z",
+		},
+		{
+			fields:   fields{2000, 1, 2, 3, 4, 5, 120000000, TimestampPrecisionNanosecond, 2},
+			expected: "2000-01-02T03:04:05.12Z",
+		},
+		{
+			fields:   fields{2000, 1, 2, 3, 4, 5, 123000000, TimestampPrecisionNanosecond, 3},
+			expected: "2000-01-02T03:04:05.123Z",
+		},
+		{
+			fields:   fields{2000, 1, 2, 3, 4, 5, 123400000, TimestampPrecisionNanosecond, 4},
+			expected: "2000-01-02T03:04:05.1234Z",
+		},
+		{
+			fields:   fields{2000, 1, 2, 3, 4, 5, 123450000, TimestampPrecisionNanosecond, 5},
+			expected: "2000-01-02T03:04:05.12345Z",
+		},
+		{
+			fields:   fields{2000, 1, 2, 3, 4, 5, 123456000, TimestampPrecisionNanosecond, 6},
+			expected: "2000-01-02T03:04:05.123456Z",
+		},
+		{
+			fields:   fields{2000, 1, 2, 3, 4, 5, 123456700, TimestampPrecisionNanosecond, 7},
+			expected: "2000-01-02T03:04:05.1234567Z",
+		},
+		{
+			fields:   fields{2000, 1, 2, 3, 4, 5, 123456780, TimestampPrecisionNanosecond, 8},
+			expected: "2000-01-02T03:04:05.12345678Z",
+		},
+		{
+			fields:   fields{2000, 1, 2, 3, 4, 5, 123456789, TimestampPrecisionNanosecond, 9},
+			expected: "2000-01-02T03:04:05.123456789Z",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.expected, func(t *testing.T) {
+			dateTime := time.Date(tt.fields.year, time.Month(tt.fields.month), tt.fields.day, tt.fields.hour,
+				tt.fields.minute, tt.fields.second, tt.fields.nanosecond, time.UTC)
+
+			kind := TimezoneUnspecified
+			if tt.fields.precision >= TimestampPrecisionMinute {
+				kind = TimezoneUTC
+			}
+
+			ts := &Timestamp{
+				dateTime:             dateTime,
+				precision:            tt.fields.precision,
+				kind:                 kind,
+				numFractionalSeconds: tt.fields.numFractionalSeconds,
+			}
+			if actual := ts.String(); actual != tt.expected {
+				t.Fatalf("expected %v, got %v", tt.expected, actual)
+			}
+		})
+	}
+}
diff --git a/ion/unmarshal_test.go b/ion/unmarshal_test.go
index f00a0a9..940e639 100644
--- a/ion/unmarshal_test.go
+++ b/ion/unmarshal_test.go
@@ -282,9 +282,9 @@ func TestUnmarshalBinary(t *testing.T) {
 			case *Decimal:
 				thisDecimal := ionDecimal{thisValue}
 				res = thisDecimal.eq(ionDecimal{eval.(*Decimal)})
-			case time.Time:
+			case Timestamp:
 				thisTime := ionTimestamp{thisValue}
-				res = thisTime.eq(ionTimestamp{eval.(time.Time)})
+				res = thisTime.eq(ionTimestamp{eval.(Timestamp)})
 			default:
 				res = reflect.DeepEqual(val, eval)
 			}
@@ -318,9 +318,10 @@ func TestUnmarshalBinary(t *testing.T) {
 	decimalBytes := prefixIVM([]byte{0x51, 0xFF}) // 0d-63
 	test(decimalBytes, decimalVal, MustParseDecimal("0d-63"))
 
-	var timeValue time.Time
-	timeBytes := prefixIVM([]byte{0x67, 0xC0, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86}) // 0001-02-03T04:05:06Z
-	test(timeBytes, timeValue, time.Date(1, time.Month(2), 3, 4, 5, 6, 0, time.FixedZone("fixed", 0)))
+	var timestampValue Timestamp
+	timestampBytes := prefixIVM([]byte{0x67, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86}) // 0001-02-03T04:05:06Z
+	dateTime := time.Date(1, time.Month(2), 3, 4, 5, 6, 0, time.FixedZone("fixed", 0))
+	test(timestampBytes, timestampValue, NewTimestamp(dateTime, TimestampPrecisionSecond, TimezoneUTC))
 
 	var symbolVal string
 	symbolBytes := prefixIVM([]byte{0x71, 0x0A}) // $10
@@ -458,24 +459,24 @@ func TestDecodeDecimal(t *testing.T) {
 	test("1.20", MustParseDecimal("1.20"))
 }
 
-func TestDecodeTimeTo(t *testing.T) {
-	test := func(str string, eval time.Time) {
+func TestDecodeTimestampTo(t *testing.T) {
+	test := func(str string, eval Timestamp) {
 		t.Run(str, func(t *testing.T) {
 			d := NewDecoder(NewReaderStr(str))
 
-			var val time.Time
+			var val Timestamp
 			err := d.DecodeTo(&val)
 			if err != nil {
 				t.Fatal(err)
 			}
 
-			if val != eval {
+			if !val.Equal(eval) {
 				t.Errorf("expected %v, got %v", eval, val)
 			}
 		})
 	}
-	test("null", time.Time{})
-	test("2020T", time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC))
+	test("null", Timestamp{})
+	test("2020T", NewDateTimestamp(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), TimestampPrecisionYear))
 }
 
 func TestDecodeStringTo(t *testing.T) {
@@ -636,7 +637,7 @@ func TestDecode(t *testing.T) {
 
 	test("0.", MustParseDecimal("0."))
 
-	test("2020T", time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC))
+	test("2020T", NewDateTimestamp(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), TimestampPrecisionYear))
 
 	test("hello", "hello")
 	test("\"hello\"", "hello")
@@ -724,7 +725,7 @@ func TestUnmarshalWithAnnotation(t *testing.T) {
 	test("with::multiple::annotations::2.1e1", "float", foo{2.1e1, []string{"with", "multiple", "annotations"}})
 	test("with::multiple::annotations::2.2", "decimal", foo{MustParseDecimal("2.2"), []string{"with", "multiple", "annotations"}})
 	test("with::multiple::annotations::\"abc\"", "string", foo{"abc", []string{"with", "multiple", "annotations"}})
-	timestamp := time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC)
+	timestamp := NewTimestamp(time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC), TimestampPrecisionSecond, TimezoneUTC)
 	test("with::multiple::annotations::2000-01-02T03:04:05Z", "timestamp", foo{timestamp, []string{"with", "multiple", "annotations"}})
 	test("with::multiple::annotations::{{'''abc'''}}", "clob", foo{[]byte{97, 98, 99}, []string{"with", "multiple", "annotations"}})
 	test("with::multiple::annotations::{{/w==}}", "blob", foo{[]byte{255}, []string{"with", "multiple", "annotations"}})