Repository: amzn/ion-java
Pull request #302 (2020-08-12 18:33:38)

Adds Timestamp.forEpochSecond()
Implements #301 by adding `Timestamp.forEpochSecond` to allow easy conversion from Java 8's `java.time.Instant`.

This test works on my local machine when I target Java 8.  (It's not included in this PR since we can't yet update `ion-java` to Java 8.)

```Java
@Test
public void convertFromInstantTest() {
    Instant i = Instant.now();
    Timestamp ts = Timestamp.forEpochSecond(i.getEpochSecond(), i.getNano());
    assertEquals(i.toString(), ts.toString());
    System.out.println(i.toString());
    System.out.println(ts.toString());
}
```

Output is: 

```
2020-08-12T18:28:26.439Z
2020-08-12T18:28:26.439Z
```

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.


codecov[bot]: # [Codecov](https://codecov.io/gh/amzn/ion-java/pull/302?src=pr&el=h1) Report
> Merging [#302](https://codecov.io/gh/amzn/ion-java/pull/302?src=pr&el=desc) into [master](https://codecov.io/gh/amzn/ion-java/commit/b6842d54245fdef9366236856130d9cb086b6f37&el=desc) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

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

```diff
@@            Coverage Diff            @@
##             master     #302   +/-   ##
=========================================
  Coverage     63.87%   63.87%           
- Complexity     4806     4809    +3     
=========================================
  Files           136      136           
  Lines         21052    21059    +7     
  Branches       3813     3815    +2     
=========================================
+ Hits          13447    13452    +5     
  Misses         6267     6267           
- Partials       1338     1340    +2     
```


| [Impacted Files](https://codecov.io/gh/amzn/ion-java/pull/302?src=pr&el=tree) | Coverage Δ | Complexity Δ | |
|---|---|---|---|
| [src/com/amazon/ion/Timestamp.java](https://codecov.io/gh/amzn/ion-java/pull/302/diff?src=pr&el=tree#diff-c3JjL2NvbS9hbWF6b24vaW9uL1RpbWVzdGFtcC5qYXZh) | `88.95% <100.00%> (+0.10%)` | `266.00 <4.00> (+4.00)` | |
| [src/com/amazon/ion/impl/BlockedBuffer.java](https://codecov.io/gh/amzn/ion-java/pull/302/diff?src=pr&el=tree#diff-c3JjL2NvbS9hbWF6b24vaW9uL2ltcGwvQmxvY2tlZEJ1ZmZlci5qYXZh) | `50.85% <0.00%> (-0.25%)` | `77.00% <0.00%> (-1.00%)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/amzn/ion-java/pull/302?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-java/pull/302?src=pr&el=footer). Last update [b6842d5...bbef742](https://codecov.io/gh/amzn/ion-java/pull/302?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

wsargent: Conceptually, this looks perfect.

Patch:
diff --git a/src/com/amazon/ion/Timestamp.java b/src/com/amazon/ion/Timestamp.java
index 39df6e08..591d8652 100644
--- a/src/com/amazon/ion/Timestamp.java
+++ b/src/com/amazon/ion/Timestamp.java
@@ -84,18 +84,37 @@ public final class Timestamp
     private static final int NO_SECONDS = 0;
     private static final BigDecimal NO_FRACTIONAL_SECONDS = null;
 
-    // 0001-01-01T00:00:00.0Z in millis
+    /**
+     * 0001-01-01T00:00:00.0Z in millis.
+     */
     static final long MINIMUM_TIMESTAMP_IN_MILLIS = -62135769600000L;
 
-    // 0001-01-01T00:00:00.0Z in millis
+    /**
+     * 0001-01-01T00:00:00.0Z in millis.
+     */
     static final BigDecimal MINIMUM_TIMESTAMP_IN_MILLIS_DECIMAL = new BigDecimal(MINIMUM_TIMESTAMP_IN_MILLIS);
 
-    // 10000T in millis, upper bound exclusive
+    /**
+     * 10000T in millis, upper bound exclusive.
+     */
     static final long MAXIMUM_TIMESTAMP_IN_MILLIS = 253402300800000L;
 
-    // 10000T in millis, upper bound exclusive
+    /**
+     * 10000T in millis, upper bound exclusive.
+     */
     static final BigDecimal MAXIMUM_ALLOWED_TIMESTAMP_IN_MILLIS_DECIMAL = new BigDecimal(MAXIMUM_TIMESTAMP_IN_MILLIS);
 
+
+    /**
+     * 0001-01-01T00:00:00.0Z epoch seconds.
+     */
+    static final long MINIMUM_TIMESTAMP_IN_EPOCH_SECONDS = MINIMUM_TIMESTAMP_IN_MILLIS / 1000;
+
+    /**
+     * 10000T in epoch seconds, upper bound exclusive.
+     */
+    static final long MAXIMUM_TIMESTAMP_IN_EPOCH_SECONDS = MAXIMUM_TIMESTAMP_IN_MILLIS / 1000;
+
     /**
      * Unknown local offset from UTC.
      */
@@ -1254,16 +1273,19 @@ public final class Timestamp
     /**
      * Returns a Timestamp, precise to the minute, with a given local
      * offset.
-     * <p>
-     * This is equivalent to the corresponding Ion value
+     *
+     * <p>This is equivalent to the corresponding Ion value
      * {@code YYYY-MM-DDThh:mm+-oo:oo}, where {@code oo:oo} represents the
-     * hour and minutes of the local offset from UTC.
+     * hour and minutes of the local offset from UTC.</p>
+     *
+     * <p>The values of the {@code year}, {@code month}, {@code day},
+     * {@code hour}, and {@code minute} parameters are relative to the
+     * local {@code offset}.</p>
      *
      * @param offset
      *          the local offset from UTC, measured in minutes;
      *          may be {@code null} to represent an unknown local offset
      *
-
      */
     public static Timestamp forMinute(int year, int month, int day,
                                       int hour, int minute,
@@ -1275,10 +1297,14 @@ public final class Timestamp
 
     /**
      * Returns a Timestamp, precise to the second, with a given local offset.
-     * <p>
-     * This is equivalent to the corresponding Ion value
+     *
+     * <p>This is equivalent to the corresponding Ion value
      * {@code YYYY-MM-DDThh:mm:ss+-oo:oo}, where {@code oo:oo} represents the
-     * hour and minutes of the local offset from UTC.
+     * hour and minutes of the local offset from UTC.</p>
+     *
+     * <p>The values of the {@code year}, {@code month}, {@code day},
+     * {@code hour}, {@code minute} and {@code second} parameters are relative
+     * to the local {@code offset}.</p>
      *
      * @param offset
      *          the local offset from UTC, measured in minutes;
@@ -1296,19 +1322,19 @@ public final class Timestamp
 
     /**
      * Returns a Timestamp, precise to the second, with a given local offset.
-     * <p>
-     * This is equivalent to the corresponding Ion value
+     *
+     * <p>This is equivalent to the corresponding Ion value
      * {@code YYYY-MM-DDThh:mm:ss.sss+-oo:oo}, where {@code oo:oo} represents
-     * the hour and minutes of the local offset from UTC.
+     * the hour and minutes of the local offset from UTC.</p>
      *
-     * @param second must be at least zero and less than 60.
-     * Must not be null.
+     * <p>The values of the {@code year}, {@code month}, {@code day},
+     * {@code hour}, {@code minute} and {@code second} parameters are relative
+     * to the local {@code offset}.</p>
      *
-     * @param offset
-     *          the local offset from UTC, measured in minutes;
-     *          may be {@code null} to represent an unknown local offset
+     * @param second must be at least zero and less than 60. Must not be null.
+     * @param offset the local offset from UTC, measured in minutes;
+     *              may be {@code null} to represent an unknown local offset
      *
-
      */
     public static Timestamp forSecond(int year, int month, int day,
                                       int hour, int minute, BigDecimal second,
@@ -1323,46 +1349,33 @@ public final class Timestamp
 
 
     /**
-     * Returns a Timestamp that represents the point in time that is
-     * {@code millis} milliseconds from the epoch, with a given local offset.
-     * <p>
-     * The resulting Timestamp will be precise to the millisecond.
+     * Returns a Timestamp that represents the point in time that is {@code millis} milliseconds from the epoch,
+     * with a given local offset.
      *
-     * @param millis
-     * the number of milliseconds from the epoch (1970-01-01T00:00:00.000Z).
-     * @param localOffset
-     *          the local offset from UTC, measured in minutes;
-     *          may be {@code null} to represent an unknown local offset.
+     * <p>The resulting Timestamp will be precise to the millisecond.</p>
      *
-
+     * <p>{@code millis} is relative to UTC, regardless of the value supplied for {@code localOffset}.  This
+     * varies from the {@link #forMinute} and {@link #forSecond} methods that assume the specified date and time
+     * values are relative to the local offset.  For example, the following two Timestamps represent
+     * the same point in time:</p>
+     *
+     * <code>
+     * Timestamp theEpoch = Timestamp.forMillis(0, 0);
+     * Timestamp alsoTheEpoch = Timestamp.forSecond(1969, 12, 31, 23, 0, BigDecimal.ZERO, -60);
+     * </code>
+     *
+     * @param millis the number of milliseconds from the epoch (1970-01-01T00:00:00.000Z) in UTC.
+     * @param localOffset the local offset from UTC, measured in minutes;
+     *                    may be {@code null} to represent an unknown local offset.
      */
     public static Timestamp forMillis(long millis, Integer localOffset)
     {
         return new Timestamp(millis, localOffset);
     }
 
-
     /**
-     * Returns a Timestamp that represents the point in time that is
-     * {@code millis} milliseconds (including any fractional
-     * milliseconds) from the epoch, with a given local offset.
-     *
-     * <p>
-     * The resulting Timestamp will be precise to the second if {@code millis}
-     * doesn't contain information that is more granular than seconds.
-     * For example, a {@code BigDecimal} of
-     * value <tt>132541995e4 (132541995 &times; 10<sup>4</sup>)</tt>
-     * will return a Timestamp of {@code 2012-01-01T12:12:30Z},
-     * precise to the second.
-     *
-     * <p>
-     * The resulting Timestamp will be precise to the fractional second if
-     * {@code millis} contains information that is at least granular to
-     * milliseconds.
-     * For example, a {@code BigDecimal} of
-     * value <tt>1325419950555</tt>
-     * will return a Timestamp of {@code 2012-01-01T12:12:30.555Z},
-     * precise to the fractional second.
+     * The same as {@link #forMillis(long, Integer)} but the millisecond component is specified using a
+     * {@link BigDecimal} and therefore may include fractional milliseconds.
      *
      * @param millis
      *          number of milliseconds (including any fractional
@@ -1374,7 +1387,6 @@ public final class Timestamp
      *
      * @throws NullPointerException if {@code millis} is {@code null}
      *
-
      */
     public static Timestamp forMillis(BigDecimal millis, Integer localOffset)
     {
@@ -1382,6 +1394,41 @@ public final class Timestamp
     }
 
 
+    /**
+     * Returns a Timestamp that represents the point in time that is {@code seconds} from the unix epoch
+     * (1970-01-01T00:00:00.000Z), with the {@code nanoOffset} applied and a given local offset.
+     *
+     * <p>This function is intended to allow easy conversion to Timestamp from Java 8's {@code java.time.Instant}
+     * without having to port this library to Java 8.  The following snippet will yield a Timestamp {@code ts}
+     * that equivalent to the {@code java.time.Instant} {@code i}:</p>
+     *
+     * <code>
+     *     Instant i = Instant.now();
+     *     Timestamp ts = Timestamp.forEpochSecond(i.getEpochSecond(), i.getNano(), 0);
+     * </code>
+     *
+     * <p>Like {@link #forMillis}, {@code seconds} is relative to UTC, regardless of the value
+     * supplied for {@code localOffset}.</p>
+     *
+     * @param seconds The number of seconds from the unix epoch (1970-01-01T00:00:00.000Z) UTC.
+     * @param nanoOffset The number of nanoseconds for the fractional component. Must be between 0 and 999,999,999.
+     * @param localOffset the local offset from UTC, measured in minutes;
+     *                    may be {@code null} to represent an unknown local offset.
+     */
+    public static Timestamp forEpochSecond(long seconds, int nanoOffset, Integer localOffset) {
+        // We do not validate seconds here because that is done within the forMillis static constructor.
+        long millis = seconds * 1000L;
+        Timestamp ts = forMillis(millis, localOffset);
+        if(nanoOffset != 0) {
+            if(nanoOffset < 0 || nanoOffset > 999999999) {
+                throw new IllegalArgumentException("nanoOffset must be between 0 and 999,999,999");
+            }
+            ts._fraction = ts._fraction.add(BigDecimal.valueOf(nanoOffset).movePointLeft(9));
+        }
+        return ts;
+    }
+
+
     /**
      * Converts a {@link Calendar} to a Timestamp, preserving the calendar's
      * time zone as the equivalent local offset when it has at least minutes


Test patch:
diff --git a/test/com/amazon/ion/TimestampTest.java b/test/com/amazon/ion/TimestampTest.java
index eef97cb4..fb9129cf 100644
--- a/test/com/amazon/ion/TimestampTest.java
+++ b/test/com/amazon/ion/TimestampTest.java
@@ -42,7 +42,9 @@ import java.util.GregorianCalendar;
 import java.util.TimeZone;
 
 import org.junit.Ignore;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
 
 /**
  * Validates Ion date parsing, specified as per W3C but with requiring
@@ -53,6 +55,9 @@ import org.junit.Test;
 public class TimestampTest
     extends IonTestCase
 {
+    @Rule
+    public ExpectedException thrown = ExpectedException.none();
+
     /**
      * Earliest Ion timestamp possible, that is, "0001-01-01".
      */
@@ -761,9 +766,14 @@ public class TimestampTest
         ts = new Timestamp(2010, 2, 1, 10, 11, 12, BigDecimal.ZERO, 0);
         assertEquals(Timestamp.valueOf("2010-02-01T10:11:12Z"), ts);
         checkFields(2010, 2, 1, 10, 11, 12, null, 0, SECOND, ts);
+    }
 
-        // New static method unifies decimal seconds
+    @Test
+    public void testForSecond() {
+        BigDecimal fraction = new BigDecimal(".34");
         BigDecimal second = new BigDecimal("12.34");
+
+        Timestamp ts;
         ts = Timestamp.forSecond(2010, 2, 1, 10, 11, second, PST_OFFSET);
         checkFields(2010, 2, 1, 10, 11, 12, fraction, PST_OFFSET, FRACTION, ts);
         assertEquals("2010-02-01T10:11:12.34-08:00", ts.toString());
@@ -781,6 +791,111 @@ public class TimestampTest
         assertEquals("2010-02-01T18:11:12Z", ts.toZString());
     }
 
+    @Test
+    public void forEpochSecondTest() {
+        // Unix epoch
+        assertEquals(
+                Timestamp.valueOf("1970-01-01T00:00:00.000Z"),
+                Timestamp.forEpochSecond(0, 0, 0));
+
+        // One day before unix epoch
+        assertEquals(
+                Timestamp.valueOf("1969-12-31T00:00:00.000Z"),
+                Timestamp.forEpochSecond(-86400, 0, 0));
+
+        // One day after unix epoch
+        assertEquals(
+                Timestamp.valueOf("1970-01-02T00:00:00.000Z"),
+                Timestamp.forEpochSecond(86400, 0, 0));
+
+        // Maximum timestamp value (that can be created with forEpochSecond)
+        assertEquals(
+                Timestamp.valueOf("9999-12-31T23:59:59.999999999Z"),
+                Timestamp.forEpochSecond(Timestamp.MAXIMUM_TIMESTAMP_IN_EPOCH_SECONDS-1, 999999999, 0));
+
+        // Minimum timestamp value (that can be created with forEpochSecond)
+        assertEquals(
+                Timestamp.valueOf("0001-01-01T00:00:00.000Z"),
+                Timestamp.forEpochSecond(Timestamp.MINIMUM_TIMESTAMP_IN_EPOCH_SECONDS, 0, 0));
+
+        // Without fractional component
+        assertEquals(
+                Timestamp.valueOf("2009-01-20T20:17:00.000Z"),
+                Timestamp.forEpochSecond(1232482620, 0, 0));
+
+        // With fractional component (1/2 second)
+        assertEquals(
+                Timestamp.valueOf("2009-01-20T20:17:00.500000000Z"),
+                Timestamp.forEpochSecond(1232482620, 500000000, 0));
+
+        // With fractional component (one nanosecond)
+        assertEquals(
+                Timestamp.valueOf("2009-01-20T20:17:00.000000001Z"),
+                Timestamp.forEpochSecond(1232482620, 1, 0));
+
+        // With fractional component (999,999,999 nanoseconds)
+        assertEquals(
+                Timestamp.valueOf("2009-01-20T20:17:00.999999999Z"),
+                Timestamp.forEpochSecond(1232482620, 999999999, 0));
+
+        // With unknown local offset
+        assertEquals(
+                Timestamp.valueOf("1970-01-01T00:00:00.000-00:00"),
+                Timestamp.forEpochSecond(0, 0, null));
+
+        // With a local offset
+        assertEquals(
+                Timestamp.valueOf("1970-01-01T01:00:00.000+01:00"),
+                Timestamp.forEpochSecond(0, 0, 60));
+
+        // With negative local offset
+        assertEquals(
+                Timestamp.valueOf("1969-12-31T23:00:00.000-01:00"),
+                Timestamp.forEpochSecond(0, 0, -60));
+    }
+
+    @Test
+    public void forEpochSecondTestSecondsTooLow() {
+        thrown.expect(IllegalArgumentException.class);
+        // MINIMUM_TIMESTAMP_IN_EPOCH_SECONDS is inclusive
+        Timestamp.forEpochSecond(Timestamp.MINIMUM_TIMESTAMP_IN_EPOCH_SECONDS - 1, 0, 0);
+    }
+
+    @Test
+    public void forEpochSecondTestSecondsTooHigh() {
+        thrown.expect(IllegalArgumentException.class);
+        // MAXIMUM_TIMESTAMP_IN_EPOCH_SECONDS is exclusive
+        Timestamp.forEpochSecond(Timestamp.MAXIMUM_TIMESTAMP_IN_EPOCH_SECONDS, 0, 0);
+    }
+
+    @Test
+    public void forEpochSecondTestNanosTooLow() {
+        thrown.expect(IllegalArgumentException.class);
+        Timestamp.forEpochSecond(0, -1, 0);
+    }
+
+    @Test
+    public void forEpochSecondTestNanosTooHigh() {
+        thrown.expect(IllegalArgumentException.class);
+        Timestamp.forEpochSecond(0, 1000000000, 0);
+    }
+
+    @Test
+    @Ignore("https://github.com/amzn/ion-java/issues/303")
+    public void forEpochSecondTesOffsetTooLow() {
+        thrown.expect(IllegalArgumentException.class);
+        Timestamp.forEpochSecond(0, 0, -24 * 60);
+    }
+
+    @Test
+    @Ignore("https://github.com/amzn/ion-java/issues/303")
+    public void forEpochSecondTestOffsetTooHigh() {
+        thrown.expect(IllegalArgumentException.class);
+        Timestamp.forEpochSecond(0, 0, 24 * 60);
+    }
+
+
+
     /** Test for {@link Timestamp#Timestamp(BigDecimal, Integer)} */
     @Test
     public void testNewTimestampFromBigDecimal()