Repository: amzn/ion-go
Pull request #177 (2021-05-28 22:55:08)

Correct the coefficient value of timestamp nanoseconds when writing to Ion binary
**Issue #, if available:**
* #173 
* [error inserting ion.Timestamp values](https://github.com/awslabs/amazon-qldb-driver-go/issues/52)
* [error inserting time.Time values](https://github.com/awslabs/amazon-qldb-driver-go/issues/51)


This PR is to address digest mismatch error when passing various timestamp objects into [QLDB driver for Go](https://github.com/awslabs/amazon-qldb-driver-go).

**Timestamps tested:**
* `timestamp := time.Now()`
* `timestamp := time.Now().UTC()`
*
        millis := int64(1608218715340)
        t := time.Unix(0, millis*1000000)
        timeStamp := ion.NewTimestampWithFractionalSeconds(t, ion.TimestampPrecisionNanosecond, ion.TimezoneUTC, 3)
* `timestamp := ion.NewTimestampWithFractionalSeconds(time.Now().UTC(), ion.TimestampPrecisionNanosecond, ion.TimezoneUTC, 9)`
* `timestamp := ion.NewTimestampWithFractionalSeconds(time.Now().UTC(), ion.TimestampPrecisionSecond, ion.TimezoneUTC, 9)`
*  `timestamp, _ := ion.ParseTimestamp(t.Format("2006-01-02T15:04:05.000000Z"))`
* `timestamp := ion.NewTimestamp(t, ion.TimestampPrecisionNanosecond, ion.TimezoneUTC)`
* `timestamp := time.Date(2021, time.May, 25, 13, 41, 31, 90000, time.UTC)`

**Description of changes:**
Writing a timestamp nanosecond into Ion binary will write bytes of two values: Coefficient and exponent. These two values are used to calculate the nanoseconds using the equation: `coefficient * 10 ^ -exponent`.

For example a timestamp:  `2021-05-27 00:19:05.000050000` has coefficient: `50000` and exponent: `9`.

Inside `encodeTimeDate` function, we get the correct coefficient by calling `time.Nanosecond` which returns `50000` however the exponent is calculated by converting the coefficient to string and getting the length which returns `5` and is incorrect. The fix is to set the exponent to `9` if there are nanoseconds because `time.time` will always contain 9 precision units.

The next step is to read the binary ion timestamps correctly. Original implementation was to look at the coefficient value and remove trailing zeros but the binary ion already contains a byte containing the exponent so we can just use that.


Has gone through internal BQ review with comments in a [PR](https://github.com/byronlin13/ion-go/pull/1) to my fork.

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/177?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#177](https://codecov.io/gh/amzn/ion-go/pull/177?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (3c5587e) into [master](https://codecov.io/gh/amzn/ion-go/commit/4e5ade8b64d070af5ec73a897fbb401f737ca487?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (4e5ade8) will **decrease** coverage by `0.02%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/amzn/ion-go/pull/177/graphs/tree.svg?width=650&height=150&src=pr&token=OBDT1TNoNE&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)](https://codecov.io/gh/amzn/ion-go/pull/177?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)

```diff
@@            Coverage Diff             @@
##           master     #177      +/-   ##
==========================================
- Coverage   74.67%   74.64%   -0.03%     
==========================================
  Files          25       25              
  Lines        4928     4923       -5     
==========================================
- Hits         3680     3675       -5     
  Misses        743      743              
  Partials      505      505              
```


| [Impacted Files](https://codecov.io/gh/amzn/ion-go/pull/177?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [ion/bitstream.go](https://codecov.io/gh/amzn/ion-go/pull/177/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-aW9uL2JpdHN0cmVhbS5nbw==) | `79.23% <100.00%> (-0.24%)` | :arrow_down: |
| [ion/marshal.go](https://codecov.io/gh/amzn/ion-go/pull/177/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-aW9uL21hcnNoYWwuZ28=) | `71.00% <100.00%> (ø)` | |
| [ion/timestamp.go](https://codecov.io/gh/amzn/ion-go/pull/177/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-aW9uL3RpbWVzdGFtcC5nbw==) | `67.55% <100.00%> (+0.14%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/amzn/ion-go/pull/177?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/amzn/ion-go/pull/177?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Last update [4e5ade8...3c5587e](https://codecov.io/gh/amzn/ion-go/pull/177?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).


Patch:
diff --git a/ion/bitstream.go b/ion/bitstream.go
index 4408f6a..545ea8b 100644
--- a/ion/bitstream.go
+++ b/ion/bitstream.go
@@ -652,30 +652,12 @@ func (b *bitstream) ReadTimestamp() (Timestamp, error) {
 
 	// 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)
+		nsecs, overflow, fractionPrecision, err = b.readNsecs(length)
 		if err != nil {
 			return Timestamp{}, err
 		}
 
-		nsecs, overflow, err = b.readNsecs(length)
-		if err != nil {
-			return Timestamp{}, err
-		}
-
-		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
+		if fractionPrecision > 0 {
 			precision = TimestampPrecisionNanosecond
 		}
 	}
@@ -692,32 +674,34 @@ func (b *bitstream) ReadTimestamp() (Timestamp, error) {
 }
 
 // ReadNsecs reads the fraction part of a timestamp and rounds to nanoseconds.
-// This function returns the nanoseconds as an int, overflow as a bool, and an error
+// This function returns the nanoseconds as an int, overflow as a bool, exponent as an uint8, and an error
 // if there was a problem executing this function.
-func (b *bitstream) readNsecs(length uint64) (int, bool, error) {
+func (b *bitstream) readNsecs(length uint64) (int, bool, uint8, error) {
 	d, err := b.readDecimal(length)
 	if err != nil {
-		return 0, false, err
+		return 0, false, 0, err
 	}
 
 	nsec, err := d.ShiftL(9).trunc()
 	if err != nil || nsec < 0 || nsec > 999999999 {
 		msg := fmt.Sprintf("invalid timestamp fraction: %v", d)
-		return 0, false, &SyntaxError{msg, b.pos}
+		return 0, false, 0, &SyntaxError{msg, b.pos}
 	}
 
 	nsec, err = d.ShiftL(9).round()
 	if err != nil {
 		msg := fmt.Sprintf("invalid timestamp fraction: %v", d)
-		return 0, false, &SyntaxError{msg, b.pos}
+		return 0, false, 0, &SyntaxError{msg, b.pos}
 	}
 
+	exponent := uint8(d.scale)
+
 	// Overflow to second.
 	if nsec == 1000000000 {
-		return 0, true, nil
+		return 0, true, exponent, nil
 	}
 
-	return int(nsec), false, nil
+	return int(nsec), false, exponent, nil
 }
 
 // ReadDecimal reads a decimal value of the given length: an exponent encoded as a
diff --git a/ion/marshal.go b/ion/marshal.go
index 8ee8533..586048d 100644
--- a/ion/marshal.go
+++ b/ion/marshal.go
@@ -22,7 +22,6 @@ import (
 	"math/big"
 	"reflect"
 	"sort"
-	"strconv"
 	"time"
 )
 
@@ -443,15 +442,8 @@ func (m *Encoder) encodeTimeDate(v reflect.Value) error {
 		kind = TimezoneUnspecified
 	}
 
-	// Get number of fractional seconds precisions
-	ns := t.Nanosecond()
-	numFractionalSeconds := 0
-	if ns > 0 {
-		numFractionalSeconds = len(strconv.Itoa(ns))
-	}
-
 	// Time.Date has nano second component
-	timestamp := NewTimestampWithFractionalSeconds(t, TimestampPrecisionNanosecond, kind, uint8(numFractionalSeconds))
+	timestamp := NewTimestampWithFractionalSeconds(t, TimestampPrecisionNanosecond, kind, maxFractionalPrecision)
 	return m.w.WriteTimestamp(timestamp)
 }
 
diff --git a/ion/timestamp.go b/ion/timestamp.go
index 96c6155..dd73bf5 100644
--- a/ion/timestamp.go
+++ b/ion/timestamp.go
@@ -36,6 +36,8 @@ const (
 	TimestampPrecisionNanosecond
 )
 
+const maxFractionalPrecision = 9
+
 func (tp TimestampPrecision) String() string {
 	switch tp {
 	case TimestampNoPrecision:
@@ -131,23 +133,34 @@ type Timestamp struct {
 
 // 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}
+	numDecimalPlacesOfFractionalSeconds := uint8(0)
+	if precision >= TimestampPrecisionNanosecond {
+		numDecimalPlacesOfFractionalSeconds = maxFractionalPrecision
+	}
+	return Timestamp{dateTime, precision, TimezoneUnspecified, numDecimalPlacesOfFractionalSeconds}
 }
 
 // NewTimestamp constructor
 func NewTimestamp(dateTime time.Time, precision TimestampPrecision, kind TimezoneKind) Timestamp {
+	numDecimalPlacesOfFractionalSeconds := uint8(0)
+
 	if precision <= TimestampPrecisionDay {
 		// Timestamps with Year, Month, or Day precision necessarily have TimezoneUnspecified timezone.
 		kind = TimezoneUnspecified
+	} else if precision >= TimestampPrecisionNanosecond {
+		numDecimalPlacesOfFractionalSeconds = maxFractionalPrecision
 	}
-	return Timestamp{dateTime, precision, kind, 0}
+	return Timestamp{dateTime, precision, kind, numDecimalPlacesOfFractionalSeconds}
 }
 
 // NewTimestampWithFractionalSeconds constructor
 func NewTimestampWithFractionalSeconds(dateTime time.Time, precision TimestampPrecision, kind TimezoneKind, fractionPrecision uint8) Timestamp {
-	if fractionPrecision > 9 {
+	if fractionPrecision > maxFractionalPrecision {
 		// 9 is the max precision supported
-		fractionPrecision = 9
+		fractionPrecision = maxFractionalPrecision
+	}
+	if precision < TimestampPrecisionNanosecond {
+		fractionPrecision = 0
 	}
 	return Timestamp{dateTime, precision, kind, fractionPrecision}
 }
@@ -159,13 +172,8 @@ func NewTimestampFromStr(dateStr string, precision TimestampPrecision, kind Time
 	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++
 			}
@@ -173,16 +181,6 @@ func NewTimestampFromStr(dateStr string, precision TimestampPrecision, kind Time
 			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--
-				}
-			}
 		}
 	}
 
@@ -461,7 +459,7 @@ func (ts Timestamp) String() string {
 	// 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 {
+	if ts.precision >= TimestampPrecisionNanosecond && ts.numFractionalSeconds > 0 {
 		// Find the position of 'T'
 		tIndex := strings.Index(format, "T")
 		if tIndex == -1 {
@@ -471,23 +469,39 @@ func (ts Timestamp) String() string {
 			}
 		}
 
-		index := strings.LastIndex(format, "Z")
-		if index == -1 || index < tIndex {
-			index = strings.LastIndex(format, "+")
-			if index == -1 || index < tIndex {
-				index = strings.LastIndex(format, "-")
+		timeZoneIndex := strings.LastIndex(format, "Z")
+		if timeZoneIndex == -1 || timeZoneIndex < tIndex {
+			timeZoneIndex = strings.LastIndex(format, "+")
+			if timeZoneIndex == -1 || timeZoneIndex < tIndex {
+				timeZoneIndex = strings.LastIndex(format, "-")
 			}
 		}
 
 		// This position better be right of 'T'
-		if index != -1 && tIndex < index {
+		if timeZoneIndex != -1 && tIndex < timeZoneIndex {
 			zeros := strings.Builder{}
-			zeros.WriteByte('.')
-			for i := uint8(0); i < ts.numFractionalSeconds; i++ {
+			numZerosNeeded := 0
+
+			// Specify trailing zeros if fractional precision is less than the nanoseconds.
+			// e.g. A timestamp: 2021-05-25T13:41:31.00001234 with fractional precision: 2 will return "2021-05-25 13:41:31.00"
+			ns := ts.dateTime.Nanosecond()
+			if ns == 0 || maxFractionalPrecision-len(strconv.Itoa(ns)) >= int(ts.numFractionalSeconds) {
+				zeros.WriteByte('.')
+				numZerosNeeded = int(ts.numFractionalSeconds)
+			} else {
+				decimalPlaceIndex := strings.LastIndex(format, ".")
+				if decimalPlaceIndex != -1 {
+					decimalPlacesOccupied := timeZoneIndex - decimalPlaceIndex - 1
+					numZerosNeeded = int(ts.numFractionalSeconds) - decimalPlacesOccupied
+				}
+			}
+
+			// Add trailing zeros until the fractional seconds component is the correct length
+			for i := 0; i < numZerosNeeded; i++ {
 				zeros.WriteByte('0')
 			}
 
-			format = format[0:index] + zeros.String() + format[index:]
+			format = format[0:timeZoneIndex] + zeros.String() + format[timeZoneIndex:]
 		}
 	}
 
@@ -515,12 +529,13 @@ func (ts Timestamp) Equal(ts1 Timestamp) bool {
 		ts.numFractionalSeconds == ts1.numFractionalSeconds
 }
 
-// TruncatedNanoseconds returns nanoseconds with trailing zeros removed (ie. 123456000 gets truncated to 123456).
+// TruncatedNanoseconds returns nanoseconds with trailing values removed up to the difference of max fractional precision - time stamp's fractional precision
+// e.g. 123456000 with fractional precision: 3 will get truncated to 123.
 func (ts Timestamp) TruncatedNanoseconds() int {
 	nsecs := ts.dateTime.Nanosecond()
-	for i := uint8(0); i < (9-ts.numFractionalSeconds) && nsecs > 0 && (nsecs%10) == 0; i++ {
+
+	for i := uint8(0); i < (maxFractionalPrecision-ts.numFractionalSeconds) && nsecs > 0; i++ {
 		nsecs /= 10
 	}
-
 	return nsecs
 }


Test patch:
diff --git a/ion/binarywriter_test.go b/ion/binarywriter_test.go
index f94ef1d..d4172ee 100644
--- a/ion/binarywriter_test.go
+++ b/ion/binarywriter_test.go
@@ -186,7 +186,7 @@ func TestWriteBinaryTimestamp(t *testing.T) {
 	nowish, _ := NewTimestampFromStr("2019-08-04T18:15:43.863494+10:00", TimestampPrecisionNanosecond, TimezoneLocal)
 
 	testBinaryWriter(t, eval, func(w Writer) {
-		assert.NoError(t, w.WriteTimestamp(NewTimestamp(time.Time{}, TimestampPrecisionNanosecond, TimezoneUTC)))
+		assert.NoError(t, w.WriteTimestamp(NewTimestamp(time.Time{}, TimestampPrecisionSecond, TimezoneUTC)))
 		assert.NoError(t, w.WriteTimestamp(nowish))
 	})
 }
diff --git a/ion/marshal_test.go b/ion/marshal_test.go
index b763aac..3508a96 100644
--- a/ion/marshal_test.go
+++ b/ion/marshal_test.go
@@ -52,11 +52,12 @@ func TestMarshalText(t *testing.T) {
 	test(MustParseDecimal("1.20"), "1.20")
 	test(NewTimestamp(time.Date(2010, 1, 1, 0, 0, 0, 0, time.UTC), TimestampPrecisionSecond, TimezoneUTC),
 		"2010-01-01T00:00:00Z")
-	test(time.Date(2010, 1, 1, 0, 0, 0, 770000000, time.UTC), "2010-01-01T00:00:00.77Z")
+	test(time.Date(2010, 1, 1, 0, 0, 0, 1, time.UTC), "2010-01-01T00:00:00.000000001Z")
+	test(time.Date(2010, 1, 1, 0, 0, 0, 770000000, time.UTC), "2010-01-01T00:00:00.770000000Z")
 	loc, _ := time.LoadLocation("EST")
-	test(time.Date(2010, 1, 1, 0, 0, 0, 0, loc), "2010-01-01T00:00:00-05:00")
+	test(time.Date(2010, 1, 1, 0, 0, 0, 0, loc), "2010-01-01T00:00:00.000000000-05:00")
 	loc = time.FixedZone("UTC+8", 8*60*60)
-	test(time.Date(2010, 1, 1, 0, 0, 0, 0, loc), "2010-01-01T00:00:00+08:00")
+	test(time.Date(2010, 1, 1, 0, 0, 0, 0, loc), "2010-01-01T00:00:00.000000000+08:00")
 
 	test("hello\tworld", "\"hello\\tworld\"")
 
@@ -138,6 +139,18 @@ func TestMarshalBinary(t *testing.T) {
 		0x8A, 0x21, 0x2A,
 		0x8B, 0x20,
 	}))
+
+	test(time.Date(2010, 1, 1, 0, 0, 0, 1, time.UTC), "time with 1 nanosecond", prefixIVM([]byte{
+		0x6A, 0x80, 0x0F, 0xDA, 0x81, 0x81, 0x80, 0x80, 0x80,
+		0xC9, // exponent: 9
+		0x01, // coefficient: 1
+	}))
+
+	test(time.Date(2010, 1, 1, 0, 0, 0, 5000, time.UTC), "time with 5000 nanoseconds", prefixIVM([]byte{
+		0x6B, 0x80, 0x0F, 0xDA, 0x81, 0x81, 0x80, 0x80, 0x80,
+		0xC9,       // exponent: 9
+		0x13, 0x88, // coefficient: 5000
+	}))
 }
 
 func prefixIVM(data []byte) []byte {
diff --git a/ion/textutils_test.go b/ion/textutils_test.go
index eeddda7..cb969a0 100644
--- a/ion/textutils_test.go
+++ b/ion/textutils_test.go
@@ -45,21 +45,20 @@ func TestParseTimestamp(t *testing.T) {
 	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("1234-05-06T07:08:09.100Z", "1234-05-06T07:08:09.100Z", TimestampPrecisionNanosecond, TimezoneUTC, 3)
+	test("1234-05-06T07:08:09.100100Z", "1234-05-06T07:08:09.100100Z", TimestampPrecisionNanosecond, TimezoneUTC, 6)
 
 	// Test rounding of >=9 fractional seconds.
-	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.000100100Z", "1234-05-06T07:08:09.000100100Z", TimestampPrecisionNanosecond, TimezoneUTC, 9)
+	test("1234-05-06T07:08:09.100100100Z", "1234-05-06T07:08:09.100100100Z", TimestampPrecisionNanosecond, TimezoneUTC, 9)
+	test("1234-05-06T07:08:09.00010010044Z", "1234-05-06T07:08:09.000100100Z", TimestampPrecisionNanosecond, TimezoneUTC, 9)
 	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.000100100+09:10", "1234-05-06T07:08:09.000100100+09:10", TimestampPrecisionNanosecond, TimezoneLocal, 9)
+	test("1234-05-06T07:08:09.100100100-10:11", "1234-05-06T07:08:09.100100100-10:11", TimestampPrecisionNanosecond, TimezoneLocal, 9)
+	test("1234-05-06T07:08:09.00010010044+09:10", "1234-05-06T07:08:09.000100100+09:10", TimestampPrecisionNanosecond, TimezoneLocal, 9)
 	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)
diff --git a/ion/timestamp_test.go b/ion/timestamp_test.go
index 688d983..c8f0049 100644
--- a/ion/timestamp_test.go
+++ b/ion/timestamp_test.go
@@ -104,6 +104,11 @@ func TestNewTimestampFromStr(t *testing.T) {
 			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},
 		},
+		{
+			testCase: "2000-01-02T03:04:05.123000000Z",
+			args:     args{"2000-01-02T03:04:05.123000000Z", TimestampPrecisionSecond, TimezoneUTC},
+			expected: Timestamp{dateTime: time.Date(2000, time.Month(1), 2, 3, 4, 5, 123000000, time.UTC), precision: TimestampPrecisionSecond, kind: TimezoneUTC},
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.testCase, func(t *testing.T) {
@@ -155,6 +160,18 @@ func TestTimestampString(t *testing.T) {
 			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, 220000000, TimestampPrecisionNanosecond, 1},
+			expected: "2000-01-02T03:04:05.2Z",
+		},
+		{
+			fields:   fields{2000, 1, 2, 3, 4, 5, 12000000, TimestampPrecisionNanosecond, 1},
+			expected: "2000-01-02T03:04:05.0Z",
+		},
+		{
+			fields:   fields{2000, 1, 2, 3, 4, 5, 12000000, TimestampPrecisionNanosecond, 2},
+			expected: "2000-01-02T03:04:05.01Z",
+		},
 		{
 			fields:   fields{2000, 1, 2, 3, 4, 5, 120000000, TimestampPrecisionNanosecond, 2},
 			expected: "2000-01-02T03:04:05.12Z",
@@ -164,7 +181,7 @@ func TestTimestampString(t *testing.T) {
 			expected: "2000-01-02T03:04:05.123Z",
 		},
 		{
-			fields:   fields{2000, 1, 2, 3, 4, 5, 123400000, TimestampPrecisionNanosecond, 4},
+			fields:   fields{2000, 1, 2, 3, 4, 5, 123456789, TimestampPrecisionNanosecond, 4},
 			expected: "2000-01-02T03:04:05.1234Z",
 		},
 		{
@@ -208,3 +225,148 @@ func TestTimestampString(t *testing.T) {
 		})
 	}
 }
+
+func TestTruncateNanoseconds(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
+		name     string
+		expected int
+	}{
+		{
+			fields:   fields{2000, 1, 1, 1, 0, 0, 0, TimestampPrecisionYear, 0},
+			name:     "2000T",
+			expected: 0,
+		},
+		{
+			fields:   fields{2000, 1, 1, 1, 0, 0, 0, TimestampPrecisionMonth, 0},
+			name:     "2000-01T",
+			expected: 0,
+		},
+		{
+			fields:   fields{2000, 1, 2, 1, 0, 0, 0, TimestampPrecisionDay, 0},
+			name:     "2000-01-02T",
+			expected: 0,
+		},
+		{
+			fields:   fields{2000, 1, 2, 3, 4, 0, 0, TimestampPrecisionMinute, 0},
+			name:     "2000-01-02T03:04Z",
+			expected: 0,
+		},
+		{
+			fields:   fields{2000, 1, 2, 3, 4, 5, 0, TimestampPrecisionSecond, 0},
+			name:     "2000-01-02T03:04:05Z",
+			expected: 0,
+		},
+		{
+			fields:   fields{2000, 1, 2, 3, 4, 5, 100000000, TimestampPrecisionNanosecond, 1},
+			name:     "2000-01-02T03:04:05.1Z",
+			expected: 1,
+		},
+		{
+			fields:   fields{2000, 1, 2, 3, 4, 5, 220000000, TimestampPrecisionNanosecond, 1},
+			name:     "2000-01-02T03:04:05.2Z",
+			expected: 2,
+		},
+		{
+			fields:   fields{2000, 1, 2, 3, 4, 5, 12000000, TimestampPrecisionNanosecond, 1},
+			name:     "2000-01-02T03:04:05.0Z",
+			expected: 0,
+		},
+		{
+			fields:   fields{2000, 1, 2, 3, 4, 5, 12000000, TimestampPrecisionNanosecond, 2},
+			name:     "2000-01-02T03:04:05.01Z",
+			expected: 1,
+		},
+		{
+			fields:   fields{2000, 1, 2, 3, 4, 5, 120000000, TimestampPrecisionNanosecond, 2},
+			name:     "2000-01-02T03:04:05.12Z",
+			expected: 12,
+		},
+		{
+			fields:   fields{2000, 1, 2, 3, 4, 5, 123000000, TimestampPrecisionNanosecond, 3},
+			name:     "2000-01-02T03:04:05.123Z",
+			expected: 123,
+		},
+		{
+			fields:   fields{2000, 1, 2, 3, 4, 5, 123456789, TimestampPrecisionNanosecond, 4},
+			name:     "2000-01-02T03:04:05.1234Z",
+			expected: 1234,
+		},
+		{
+			fields:   fields{2000, 1, 2, 3, 4, 5, 123450000, TimestampPrecisionNanosecond, 5},
+			name:     "2000-01-02T03:04:05.12345Z",
+			expected: 12345,
+		},
+		{
+			fields:   fields{2000, 1, 2, 3, 4, 5, 123456000, TimestampPrecisionNanosecond, 6},
+			name:     "2000-01-02T03:04:05.123456Z",
+			expected: 123456,
+		},
+		{
+			fields:   fields{2000, 1, 2, 3, 4, 5, 123456700, TimestampPrecisionNanosecond, 7},
+			name:     "2000-01-02T03:04:05.1234567Z",
+			expected: 1234567,
+		},
+		{
+			fields:   fields{2000, 1, 2, 3, 4, 5, 123456780, TimestampPrecisionNanosecond, 8},
+			name:     "2000-01-02T03:04:05.12345678Z",
+			expected: 12345678,
+		},
+		{
+			fields:   fields{2000, 1, 2, 3, 4, 5, 123456789, TimestampPrecisionNanosecond, 9},
+			name:     "2000-01-02T03:04:05.123456789Z",
+			expected: 123456789,
+		},
+		{
+			fields:   fields{2000, 1, 2, 3, 4, 5, 5000, TimestampPrecisionNanosecond, 2},
+			name:     "2000-01-02T03:04:05.000005000",
+			expected: 0,
+		},
+		{
+			fields:   fields{2000, 1, 2, 3, 4, 5, 6000, TimestampPrecisionNanosecond, 5},
+			name:     "2000-01-02T03:04:05.000006000",
+			expected: 0,
+		},
+		{
+			fields:   fields{2000, 1, 2, 3, 4, 5, 7000, TimestampPrecisionNanosecond, 6},
+			name:     "2000-01-02T03:04:05.000007000",
+			expected: 7,
+		},
+		{
+			fields:   fields{2000, 1, 2, 3, 4, 5, 7001, TimestampPrecisionNanosecond, 6},
+			name:     "2000-01-02T03:04:05.000007001",
+			expected: 7,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, 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,
+			}
+			assert.Equal(t, tt.expected, ts.TruncatedNanoseconds())
+		})
+	}
+}