diff --git a/src/main/java/com/amazon/ion/Timestamp.java b/src/main/java/com/amazon/ion/Timestamp.java index 13057cf68..b41e73629 100644 --- a/src/main/java/com/amazon/ion/Timestamp.java +++ b/src/main/java/com/amazon/ion/Timestamp.java @@ -1,18 +1,5 @@ -/* - * Copyright 2007-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 package com.amazon.ion; import static com.amazon.ion.impl._Private_Utils.safeEquals; @@ -24,6 +11,8 @@ import java.io.IOException; import java.math.BigDecimal; import java.math.RoundingMode; +import java.time.LocalDateTime; +import java.time.ZoneOffset; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; @@ -90,7 +79,7 @@ public final class Timestamp /** * 0001-01-01T00:00:00.0Z in millis. */ - static final long MINIMUM_TIMESTAMP_IN_MILLIS = -62135769600000L; + static final long MINIMUM_TIMESTAMP_IN_MILLIS = -62135596800000L; /** * 0001-01-01T00:00:00.0Z in millis. @@ -321,13 +310,6 @@ private void apply_offset(int offset) } } - private static byte requireByte(int value, String location) { - if (value > Byte.MAX_VALUE || value < Byte.MIN_VALUE) { - throw new IllegalArgumentException(String.format("%s of %d is out of range.", location, value)); - } - return (byte) value; - } - private static short requireShort(int value, String location) { if (value > Short.MAX_VALUE || value < Short.MIN_VALUE) { throw new IllegalArgumentException(String.format("%s of %d is out of range.", location, value)); @@ -336,48 +318,33 @@ private static short requireShort(int value, String location) { } /** - * This method uses deprecated methods from {@link java.util.Date} - * instead of {@link Calendar} so that this code can be used (more easily) - * on the mobile Java platform (which has Date but does not have Calendar). + * Milliseconds from the epoch for the given UTC fields, in the proleptic Gregorian calendar. + * + * @see java.time.chrono.IsoChronology + */ + private static long utcMillisFromCivil(int year, int month, int day, int hour, int minute, int second) + { + return LocalDateTime.of(year, month, day, hour, minute, second).toEpochSecond(ZoneOffset.UTC) * 1000L; + } + + /** + * Sets this Timestamp's UTC fields from a millisecond offset from the epoch, in the proleptic + * Gregorian calendar. */ - @SuppressWarnings("deprecation") private void set_fields_from_millis(long millis) { if(millis < MINIMUM_TIMESTAMP_IN_MILLIS){ throw new IllegalArgumentException("year is less than 1"); } - Date date = new Date(millis); - - // The Date getters return values in the Date's time zone (i.e. the system time zone). - // The components need to be converted to UTC before being validated for exact ranges, because the offset - // conversion can affect which values are considered valid. Simply verify that the values can fit in the - // destination type here. If they do not, they would be out of range no matter the offset. - _minute = requireByte(date.getMinutes(), "Minute"); - _second = requireByte(date.getSeconds(), "Second"); - _hour = requireByte(date.getHours(), "Hour"); - _day = requireByte(date.getDate(), "Day"); - _month = requireByte(date.getMonth() + 1, "Month"); - - // Date does not correctly handle year values that represent year 0 or earlier in the system time zone through - // getYear(). This case is detected and forced to zero. - int offset = -date.getTimezoneOffset(); - if(offset < 0 && MINIMUM_TIMESTAMP_IN_MILLIS - offset > millis) { - _year = 0; - } else { - _year = requireShort(date.getYear() + 1900, "Year"); - } - - // Now apply the offset to convert the components to UTC. - apply_offset(offset); + LocalDateTime utc = LocalDateTime.ofEpochSecond(Math.floorDiv(millis, 1000L), 0, ZoneOffset.UTC); - // Now that all components are in UTC, they may be validated for exact ranges. - this._year = checkAndCastYear(_year); - this._month = checkAndCastMonth(_month); - this._day = checkAndCastDay(_day, _year, _month); - this._hour = checkAndCastHour(_hour); - this._minute = checkAndCastMinute(_minute); - this._second = checkAndCastSecond(_second); + this._year = checkAndCastYear(requireShort(utc.getYear(), "Year")); + this._month = checkAndCastMonth(utc.getMonthValue()); + this._day = checkAndCastDay(utc.getDayOfMonth(), _year, _month); + this._hour = checkAndCastHour(utc.getHour()); + this._minute = checkAndCastMinute(utc.getMinute()); + this._second = checkAndCastSecond(utc.getSecond()); } /** @@ -1565,6 +1532,9 @@ public static Timestamp nowZ() *
* Because {@link Date} instances are mutable, this method returns a * new instance from each call. + *
+ * Uses the proleptic Gregorian calendar, so before 1582-10-15 it differs from a default + * {@link GregorianCalendar}. * * @return a new {@code Date} instance, in UTC */ @@ -1581,6 +1551,9 @@ public Date dateValue() *
* Because {@link Calendar} instances are mutable, this method returns a * new instance from each call. + *
+ * The returned {@code Calendar} is proleptic Gregorian, so its fields agree with this + * Timestamp before 1582-10-15. * * @return a new {@code Calendar} instance, in its local time. * @@ -1588,7 +1561,8 @@ public Date dateValue() */ public Calendar calendarValue() { - Calendar cal = new GregorianCalendar(_Private_Utils.UTC); + GregorianCalendar cal = new GregorianCalendar(_Private_Utils.UTC); + cal.setGregorianChange(new Date(Long.MIN_VALUE)); long millis = getMillis(); Integer offset = _offset; @@ -1632,16 +1606,17 @@ public Calendar calendarValue() *
* This method will return the same result for all Timestamps representing * the same point in time, regardless of the local offset. + *
+ * Uses the proleptic Gregorian calendar, so before 1582-10-15 it differs from a default + * {@link GregorianCalendar}. * * @return * number of milliseconds (ignoring any fractional * milliseconds) from the epoch (1970-01-01T00:00:00.000Z) */ - @SuppressWarnings("deprecation") public long getMillis() { - // month is 0 based for Date - long millis = Date.UTC(this._year - 1900, this._month - 1, this._day, this._hour, this._minute, this._second); + long millis = utcMillisFromCivil(this._year, this._month, this._day, this._hour, this._minute, this._second); if (this._fraction != null) { BigDecimal fracAsDecimal = this._fraction.movePointRight(3); int frac = isIntegralZero(fracAsDecimal) ? 0 : fracAsDecimal.intValue(); @@ -1658,12 +1633,14 @@ public long getMillis() *
* This method will return the same result for all Timestamps representing * the same point in time, regardless of the local offset. + *
+ * Uses the proleptic Gregorian calendar, so before 1582-10-15 it differs from a default + * {@link GregorianCalendar}. * * @return * number of milliseconds (including any fractional * milliseconds) from the epoch (1970-01-01T00:00:00.000Z) */ - @SuppressWarnings("deprecation") public BigDecimal getDecimalMillis() { switch (this._precision) { @@ -1673,7 +1650,7 @@ public BigDecimal getDecimalMillis() case MINUTE: case SECOND: case FRACTION: - long millis = Date.UTC(this._year - 1900, this._month - 1, this._day, this._hour, this._minute, this._second); + long millis = utcMillisFromCivil(this._year, this._month, this._day, this._hour, this._minute, this._second); BigDecimal dec = BigDecimal.valueOf(millis); if (_fraction != null) { dec = dec.add(this._fraction.movePointRight(3)); diff --git a/src/test/java/com/amazon/ion/TimestampTest.java b/src/test/java/com/amazon/ion/TimestampTest.java index 819f980a1..c8519acdd 100644 --- a/src/test/java/com/amazon/ion/TimestampTest.java +++ b/src/test/java/com/amazon/ion/TimestampTest.java @@ -1,18 +1,5 @@ -/* - * Copyright 2007-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 package com.amazon.ion; import static com.amazon.ion.Decimal.NEGATIVE_ZERO; @@ -80,7 +67,10 @@ public class TimestampTest public static Calendar makeUtcCalendar() { - Calendar cal = Calendar.getInstance(UTC); + GregorianCalendar cal = new GregorianCalendar(UTC); + // Proleptic Gregorian, as Ion timestamps are; the default cutover would make this oracle + // disagree with the values under test before 1582-10-15. + cal.setGregorianChange(new Date(Long.MIN_VALUE)); cal.setTimeInMillis(0); // clear all fields, else they are "now" return cal; } @@ -362,13 +352,32 @@ public void modifyTimestamp(IonTimestamp value) * class as other test methods might be based off the correctness of * assumptions declared here. */ + @Test + public void testDatesAcrossTheGregorianCutoverAreDistinctInstants() + { + // Under the Julian cutover both of these mapped to the same epoch millisecond, so + // compareTo called them equal while equals did not. + Timestamp before = Timestamp.valueOf("1582-10-05T00:00:00Z"); + Timestamp after = Timestamp.valueOf("1582-10-15T00:00:00Z"); + assertEquals(10L * 24 * 60 * 60 * 1000, after.getMillis() - before.getMillis()); + assertTrue(before.compareTo(after) < 0); + assertEquals("1582-10-05T00:00:00.000Z", Timestamp.forMillis(before.getMillis(), 0).toString()); + } + @Test public void testTimestampConstants() { checkFields(1, 1, 1, 0, 0, 0, null, null, DAY, EARLIEST_ION_TIMESTAMP); assertEquals("0001-01-01", EARLIEST_ION_TIMESTAMP.toZString()); assertEquals("0001-01-01", EARLIEST_ION_TIMESTAMP.toString()); - assertEquals(-62135769600000L, EARLIEST_ION_TIMESTAMP.getMillis()); + // 0001-01-01T00:00:00Z, proleptic Gregorian, cross-checked against a Calendar with the + // cutover moved out of range. + GregorianCalendar earliest = new GregorianCalendar(UTC); + earliest.setGregorianChange(new Date(Long.MIN_VALUE)); + earliest.clear(); + earliest.set(1, Calendar.JANUARY, 1, 0, 0, 0); + assertEquals(-62135596800000L, earliest.getTimeInMillis()); + assertEquals(-62135596800000L, EARLIEST_ION_TIMESTAMP.getMillis()); checkFields(1970, 1, 1, 0, 0, 0, new BigDecimal("0.000"), 0, FRACTION, UNIX_EPOCH_TIMESTAMP); assertEquals("1970-01-01T00:00:00.000Z", UNIX_EPOCH_TIMESTAMP.toZString());