Skip to content

Commit

Permalink
Added error checking and throwing DateTimeParseException to include t…
Browse files Browse the repository at this point in the history
…he parse position where the error occurred (#22)

Co-authored-by: Morten Haraldsen <dev@ethlo.com>
  • Loading branch information
ethlo and Morten Haraldsen committed Jan 23, 2024
1 parent d2ef9e1 commit 10a8d67
Show file tree
Hide file tree
Showing 11 changed files with 169 additions and 58 deletions.
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ limitations under the License.
<groupId>com.ethlo.time</groupId>
<packaging>bundle</packaging>
<artifactId>itu</artifactId>
<version>1.7.8-SNAPSHOT</version>
<version>1.8.0-SNAPSHOT</version>
<name>Internet Time Utility</name>
<description>Very fast date-time parser and formatter - RFC 3339 (ISO 8601 profile) and W3C format
<description>Extremely fast date-time parser and formatter - RFC 3339 (ISO 8601 profile) and W3C format
</description>
<licenses>
<license>
Expand Down
6 changes: 3 additions & 3 deletions src/main/java/com/ethlo/time/DateTime.java
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}

/**
Expand Down Expand Up @@ -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");
}
}

Expand Down Expand Up @@ -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];
Expand Down
31 changes: 31 additions & 0 deletions src/main/java/com/ethlo/time/DateTimeFormatException.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
10 changes: 0 additions & 10 deletions src/main/java/com/ethlo/time/TimezoneOffset.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
* #L%
*/

import java.time.DateTimeException;
import java.time.ZoneOffset;
import java.util.Objects;

Expand All @@ -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;
}
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/ethlo/time/internal/AbstractRfc3339.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
* #L%
*/

import java.time.DateTimeException;
import com.ethlo.time.DateTimeFormatException;

public abstract class AbstractRfc3339 implements Rfc3339
{
Expand All @@ -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);
}
}
Expand Down
93 changes: 62 additions & 31 deletions src/main/java/com/ethlo/time/internal/EthloITU.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -132,32 +133,32 @@ 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);
}
}

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;
Expand All @@ -172,16 +173,16 @@ 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);
}
}

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;
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
}
}

Expand All @@ -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);
}

Expand All @@ -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);
}
Expand All @@ -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);
}
Expand All @@ -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;
Expand All @@ -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;
}
Expand All @@ -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;
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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
Expand Down
Loading

0 comments on commit 10a8d67

Please sign in to comment.