diff --git a/pom.xml b/pom.xml
index d320530..324a4af 100644
--- a/pom.xml
+++ b/pom.xml
@@ -23,9 +23,9 @@ limitations under the License.
com.ethlo.time
bundle
itu
- 1.7.8-SNAPSHOT
+ 1.8.0-SNAPSHOT
Internet Time Utility
- Very fast date-time parser and formatter - RFC 3339 (ISO 8601 profile) and W3C format
+ Extremely fast date-time parser and formatter - RFC 3339 (ISO 8601 profile) and W3C format
diff --git a/src/main/java/com/ethlo/time/DateTime.java b/src/main/java/com/ethlo/time/DateTime.java
index 25d3296..b16bb9e 100644
--- a/src/main/java/com/ethlo/time/DateTime.java
+++ b/src/main/java/com/ethlo/time/DateTime.java
@@ -280,7 +280,7 @@ public OffsetDateTime toOffsetDatetime()
{
return OffsetDateTime.of(year, month, day, hour, minute, second, nano, offset.toZoneOffset());
}
- throw new DateTimeException("No zone offset information found");
+ throw new DateTimeFormatException("No zone offset information found");
}
/**
@@ -308,7 +308,7 @@ private void assertMinGranularity(Field field)
{
if (!includesGranularity(field))
{
- throw new DateTimeException("No " + field.name() + " field found");
+ throw new DateTimeFormatException("No " + field.name() + " field found");
}
}
@@ -338,7 +338,7 @@ private String toString(final DateTime date, final Field lastIncluded, final int
{
if (lastIncluded.ordinal() > date.getMostGranularField().ordinal())
{
- throw new DateTimeException("Requested granularity was " + lastIncluded.name() + ", but contains only granularity " + date.getMostGranularField().name());
+ throw new DateTimeFormatException("Requested granularity was " + lastIncluded.name() + ", but contains only granularity " + date.getMostGranularField().name());
}
final TimezoneOffset tz = date.getOffset().orElse(null);
final char[] buffer = new char[35];
diff --git a/src/main/java/com/ethlo/time/DateTimeFormatException.java b/src/main/java/com/ethlo/time/DateTimeFormatException.java
new file mode 100644
index 0000000..8c290cc
--- /dev/null
+++ b/src/main/java/com/ethlo/time/DateTimeFormatException.java
@@ -0,0 +1,31 @@
+package com.ethlo.time;
+
+/*-
+ * #%L
+ * Internet Time Utility
+ * %%
+ * Copyright (C) 2017 - 2024 Morten Haraldsen (ethlo)
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License 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.
+ * #L%
+ */
+
+import java.time.DateTimeException;
+
+public class DateTimeFormatException extends DateTimeException
+{
+ public DateTimeFormatException(final String message)
+ {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/ethlo/time/TimezoneOffset.java b/src/main/java/com/ethlo/time/TimezoneOffset.java
index 679a042..5dab233 100644
--- a/src/main/java/com/ethlo/time/TimezoneOffset.java
+++ b/src/main/java/com/ethlo/time/TimezoneOffset.java
@@ -20,7 +20,6 @@
* #L%
*/
-import java.time.DateTimeException;
import java.time.ZoneOffset;
import java.util.Objects;
@@ -35,15 +34,6 @@ public class TimezoneOffset
private TimezoneOffset(final int hours, final int minutes)
{
- if (hours > 0 && minutes < 0)
- {
- throw new DateTimeException("Zone offset minutes must be positive because hours is positive");
- }
- else if (hours < 0 && minutes > 0)
- {
- throw new DateTimeException("Zone offset minutes must be negative because hours is negative");
- }
-
this.hours = hours;
this.minutes = minutes;
}
diff --git a/src/main/java/com/ethlo/time/internal/AbstractRfc3339.java b/src/main/java/com/ethlo/time/internal/AbstractRfc3339.java
index 2ad2e42..9b9c6c7 100644
--- a/src/main/java/com/ethlo/time/internal/AbstractRfc3339.java
+++ b/src/main/java/com/ethlo/time/internal/AbstractRfc3339.java
@@ -20,7 +20,7 @@
* #L%
*/
-import java.time.DateTimeException;
+import com.ethlo.time.DateTimeFormatException;
public abstract class AbstractRfc3339 implements Rfc3339
{
@@ -30,7 +30,7 @@ protected void assertMaxFractionDigits(int fractionDigits)
{
if (fractionDigits > MAX_FRACTION_DIGITS)
{
- throw new DateTimeException("Maximum supported number of fraction digits in second is "
+ throw new DateTimeFormatException("Maximum supported number of fraction digits in second is "
+ MAX_FRACTION_DIGITS + ", got " + fractionDigits);
}
}
diff --git a/src/main/java/com/ethlo/time/internal/EthloITU.java b/src/main/java/com/ethlo/time/internal/EthloITU.java
index e6ff52d..1983ca7 100644
--- a/src/main/java/com/ethlo/time/internal/EthloITU.java
+++ b/src/main/java/com/ethlo/time/internal/EthloITU.java
@@ -30,6 +30,7 @@
import java.time.OffsetDateTime;
import java.time.YearMonth;
import java.time.ZoneOffset;
+import java.time.format.DateTimeParseException;
import java.util.Arrays;
import com.ethlo.time.DateTime;
@@ -90,12 +91,12 @@ private static int writeTz(char[] buf, int start, TimezoneOffset tz)
}
}
- private static int scale(int fractions, int len)
+ private static int scale(int fractions, int len, String parsedData, final int index)
{
switch (len)
{
case 0:
- throw new DateTimeException("Must have at least 1 fraction digit");
+ throw new DateTimeParseException("Must have at least 1 fraction digit", parsedData, index);
case 1:
return fractions * 100_000_000;
case 2:
@@ -132,24 +133,24 @@ private static Object handleTime(String chars, int year, int month, int day, int
final TimezoneOffset zoneOffset = parseTimezone(chars, 16);
if (!raw)
{
- throw raiseMissingField(Field.SECOND);
+ throw raiseMissingField(Field.SECOND, chars, 16);
}
return DateTime.of(year, month, day, hour, minute, zoneOffset);
}
- throw new DateTimeException(chars);
+ throw new DateTimeParseException("Unexpected character at position 16: " + chars.charAt(16), chars, 16);
}
private static void assertPositionContains(String chars, int offset, char expected)
{
if (offset >= chars.length())
{
- raiseDateTimeException(chars, "Unexpected end of input");
+ raiseDateTimeException(chars, "Unexpected end of input", offset);
}
if (chars.charAt(offset) != expected)
{
- throw new DateTimeException("Expected character " + expected
- + " at position " + (offset + 1) + " '" + chars + "'");
+ throw new DateTimeParseException("Expected character " + expected
+ + " at position " + (offset + 1) + " '" + chars + "'", chars, offset);
}
}
@@ -157,7 +158,7 @@ private static void assertPositionContains(String chars, char... expected)
{
if (10 >= chars.length())
{
- raiseDateTimeException(chars, "Unexpected end of input");
+ raiseDateTimeException(chars, "Unexpected end of input", 10);
}
boolean found = false;
@@ -172,8 +173,8 @@ private static void assertPositionContains(String chars, char... expected)
}
if (!found)
{
- throw new DateTimeException("Expected character " + Arrays.toString(expected)
- + " at position " + (10 + 1) + " '" + chars + "'");
+ throw new DateTimeParseException("Expected character " + Arrays.toString(expected)
+ + " at position " + (10 + 1) + " '" + chars + "'", chars, 10);
}
}
@@ -181,7 +182,7 @@ private static TimezoneOffset parseTimezone(String chars, int offset)
{
if (offset >= chars.length())
{
- throw new DateTimeException("No timezone information: " + chars);
+ throw new DateTimeParseException("No timezone information: " + chars, chars, offset);
}
final int len = chars.length();
final int left = len - offset;
@@ -195,12 +196,12 @@ private static TimezoneOffset parseTimezone(String chars, int offset)
final char sign = chars.charAt(offset);
if (sign != PLUS && sign != MINUS)
{
- throw new DateTimeException("Invalid character starting at position " + offset + ": " + chars);
+ throw new DateTimeParseException("Invalid character starting at position " + offset + ": " + chars, chars, offset);
}
if (left != 6)
{
- throw new DateTimeException("Invalid timezone offset: " + new String(chars.toCharArray(), offset, left));
+ throw new DateTimeParseException("Invalid timezone offset: " + chars, chars, offset);
}
int hours = parsePositiveInt(chars, offset + 1, offset + 3);
@@ -213,7 +214,7 @@ private static TimezoneOffset parseTimezone(String chars, int offset)
if (sign == MINUS && hours == 0 && minutes == 0)
{
- throw new DateTimeException("Unknown 'Local Offset Convention' date-time not allowed");
+ throw new DateTimeParseException("Unknown 'Local Offset Convention' date-time not allowed", chars, offset);
}
return TimezoneOffset.ofHoursMinutes(hours, minutes);
@@ -223,7 +224,7 @@ private static void assertNoMoreChars(String chars, int lastUsed)
{
if (chars.length() > lastUsed + 1)
{
- throw new DateTimeException("Trailing junk data after position " + (lastUsed + 1) + ": " + chars);
+ throw new DateTimeParseException("Trailing junk data after position " + (lastUsed + 1) + ": " + chars, chars, lastUsed + 1);
}
}
@@ -242,6 +243,10 @@ private static Object parse(String chars, boolean raw)
final int years = parsePositiveInt(chars, 0, 4);
if (4 == len)
{
+ if (!raw)
+ {
+ throw raiseMissingField(Field.YEAR, chars, 2);
+ }
return DateTime.ofYear(years);
}
@@ -252,7 +257,7 @@ private static Object parse(String chars, boolean raw)
{
if (!raw)
{
- throw raiseMissingField(Field.MONTH);
+ throw raiseMissingField(Field.MONTH, chars, 5);
}
return DateTime.ofYearMonth(years, months);
}
@@ -264,7 +269,7 @@ private static Object parse(String chars, boolean raw)
{
if (!raw)
{
- throw raiseMissingField(Field.DAY);
+ throw raiseMissingField(Field.DAY, chars, 9);
}
return DateTime.ofDate(years, months, days);
}
@@ -287,36 +292,52 @@ private static Object parse(String chars, boolean raw)
{
return DateTime.of(years, months, days, hours, minutes, null);
}
- throw raiseMissingField(Field.SECOND);
+ throw raiseMissingField(Field.SECOND, chars, 16);
}
- private static DateTimeException raiseMissingField(Field field)
+ private static DateTimeException raiseMissingField(Field field, final String chars, final int offset)
{
- return new DateTimeException("No " + field.name() + " field found");
+ return new DateTimeParseException("No " + field.name() + " field found", chars, offset);
}
private static Object handleTime(int year, int month, int day, int hour, int minute, String chars, boolean raw)
{
// From here the specification is more lenient
final int len = chars.length();
- final int remaining = len - 19;
- if (remaining == 0)
+ final int remaining = len - 17;
+ if (remaining == 2)
{
final int seconds = parsePositiveInt(chars, 17, 19);
- leapSecondCheck(year, month, day, hour, minute, seconds, 0, null);
+ leapSecondCheck(year, month, day, hour, minute, 0, 0, null);
if (raw)
{
return new DateTime(Field.SECOND, year, month, day, hour, minute, seconds, 0, null, 0);
}
- throw new DateTimeException("No timezone information: " + chars);
+ throw new DateTimeParseException("No timezone information: " + chars, chars, 19);
+ }
+ else if (remaining == 0)
+ {
+ if (raw)
+ {
+ return new DateTime(Field.SECOND, year, month, day, hour, minute, 0, 0, null, 0);
+ }
+ throw new DateTimeParseException("No timezone information: " + chars, chars, 16);
}
TimezoneOffset offset = null;
int fractions = 0;
int fractionDigits = 0;
+ if (chars.length() < 20)
+ {
+ throw new DateTimeParseException("Unexpected end of input: " + chars, chars, 16);
+ }
char c = chars.charAt(19);
if (c == FRACTION_SEPARATOR)
{
+ if (chars.length() < 21)
+ {
+ throw new DateTimeParseException("Unexpected end of input: " + chars, chars, 20);
+ }
// We have fractional seconds
int result = 0;
int idx = 20;
@@ -328,11 +349,14 @@ private static Object handleTime(int year, int month, int day, int hour, int min
{
nonDigitFound = true;
fractionDigits = idx - 20;
- fractions = scale(-result, fractionDigits);
+ assertFractionDigits(chars, fractionDigits, idx);
+ fractions = scale(-result, fractionDigits, chars, idx);
offset = parseTimezone(chars, idx);
}
else
{
+ fractionDigits = idx - 19;
+ assertFractionDigits(chars, fractionDigits, idx);
result = (result << 1) + (result << 3);
result -= c - ZERO;
}
@@ -342,14 +366,14 @@ private static Object handleTime(int year, int month, int day, int hour, int min
if (!nonDigitFound)
{
fractionDigits = idx - 20;
- fractions = scale(-result, fractionDigits);
+ fractions = scale(-result, fractionDigits, chars, idx);
if (!raw)
{
offset = parseTimezone(chars, idx);
}
}
}
- else if (remaining == 1 && (c == ZULU_UPPER || c == ZULU_LOWER))
+ else if (c == ZULU_UPPER || c == ZULU_LOWER)
{
// Do nothing we are done
offset = TimezoneOffset.UTC;
@@ -361,11 +385,10 @@ else if (c == PLUS || c == MINUS)
}
else
{
- raiseDateTimeException(chars, "Unexpected character at position 19");
+ raiseDateTimeException(chars, "Unexpected character at position 19", 19);
}
final int second = parsePositiveInt(chars, 17, 19);
-
leapSecondCheck(year, month, day, hour, minute, second, fractions, offset);
if (!raw)
@@ -375,6 +398,14 @@ else if (c == PLUS || c == MINUS)
return fractionDigits > 0 ? DateTime.of(year, month, day, hour, minute, second, fractions, offset, fractionDigits) : DateTime.of(year, month, day, hour, minute, second, offset);
}
+ private static void assertFractionDigits(String chars, int fractionDigits, int idx)
+ {
+ if (fractionDigits > MAX_FRACTION_DIGITS)
+ {
+ throw new DateTimeParseException("Too many fraction digits: " + chars, chars, idx);
+ }
+ }
+
private static void leapSecondCheck(int year, int month, int day, int hour, int minute, int second, int nanos, TimezoneOffset offset)
{
if (second == LEAP_SECOND_SECONDS)
@@ -398,9 +429,9 @@ private static void leapSecondCheck(int year, int month, int day, int hour, int
}
}
- private static void raiseDateTimeException(String chars, String message)
+ private static void raiseDateTimeException(String chars, String message, int index)
{
- throw new DateTimeException(message + ": " + chars);
+ throw new DateTimeParseException(message + ": " + chars, chars, index);
}
@Override
diff --git a/src/main/java/com/ethlo/time/internal/LimitedCharArrayIntegerUtil.java b/src/main/java/com/ethlo/time/internal/LimitedCharArrayIntegerUtil.java
index f5e1bbe..fee1e37 100644
--- a/src/main/java/com/ethlo/time/internal/LimitedCharArrayIntegerUtil.java
+++ b/src/main/java/com/ethlo/time/internal/LimitedCharArrayIntegerUtil.java
@@ -21,6 +21,7 @@
*/
import java.time.DateTimeException;
+import java.time.format.DateTimeParseException;
import java.util.Arrays;
public final class LimitedCharArrayIntegerUtil
@@ -52,7 +53,7 @@ public static int parsePositiveInt(final String strNum, int startInclusive, int
{
if (endExclusive > strNum.length())
{
- throw new DateTimeException("Unexpected end of expression at position " + strNum.length() + ": '" + strNum + "'");
+ throw new DateTimeParseException("Unexpected end of expression at position " + strNum.length() + ": '" + strNum + "'", strNum, startInclusive);
}
int result = 0;
@@ -61,7 +62,7 @@ public static int parsePositiveInt(final String strNum, int startInclusive, int
final char c = strNum.charAt(i);
if (c < ZERO || c > DIGIT_9)
{
- throw new DateTimeException("Character " + c + " is not a digit");
+ throw new DateTimeParseException("Character " + c + " is not a digit", strNum, i);
}
result = (result << 1) + (result << 3);
result -= c - ZERO;
diff --git a/src/test/java/com/ethlo/time/CorrectnessTest.java b/src/test/java/com/ethlo/time/CorrectnessTest.java
index e49ffe1..4962a9c 100644
--- a/src/test/java/com/ethlo/time/CorrectnessTest.java
+++ b/src/test/java/com/ethlo/time/CorrectnessTest.java
@@ -204,7 +204,7 @@ void testFormat4()
void testParseMoreThanNanoResolutionFails()
{
final DateTimeException exception = assertThrows(DateTimeException.class, () -> parser.parseDateTime("2017-02-21T15:00:00.1234567891Z"));
- assertThat(exception.getMessage()).isEqualTo("Invalid value for NanoOfSecond (valid values 0 - 999999999): 1234567891");
+ assertThat(exception.getMessage()).isEqualTo("Too many fraction digits: 2017-02-21T15:00:00.1234567891Z");
}
@Test
diff --git a/src/test/java/com/ethlo/time/ErrorOffsetTest.java b/src/test/java/com/ethlo/time/ErrorOffsetTest.java
new file mode 100644
index 0000000..61fccfa
--- /dev/null
+++ b/src/test/java/com/ethlo/time/ErrorOffsetTest.java
@@ -0,0 +1,63 @@
+package com.ethlo.time;
+
+/*-
+ * #%L
+ * Internet Time Utility
+ * %%
+ * Copyright (C) 2017 - 2024 Morten Haraldsen (ethlo)
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License 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.
+ * #L%
+ */
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.time.OffsetDateTime;
+import java.time.format.DateTimeParseException;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+public class ErrorOffsetTest
+{
+ @ParameterizedTest
+ @ValueSource(strings = {"111",
+ "1111-",
+ "2012-2",
+ "2012-11-",
+ "2012-11-1",
+ "2012-11-11x",
+ "2012-11-11t12",
+ "2012-11-11t12:",
+ "2012-11-11t12:22",
+ "2012-11-11t12:22:",
+ "2012-11-11t12:22:1",
+ "2012-11-11t12:22:11",
+ "2012-11-11t12:22:11.",
+ "2012-11-11t12:22:11y",
+ "2012-11-11t12:22:11.1234567890",
+ "2012-11-11t12:22:11.1234567890+",
+ "2012-11-11t12:22:11.1234567890+8",
+ "2012-11-11t12:22:11.1234567890+08:",
+ "2012-11-11t12:22:11.1234567890+08:1",
+ "2012-11-11t12:22:11.1234567890+08:11x"})
+ public void testParseUseErrorPosition(String arg)
+ {
+ // Compare that parser error-position with the JDK parser
+ final DateTimeParseException exc = assertThrows(DateTimeParseException.class, () -> ITU.parseDateTime(arg));
+ final DateTimeParseException original = assertThrows(DateTimeParseException.class, () -> OffsetDateTime.parse(arg));
+ assertThat(exc.getErrorIndex()).isEqualTo(original.getErrorIndex());
+ }
+
+}
diff --git a/src/test/java/com/ethlo/time/ITUTest.java b/src/test/java/com/ethlo/time/ITUTest.java
index 4ec2628..a90dafb 100644
--- a/src/test/java/com/ethlo/time/ITUTest.java
+++ b/src/test/java/com/ethlo/time/ITUTest.java
@@ -36,10 +36,12 @@
import java.time.Year;
import java.time.YearMonth;
import java.time.ZoneOffset;
+import java.time.format.DateTimeParseException;
import java.time.temporal.Temporal;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
@Tag("CorrectnessTest")
public class ITUTest
diff --git a/src/test/java/com/ethlo/time/W3cCorrectnessTest.java b/src/test/java/com/ethlo/time/W3cCorrectnessTest.java
index 0704350..62638f7 100644
--- a/src/test/java/com/ethlo/time/W3cCorrectnessTest.java
+++ b/src/test/java/com/ethlo/time/W3cCorrectnessTest.java
@@ -150,13 +150,6 @@ public void testTimezoneOffset()
assertThat(tz.getMinutes()).isEqualTo(-30);
}
- @Test
- public void testMismatch()
- {
- assertThrows(DateTimeException.class, () -> TimezoneOffset.ofHoursMinutes(-17, 30));
- assertThrows(DateTimeException.class, () -> TimezoneOffset.ofHoursMinutes(17, -30));
- }
-
@Test
public void testOfZoneOffset()
{