Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions src/main/java/org/rutebanken/util/LocalTimeISO8601XmlAdapter.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,31 @@
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoField;
import java.util.HashMap;

public class LocalTimeISO8601XmlAdapter extends XmlAdapter<String, LocalTime> {

private static final DateTimeFormatter formatter = new DateTimeFormatterBuilder().appendPattern("HH:mm:ss")
.optionalStart().appendFraction(ChronoField.MILLI_OF_SECOND, 0, 3, true).optionalEnd()
.optionalStart().appendPattern("XXXXX")
.optionalEnd()

//
.parseDefaulting(ChronoField.OFFSET_SECONDS,OffsetDateTime.now().getLong(ChronoField.OFFSET_SECONDS) ).toFormatter();

@Override
public LocalTime unmarshal(String inputDate) {
return LocalTime.parse(inputDate, formatter);
/**
* We store a cache of parsed LocalTime instances to avoid wasting memory in immutable value
* objects that strictly identical and interchangeable.
* Since there is a limited number of seconds in a single day, this cannot grow unbounded.
* If input data differs by milliseconds or even nanoseconds, this might cause problems. It
* is, however, very unlikely to happen in the context of NeTEx.
*/
private final HashMap<LocalTime, LocalTime> cache = new HashMap<>();

@Override
public LocalTime unmarshal(String input) {
var key = LocalTime.parse(input, formatter);
return cache.computeIfAbsent(key, time -> time);
}

@Override
Expand Down
119 changes: 119 additions & 0 deletions src/test/java/org/rutebanken/util/LocalTimeISO8601XmlAdapterTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package org.rutebanken.util;

import java.util.List;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;

import java.time.LocalTime;

import static org.junit.jupiter.api.Assertions.*;

public class LocalTimeISO8601XmlAdapterTest {

private final LocalTimeISO8601XmlAdapter adapter = new LocalTimeISO8601XmlAdapter();

@Test
public void testUnmarshalBasicTime() {
LocalTime result = adapter.unmarshal("14:30:00");
assertEquals(LocalTime.of(14, 30, 0), result);
}

@Test
public void testUnmarshalTimeWithMilliseconds() {
LocalTime result = adapter.unmarshal("14:30:00.123");
assertEquals(LocalTime.of(14, 30, 0, 123_000_000), result);
}

@Test
public void testUnmarshalTimeWithOffset() {
LocalTime result = adapter.unmarshal("14:30:00+02:00");
assertEquals(LocalTime.of(14, 30, 0), result);
}

@Test
public void testUnmarshalTimeWithMillisecondsAndOffset() {
LocalTime result = adapter.unmarshal("14:30:00.456+01:00");
assertEquals(LocalTime.of(14, 30, 0, 456_000_000), result);
}

@Test
public void testUnmarshalMidnight() {
LocalTime result = adapter.unmarshal("00:00:00");
assertEquals(LocalTime.MIDNIGHT, result);
}

@Test
public void testUnmarshalNoon() {
LocalTime result = adapter.unmarshal("12:00:00");
assertEquals(LocalTime.NOON, result);
}

@Test
public void testUnmarshalEndOfDay() {
LocalTime result = adapter.unmarshal("23:59:59");
assertEquals(LocalTime.of(23, 59, 59), result);
}

@Test
public void testMarshalBasicTime() {
String result = adapter.marshal(LocalTime.of(14, 30, 0));
assertEquals("14:30:00", result);
}

@Test
public void testMarshalTimeWithNanoseconds() {
String result = adapter.marshal(LocalTime.of(14, 30, 0, 123_000_000));
assertEquals("14:30:00.123", result);
}

@Test
public void testMarshalMidnight() {
String result = adapter.marshal(LocalTime.MIDNIGHT);
assertEquals("00:00:00", result);
}

@Test
public void testMarshalNoon() {
String result = adapter.marshal(LocalTime.NOON);
assertEquals("12:00:00", result);
}

@Test
public void testMarshalNull() {
String result = adapter.marshal(null);
assertNull(result);
}

@Test
public void testRoundTrip() {
LocalTime original = LocalTime.of(15, 45, 30, 500_000_000);
String marshalled = adapter.marshal(original);
LocalTime unmarshalled = adapter.unmarshal(marshalled);
assertEquals(original, unmarshalled);
}

@Test
public void testCaching() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a test with different variants like: "10:20:30" vs "10:20:30+02:00"?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

String timeString = "10:20:30";
LocalTime first = adapter.unmarshal(timeString);
LocalTime second = adapter.unmarshal(timeString);
assertSame(first, second, "Same string should return cached instance");
}

@Test
public void testCachingIdenticalValue() {
var equalInputs = List.of(
"12:20:00",
"12:20:00+02:00",
"12:20:00.000",
"12:20:00.000+02:00"
);

var parsed = equalInputs.stream()
.map(adapter::unmarshal)
.collect(Collectors.toList());
var first = parsed.get(0);

parsed.forEach(time -> assertSame(first, time, "Same time value should return same instance"));
}
}