diff --git a/src/main/java/com/ethlo/time/AbstractInternetDateTimeUtil.java b/src/main/java/com/ethlo/time/AbstractInternetDateTimeUtil.java new file mode 100644 index 0000000..b6c648d --- /dev/null +++ b/src/main/java/com/ethlo/time/AbstractInternetDateTimeUtil.java @@ -0,0 +1,43 @@ +package com.ethlo.time; + +import java.time.DateTimeException; + +public abstract class AbstractInternetDateTimeUtil implements InternetDateTimeUtil +{ + private final static int MAX_FRACTION_DIGITS = 9; + + private final boolean unknownLocalOffsetConvention; + + public AbstractInternetDateTimeUtil(boolean unknownLocalOffsetConvention) + { + this.unknownLocalOffsetConvention = unknownLocalOffsetConvention; + } + + /** + * RFC 3339 - 4.3. Unknown Local Offset Convention + * + * If the time in UTC is known, but the offset to local time is unknown, + * this can be represented with an offset of "-00:00". This differs + * semantically from an offset of "Z" or "+00:00", which imply that UTC + * is the preferred reference point for the specified time. + * + * @return True if allowed, otherwise false + */ + public boolean allowUnknownLocalOffsetConvention() + { + return unknownLocalOffsetConvention; + } + + protected void failUnknownLocalOffsetConvention() + { + throw new DateTimeException("Unknown Local Offset Convention date-times not allowed. See #allowUnknownLocalOffsetConvention()"); + } + + protected void assertMaxFractionDigits(int fractionDigits) + { + if (fractionDigits > MAX_FRACTION_DIGITS ) + { + throw new DateTimeException("Maximum support number of fraction dicits in second is " + MAX_FRACTION_DIGITS + ", got " + fractionDigits); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/ethlo/time/FastInternetDateTimeUtil.java b/src/main/java/com/ethlo/time/FastInternetDateTimeUtil.java index 131ee3b..f5925b5 100644 --- a/src/main/java/com/ethlo/time/FastInternetDateTimeUtil.java +++ b/src/main/java/com/ethlo/time/FastInternetDateTimeUtil.java @@ -10,8 +10,18 @@ import com.ethlo.util.CharArrayIntegerUtil; import com.ethlo.util.CharArrayUtil; -public class FastInternetDateTimeUtil implements InternetDateTimeUtil +/** + * Extreme level of optimization to squeeze every CPU cycle. + * + * @author ethlo - Morten Haraldsen + */ +public class FastInternetDateTimeUtil extends AbstractInternetDateTimeUtil { + public FastInternetDateTimeUtil() + { + super(false); + } + private final StdJdkInternetDateTimeUtil delegate = new StdJdkInternetDateTimeUtil(); private static final char dateSep = '-'; @@ -95,7 +105,7 @@ else if (chars[19] == '+' || chars[19] == '-') } else { - throw new UnsupportedOperationException(); + throw new DateTimeException("Unexpected character at offset 19:" + chars[19]); } return OffsetDateTime.of(year, month, day, hour, minute, second, fractions, offset); @@ -123,7 +133,27 @@ private ZoneOffset parseTz(char[] chars, int offset) throw new DateTimeException("Invalid offset: " + new String(chars, offset, left)); } - return ZoneOffset.of(new String(chars, offset, left)); + final char sign = chars[offset]; + int hours = CharArrayIntegerUtil.parsePositiveInt(chars, 10, offset + 1, offset + 3); + int minutes = CharArrayIntegerUtil.parsePositiveInt(chars, 10, offset + 4, offset + 4 + 2); + if (sign == '-') + { + hours = -hours; + } + else if (sign != '+') + { + throw new DateTimeException("Invalid character starting at position " + offset); + } + + if (! allowUnknownLocalOffsetConvention()) + { + if (sign == '-' && hours == 0 && minutes == 0) + { + super.failUnknownLocalOffsetConvention(); + } + } + + return ZoneOffset.ofHoursMinutes(hours, minutes); } private void assertNoMoreChars(char[] chars, int lastUsed) @@ -137,6 +167,7 @@ private void assertNoMoreChars(char[] chars, int lastUsed) @Override public String formatUtc(OffsetDateTime date, int fractionDigits) { + assertMaxFractionDigits(fractionDigits); final LocalDateTime utc = LocalDateTime.ofInstant(date.toInstant(), ZoneOffset.UTC); final char[] buf = new char[64]; @@ -175,16 +206,6 @@ public String formatUtc(OffsetDateTime date, int fractionDigits) private void addFractions(char[] buf, int fractionDigits, int nano) { - if (fractionDigits < 0) - { - fractionDigits = 0; - } - - if (fractionDigits > 9) - { - fractionDigits = 9; - } - final double d = widths[fractionDigits - 1]; CharArrayIntegerUtil.toString((int)(nano / d), buf, 20, fractionDigits); } @@ -198,7 +219,7 @@ public String format(OffsetDateTime date, String timezone) @Override public String formatUtc(Date date) { - return formatUtc(OffsetDateTime.ofInstant(date.toInstant(), ZoneOffset.UTC)); + return formatUtc(OffsetDateTime.ofInstant(date.toInstant(), ZoneOffset.UTC), 3); } @Override @@ -244,4 +265,16 @@ public String formatUtc(OffsetDateTime date) { return formatUtc(date, 0); } + + @Override + public String formatUtcMilli(Date date) + { + return formatUtcMilli(OffsetDateTime.ofInstant(date.toInstant(), ZoneOffset.UTC)); + } + + @Override + public String format(Date date, String timezone, int fractionDigits) + { + return delegate.format(date, timezone, fractionDigits); + } } \ No newline at end of file diff --git a/src/main/java/com/ethlo/time/InternetDateTimeUtil.java b/src/main/java/com/ethlo/time/InternetDateTimeUtil.java index 503a086..f56b3e5 100644 --- a/src/main/java/com/ethlo/time/InternetDateTimeUtil.java +++ b/src/main/java/com/ethlo/time/InternetDateTimeUtil.java @@ -39,6 +39,13 @@ public interface InternetDateTimeUtil * @return The formatted string */ String formatUtc(Date date); + + /** + * See {@link #formatUtcMilli(OffsetDateTime)} + * @param date The date to format + * @return The formatted string + */ + String formatUtcMilli(Date date); /** * See {@link #format(OffsetDateTime, String)} @@ -48,6 +55,15 @@ public interface InternetDateTimeUtil */ String format(Date date, String timezone); + /** + * Format the date as a date-time String with specified resolution and time-zone offset, for example 1999-12-31T16:48:36[.123456789]-05:00 + * @param date The date to format + * @param timezone The time-zone + * @param fractionDigits The number of fraction digits + * @return the formatted string + */ + String format(Date date, String timezone, int fractionDigits); + /** * Check whether the string is a valid date-time according to RFC-3339 * @param dateTime @@ -55,11 +71,33 @@ public interface InternetDateTimeUtil */ boolean isValid(String dateTime); + /** + * Format the date as a date-time String with millisecond resolution, for example 1999-12-31T16:48:36.123Z + * @param date The date to format + * @return the formatted string + */ String formatUtcMilli(OffsetDateTime date); + /** + * Format the date as a date-time String with microsecond resolution, aka 1999-12-31T16:48:36.123456Z + * @param date The date to format + * @return the formatted string + */ String formatUtcMicro(OffsetDateTime date); + /** + * Format the date as a date-time String with nanosecond resolution, aka 1999-12-31T16:48:36.123456789Z + * @param date The date to format + * @return the formatted string + */ String formatUtcNano(OffsetDateTime date); + /** + * Format the date as a date-time String with specified resolution, aka 1999-12-31T16:48:36[.123456789]Z + * @param date The date to format + * @return the formatted string + */ String formatUtc(OffsetDateTime date, int fractionDigits); + + boolean allowUnknownLocalOffsetConvention(); } \ No newline at end of file diff --git a/src/main/java/com/ethlo/time/StdJdkInternetDateTimeUtil.java b/src/main/java/com/ethlo/time/StdJdkInternetDateTimeUtil.java index 1dcd803..4b80006 100644 --- a/src/main/java/com/ethlo/time/StdJdkInternetDateTimeUtil.java +++ b/src/main/java/com/ethlo/time/StdJdkInternetDateTimeUtil.java @@ -13,14 +13,13 @@ import java.util.TimeZone; /** - * The recommendation for date-time exchange in modern APIs is to use RFC-3339, available at https://tools.ietf.org/html/rfc3339 - * This class supports both validation, parsing and formatting of such date-times. + * Java 8 JDK classes. The safe and normally "efficient enough" choice. * - * @author Ethlo, Morten Haraldsen + * @author ethlo - Morten Haraldsen */ -public class StdJdkInternetDateTimeUtil implements InternetDateTimeUtil +public class StdJdkInternetDateTimeUtil extends AbstractInternetDateTimeUtil { - private SimpleDateFormat format; + private SimpleDateFormat[] formats = new SimpleDateFormat[9]; private DateTimeFormatter rfc3339baseFormatter = new DateTimeFormatterBuilder() .appendValue(ChronoField.YEAR, 4) @@ -78,18 +77,19 @@ private DateTimeFormatter getFormatter(int fractionDigits) .optionalStart() .appendOffset("+HH:MM", "Z") .optionalEnd() + .optionalStart() + .appendOffset("+HH:MM", "z") + .optionalEnd() + .toFormatter(); public StdJdkInternetDateTimeUtil() { - this.format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss." + repeat('S', 3) + "XX"); - } - - @Override - public String format(Date date, String timezone) - { - format.setTimeZone(TimeZone.getTimeZone(timezone)); - return format.format(date); + super(false); + for (int i = 1; i < 9; i++) + { + this.formats[i] = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss." + repeat('S', i) + "XXX"); + } } @Override @@ -163,6 +163,27 @@ public String formatUtcNano(OffsetDateTime date) @Override public String formatUtc(OffsetDateTime date, int fractionDigits) { + assertMaxFractionDigits(fractionDigits); return getFormatter(fractionDigits).format(date); } + + @Override + public String formatUtcMilli(Date date) + { + return formatUtcMilli(OffsetDateTime.ofInstant(date.toInstant(), ZoneOffset.UTC)); + } + + @Override + public String format(Date date, String timezone) + { + return format(date, timezone, 3); + } + + @Override + public String format(Date date, String timezone, int fractionDigits) + { + final SimpleDateFormat formatter = (SimpleDateFormat)formats[fractionDigits].clone(); + formatter.setTimeZone(TimeZone.getTimeZone(timezone)); + return formatter.format(date); + } } \ No newline at end of file diff --git a/src/test/java/com/ethlo/time/CorrectnessTest.java b/src/test/java/com/ethlo/time/CorrectnessTest.java index a1d249a..452499c 100644 --- a/src/test/java/com/ethlo/time/CorrectnessTest.java +++ b/src/test/java/com/ethlo/time/CorrectnessTest.java @@ -19,7 +19,7 @@ public abstract class CorrectnessTest extends AbstractTest "2017-02-21T15:27:39.123456+00:00", "2017-02-21T15:27:39.123456789+00:00", "2017-02-21T15:27:39.1+00:00", "2017-02-21T15:27:39.12+00:00", "2017-02-21T15:27:39.123+00:00", "2017-02-21T15:27:39.1234+00:00", - "2017-02-21T15:27:39.112345+00:00", "2017-02-21T15:27:39.123456+00:00", + "2017-02-21T15:27:39.12345+00:00", "2017-02-21T15:27:39.123456+00:00", "2017-02-21T15:27:39.1234567+00:00", "2017-02-21T15:27:39.12345678+00:00" }; @@ -62,6 +62,20 @@ public void testFormat4() assertThat(instance.format(date, "EST")).isEqualTo("2017-02-21T10:00:00.123-05:00"); } + @Test(expected=DateTimeException.class) + public void testParseMoreThanNanoResolutionFails() + { + instance.parse("2017-02-21T15:00:00.1234567891Z"); + } + + @Test(expected=DateTimeException.class) + public void testFormatMoreThanNanoResolutionFails() + { + final OffsetDateTime d = instance.parse("2017-02-21T15:00:00.123456789Z"); + final int fractionDigits = 10; + instance.formatUtc(d, fractionDigits); + } + @Test public void testFormatUtc() { @@ -80,6 +94,14 @@ public void testFormatUtcMilli() assertThat(instance.formatUtcMilli(date)).isEqualTo("2017-02-21T15:00:00.123Z"); } + @Test + public void testFormatUtcMilliWithDate() + { + final String s = "2017-02-21T15:00:00.123456789Z"; + final OffsetDateTime date = instance.parse(s); + assertThat(instance.formatUtcMilli(new Date(date.toInstant().toEpochMilli()))).isEqualTo("2017-02-21T15:00:00.123Z"); + } + @Test public void testFormatUtcMicro() { @@ -166,12 +188,37 @@ public void testMilitaryOffset() final String s = "2017-02-21T15:27:39+0000"; assertThat(instance.isValid(s)).isFalse(); } + + @Test(expected=DateTimeException.class) + public void testParseUnknownLocalOffsetConvention() + { + final String s = "2017-02-21T15:27:39-00:00"; + instance.parse(s); + } + + @Test + public void testParseLowercaseZ() + { + final String s = "2017-02-21T15:27:39.000z"; + instance.parse(s); + } + + @Test + public void testFormatWithNamedTimeZoneDate() + { + final String s = "2017-02-21T15:27:39.321+00:00"; + final OffsetDateTime d = instance.parse(s); + final String formatted = instance.format(new Date(d.toInstant().toEpochMilli()), "EST"); + assertThat(formatted).isEqualTo("2017-02-21T10:27:39.321-05:00"); + } @Test public void testFormatWithNamedTimeZone() { - // TODO: Add assertions - instance.format(new Date(), "EST"); + final String s = "2017-02-21T15:27:39.321+00:00"; + final OffsetDateTime d = instance.parse(s); + final String formatted = instance.format(new Date(d.toInstant().toEpochMilli()), "EST", 3); + assertThat(formatted).isEqualTo("2017-02-21T10:27:39.321-05:00"); } @Override diff --git a/src/test/java/com/ethlo/time/StdJdkBenchmarkTest.java b/src/test/java/com/ethlo/time/StdJdkBenchmarkTest.java index ebacdd3..4b9e631 100644 --- a/src/test/java/com/ethlo/time/StdJdkBenchmarkTest.java +++ b/src/test/java/com/ethlo/time/StdJdkBenchmarkTest.java @@ -3,7 +3,7 @@ public class StdJdkBenchmarkTest extends BenchmarkTest { @Override - protected StdJdkInternetDateTimeUtil getInstance() + protected InternetDateTimeUtil getInstance() { return new StdJdkInternetDateTimeUtil(); } diff --git a/src/test/java/com/ethlo/time/StdJdkCorrectnessTest.java b/src/test/java/com/ethlo/time/StdJdkCorrectnessTest.java index 1b7a9e0..75c552f 100644 --- a/src/test/java/com/ethlo/time/StdJdkCorrectnessTest.java +++ b/src/test/java/com/ethlo/time/StdJdkCorrectnessTest.java @@ -1,12 +1,14 @@ package com.ethlo.time; +import java.time.DateTimeException; + import org.junit.Ignore; import org.junit.Test; public class StdJdkCorrectnessTest extends CorrectnessTest { @Override - protected StdJdkInternetDateTimeUtil getInstance() + protected InternetDateTimeUtil getInstance() { return new StdJdkInternetDateTimeUtil(); } @@ -18,4 +20,12 @@ public void testFormat4TrailingNoise() { } + + @Override + @Test(expected=DateTimeException.class) + @Ignore + public void testParseUnknownLocalOffsetConvention() + { + + } }