Skip to content

Commit

Permalink
Fixed formatting performance. Null or empty input is no longer accepe…
Browse files Browse the repository at this point in the history
…table to be in line with the java.time classes
  • Loading branch information
ethlo committed Feb 27, 2022
1 parent 650e270 commit f102fbb
Show file tree
Hide file tree
Showing 11 changed files with 209 additions and 132 deletions.
19 changes: 12 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,23 @@ W3C [Date and Time Formats](https://www.w3.org/TR/NOTE-datetime) in Java.

<img src="doc/performance.jpg" alt="Performance plot">

| Implementation | Parse | Format | Round-trip |
-------------------|--------:|----------:|-----:|
| Google DateTime | 1,020 ns | Not supported | N/A
| JDK Java Time | 1,558 ns | 426 ns | 1,984 ns |
| Ethlo ITU | 88 ns |166 ns |254 ns|
| Implementation | Parse | Format |
-------------------|--------:|----------:|
| Google DateTime | 885,089 | Not supported
| JDK Java Time | 739,180 | 2,406,040
| Ethlo ITU | 12,437,143 | 12,602,170


Values in nano-seconds. Lower is better.
Numbers are operations per second (higher is better).

Your mileage may vary. The tests are easy to run and are included in the repository.

## Example use
Tests performed on a Dell XPS 9700
* Intel(R) Core(TM) i7-10750H CPU @ 2.60GHz
* Ubuntu 21.10
* OpenJDK version 11.0.13

## Example usage

```java
// Parse a string
Expand Down
Binary file modified doc/performance.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
144 changes: 104 additions & 40 deletions src/main/java/com/ethlo/time/EthloITU.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,21 @@
* #L%
*/

import static com.ethlo.time.LimitedCharArrayIntegerUtil.indexOfNonDigit;
import static com.ethlo.time.LimitedCharArrayIntegerUtil.parsePositiveInt;

import java.time.DateTimeException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Month;
import java.time.OffsetDateTime;
import java.time.Year;
import java.time.YearMonth;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.temporal.Temporal;
import java.util.Arrays;
import java.util.Date;
import java.util.Objects;

public class EthloITU extends AbstractRfc3339 implements W3cDateTimeUtil
{
Expand All @@ -49,18 +53,81 @@ public class EthloITU extends AbstractRfc3339 implements W3cDateTimeUtil
private final Java8Rfc3339 delegate = new Java8Rfc3339();

@Override
public OffsetDateTime parseDateTime(String s)
public OffsetDateTime parseDateTime(String text)
{
Objects.requireNonNull(text, "text");
final char[] chars = text.toCharArray();

final int year = getYear(chars);

assertPositionContains(chars, 4, DATE_SEPARATOR);
final int month = getMonth(chars);

assertPositionContains(chars, 7, DATE_SEPARATOR);
final int day = getDay(chars);

// *** Time starts ***//

// HOURS
assertPositionContains(chars, 10, SEPARATOR_UPPER, SEPARATOR_LOWER, SEPARATOR_SPACE);
final int hour = getHour(chars);

// MINUTES
assertPositionContains(chars, 13, TIME_SEPARATOR);
final int minute = getMinute(chars);

// SECONDS or TIMEZONE
return handleTime(chars, year, month, day, hour, minute);
}

private int getHour(final char[] chars)
{
final Temporal t = doParseLenient(s, OffsetDateTime.class);
if (t == null)
return parsePositiveInt(chars, 11, 13);
}

private int getMinute(final char[] chars)
{
return parsePositiveInt(chars, 14, 16);
}

private int getDay(final char[] chars)
{
return parsePositiveInt(chars, 8, 10);
}

private OffsetDateTime handleTime(char[] chars, int year, int month, int day, int hour, int minute)
{
switch (chars[16])
{
return null;
// We have more granularity, keep going
case TIME_SEPARATOR:
return seconds(year, month, day, hour, minute, chars);

case PLUS:
case MINUS:
case ZULU_UPPER:
case ZULU_LOWER:
final ZoneOffset zoneOffset = parseTz(chars, 16);
return OffsetDateTime.of(year, month, day, hour, minute, 0, 0, zoneOffset);

default:
assertPositionContains(chars, 16, TIME_SEPARATOR, PLUS, MINUS, ZULU_UPPER);
}
else if (t instanceof OffsetDateTime)
throw new DateTimeException(new String(chars));
}

private void assertPositionContains(char[] chars, int offset, char expected)
{
if (offset >= chars.length)
{
return (OffsetDateTime) t;
throw new DateTimeException("Abrupt end of input: " + new String(chars));
}

if (chars[offset] != expected)
{
throw new DateTimeException("Expected character " + expected
+ " at position " + (offset + 1) + " '" + new String(chars) + "'");
}
throw new DateTimeException("Invalid RFC-3339 date-time: " + s);
}

private void assertPositionContains(char[] chars, int offset, char... expected)
Expand Down Expand Up @@ -101,8 +168,8 @@ private ZoneOffset parseTz(char[] chars, int offset)
}

final char sign = chars[offset];
int hours = LimitedCharArrayIntegerUtil.parsePositiveInt(chars, offset + 1, offset + 3);
int minutes = LimitedCharArrayIntegerUtil.parsePositiveInt(chars, offset + 4, offset + 4 + 2);
int hours = parsePositiveInt(chars, offset + 1, offset + 3);
int minutes = parsePositiveInt(chars, offset + 4, offset + 4 + 2);
if (sign == MINUS)
{
hours = -hours;
Expand Down Expand Up @@ -144,25 +211,25 @@ public String format(OffsetDateTime date, Field lastIncluded)
public String formatUtc(OffsetDateTime date, Field lastIncluded, int fractionDigits)
{
assertMaxFractionDigits(fractionDigits);
final LocalDateTime utc = LocalDateTime.ofInstant(date.toInstant(), ZoneOffset.UTC);
final ZonedDateTime utc = date.atZoneSameInstant(ZoneOffset.UTC);

final char[] buffer = new char[31];

if (handleDatePart(lastIncluded, buffer, utc.getYear(), 0, 4, Field.YEAR))
{
return finish(buffer, 4);
return finish(buffer, Field.YEAR.getRequiredLength());
}

buffer[4] = DATE_SEPARATOR;
if (handleDatePart(lastIncluded, buffer, utc.getMonthValue(), 5, 2, Field.MONTH))
{
return finish(buffer, 7);
return finish(buffer, Field.MONTH.getRequiredLength());
}

buffer[7] = DATE_SEPARATOR;
if (handleDatePart(lastIncluded, buffer, utc.getDayOfMonth(), 8, 2, Field.DAY))
{
return finish(buffer, 10);
return finish(buffer, Field.DAY.getRequiredLength());
}

// T separator
Expand All @@ -173,7 +240,7 @@ public String formatUtc(OffsetDateTime date, Field lastIncluded, int fractionDig
buffer[13] = TIME_SEPARATOR;
if (handleDatePart(lastIncluded, buffer, utc.getMinute(), 14, 2, Field.MINUTE))
{
return finish(buffer, 16);
return finish(buffer, Field.MINUTE.getRequiredLength());
}
buffer[16] = TIME_SEPARATOR;
LimitedCharArrayIntegerUtil.toString(utc.getSecond(), buffer, 17, 2);
Expand Down Expand Up @@ -301,65 +368,57 @@ public <T extends Temporal> Temporal doParseLenient(String s, Class<T> type)
// Date portion

// YEAR
final int year = LimitedCharArrayIntegerUtil.parsePositiveInt(chars, 0, 4);
final int year = getYear(chars);
if (maxRequired == Field.YEAR || chars.length == 4)
{
return Year.of(year);
}

// MONTH
assertPositionContains(chars, 4, DATE_SEPARATOR);
final int month = LimitedCharArrayIntegerUtil.parsePositiveInt(chars, 5, 7);
final int month = getMonth(chars);
if (maxRequired == Field.MONTH || chars.length == 7)
{
return YearMonth.of(year, month);
}

// DAY
assertPositionContains(chars, 7, DATE_SEPARATOR);
final int day = LimitedCharArrayIntegerUtil.parsePositiveInt(chars, 8, 10);
final int day = getDay(chars);
if (maxRequired == Field.DAY || chars.length == 10)
{
return LocalDate.of(year, month, day);
}

// *** Time starts ***//

// HOURS
assertPositionContains(chars, 10, SEPARATOR_UPPER, SEPARATOR_LOWER, SEPARATOR_SPACE);
final int hour = LimitedCharArrayIntegerUtil.parsePositiveInt(chars, 11, 13);
final int hour = getHour(chars);

// MINUTES
assertPositionContains(chars, 13, TIME_SEPARATOR);
final int minute = LimitedCharArrayIntegerUtil.parsePositiveInt(chars, 14, 16);
final int minute = getMinute(chars);
if (maxRequired == Field.MINUTE || chars.length == 16)
{
return LocalDate.of(year, month, day);
}

// SECONDS or TIMEZONE
switch (chars[16])
{
// We have more granularity, keep going
case TIME_SEPARATOR:
return seconds(year, month, day, hour, minute, chars);
return handleTime(chars, year, month, day, hour, minute);
}

case PLUS:
case MINUS:
case ZULU_UPPER:
case ZULU_LOWER:
final ZoneOffset zoneOffset = parseTz(chars, 16);
return OffsetDateTime.of(year, month, day, hour, minute, 0, 0, zoneOffset);
private int getMonth(final char[] chars)
{
return parsePositiveInt(chars, 5, 7);
}

default:
assertPositionContains(chars, 16, TIME_SEPARATOR, PLUS, MINUS, ZULU_UPPER);
}
throw new DateTimeException(new String(chars));
private int getYear(final char[] chars)
{
return parsePositiveInt(chars, 0, 4);
}

private OffsetDateTime seconds(int year, int month, int day, int hour, int minute, char[] chars)
{
final int second = LimitedCharArrayIntegerUtil.parsePositiveInt(chars, 17, 19);
final int second = getSecond(chars);

// From here the specification is more lenient
final int remaining = chars.length - 19;
Expand All @@ -376,7 +435,7 @@ private OffsetDateTime seconds(int year, int month, int day, int hour, int minut
else if (remaining >= 1 && chars[19] == FRACTION_SEPARATOR)
{
// We have fractional seconds
final int idx = LimitedCharArrayIntegerUtil.indexOfNonDigit(chars, 20);
final int idx = indexOfNonDigit(chars, 20);
if (idx != -1)
{
// We have an end of fractions
Expand Down Expand Up @@ -419,10 +478,15 @@ else if (remaining == 0)
return OffsetDateTime.of(year, month, day, hour, minute, second, fractions, offset);
}

private int getSecond(final char[] chars)
{
return parsePositiveInt(chars, 17, 19);
}

private int getFractions(final char[] chars, final int idx, final int len)
{
final int fractions;
fractions = LimitedCharArrayIntegerUtil.parsePositiveInt(chars, 20, idx);
fractions = parsePositiveInt(chars, 20, idx);
switch (len)
{
case 1:
Expand Down
14 changes: 13 additions & 1 deletion src/main/java/com/ethlo/time/Field.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,14 @@
public enum Field
{
// 2000-12-31T16:11:34.123456
YEAR, MONTH, DAY, MINUTE, SECOND;
YEAR(4), MONTH(7), DAY(10), MINUTE(16), SECOND(19);

private final int requiredLength;

Field(int requiredLength)
{
this.requiredLength = requiredLength;
}

public static Field valueOf(Class<? extends Temporal> type)
{
Expand All @@ -52,4 +59,9 @@ else if (OffsetDateTime.class.equals(type))

throw new IllegalArgumentException("Type " + type.getSimpleName() + " is not supported");
}

public int getRequiredLength()
{
return requiredLength;
}
}
19 changes: 7 additions & 12 deletions src/main/java/com/ethlo/time/Java8Rfc3339.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@
*/
public class Java8Rfc3339 extends AbstractRfc3339
{
private SimpleDateFormat[] formats = new SimpleDateFormat[MAX_FRACTION_DIGITS];
private final SimpleDateFormat[] formats = new SimpleDateFormat[MAX_FRACTION_DIGITS];

private DateTimeFormatter rfc3339baseFormatter = new DateTimeFormatterBuilder()
private final DateTimeFormatter rfc3339baseFormatter = new DateTimeFormatterBuilder()
.appendValue(ChronoField.YEAR, 4)
.appendLiteral('-')
.appendValue(ChronoField.MONTH_OF_YEAR, 2)
Expand All @@ -53,7 +53,8 @@ public class Java8Rfc3339 extends AbstractRfc3339
.appendLiteral(':')
.appendValue(ChronoField.SECOND_OF_MINUTE, 2)
.toFormatter();
private DateTimeFormatter rfc3339formatParser = new DateTimeFormatterBuilder()

private final DateTimeFormatter rfc3339formatParser = new DateTimeFormatterBuilder()
.appendValue(ChronoField.YEAR, 4)
.appendLiteral('-')
.appendValue(ChronoField.MONTH_OF_YEAR, 2)
Expand Down Expand Up @@ -83,14 +84,13 @@ public class Java8Rfc3339 extends AbstractRfc3339
.optionalStart()
.appendOffset("+HH:MM", "z")
.optionalEnd()

.toFormatter();

public Java8Rfc3339()
{
for (int i = 1; i < MAX_FRACTION_DIGITS; i++)
{
this.formats[i] = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss." + repeat('S', i) + "XXX");
this.formats[i] = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss." + repeat(i) + "XXX");
}
}

Expand Down Expand Up @@ -125,18 +125,13 @@ public String formatUtc(Date date)
@Override
public OffsetDateTime parseDateTime(final String s)
{
if (s == null || s.isEmpty())
{
return null;
}

return OffsetDateTime.from(rfc3339formatParser.parse(s));
}

private String repeat(char c, int repeats)
private String repeat(int repeats)
{
final char[] chars = new char[repeats];
Arrays.fill(chars, c);
Arrays.fill(chars, 'S');
return new String(chars);
}

Expand Down
Loading

0 comments on commit f102fbb

Please sign in to comment.