„Ausdauer wird früher oder später belohnt – meistens aber später.“
Wilhelm Busch
Wer sich an die Aufzählung der unterstützen temporalen Datentypen erinnert, wird diverse neue Klassen wie Instant
, ZonedDateTime
oder OffsetDateTime
vermissen.
In diesem Beitrag werden zwei von ihnen in die Gruppe der unterstützen Datentypen aufgenommen. Bevor die Implementierung dieser Erweiterung vorgestellt wird, ein paar Worte zu den temporalen Java Standardklassen.
Die Standardklassen unterscheiden die Klassen mit Bezug zu Momenten auf dem UTC (Universal Time Coordinated aber offiziell Coordinated Universal Time) Zeitstrahl und denen ohne Bezug. Bislang wurden in FreshMarker nur die drei Standardklassen hinzugefügt, die ohne Bezug zum Zeitstrahl sind.
Die Klassen LocalDate
und LocalTime
beschreiben nur das Datum bzw. die Uhrzeit. Die Klasse LocalDateTime
fasst beide Angaben zusammen.
Alle drei repräsentieren keinen Moment auf den Zeitstrahl. Bei LocalDate
und LocalTime
ist es offensichtlich, weil ihre Angaben unvollständig sind. Aber auch der Klasse LocalDateTime
fehlt der feste Bezugspunkt (daher auch der Präfix der Klassennamen).
Einen direkten Bezug zum Zeitstrahl hat die Klasse Instant
. Sie beschreibt genau einen fixen Zeitpunkt auf dem UTC Zeitstrahl. Egal wo auf der Welt, eine Instant
erzeugt in Australien und eine Instant
erzeugt in Europa beschreiben exakt den gleichen Moment, wenn sie gleich sind. Mit Instant
zu arbeiten ist aber unbequem, weil Momentangaben dann doch besser in einem Bezug zum Ort verwendet werden sollen.
Hier kommen nun Zeitzonen und Offsets in Spiel. Beim internationalen Telefonieren ist es häufig angebracht, auf die Uhrzeit zu achten um den Angerufenen nicht aus dem Bett zu schrecken. Ist es eine Telefonnummer in Greenwich England, dann ist dies mit Instant
einfach zu lösen, denn die Coordinated Universal Time entspricht der Greenwich Mean Time.
Für andere Zeitzonen bietet sich die Klasse ZonedDateTime
an. Sie definiert einen Moment auf dem Zeitstrahl mit Bezug auf eine Zeitzone. Möchte man lieber mit Offsets arbeiten, dann ist die Klasse OffsetDateTime
das Mittel der Wahl. Die Klasse ZonedDateTime
hat aber den Vorteil, dass auch Sommer- und Winterzeit beachtet werden.
Dennoch sind LocalDate
, LocalTime
und LocalDateTime
auch nützlich und eine gute erste Wahl, um die Implementierung in FreshMarker zu verifizieren. In vielen Anwendungsfällen reicht eine einfache Uhrzeit oder eine Datumsangabe. Insbesondere bei der Generierung von E-Mail und anderen Dokumenten aus einem Template. Selten sind bei Mitgliederversammlungen oder Öffnungszeiten Zeitzonen-Angaben vonnöten.
In diesem Beitrag sollen Instant
und ZonedDateTime
zu den unterstützten temporalen Klassen hinzugefügt werden. Das bedeutet erst einmal, dass für beide Klassen ein Mapping auf eine Modelklasse und ein Formatter definiert werden muss.
@Override public void registerMapper(Map<Class<?>, Function<Object, TemplateObject>> mapper) { mapper.put(Instant.class, o -> new TemplateInstant((Instant) o)); mapper.put(ZonedDateTime.class, o -> new TemplateZonedDateTime((ZonedDateTime) o)); // ... } @Override public void registerFormatter(Map<Class<? extends TemplateObject>, Formatter> formatter) { formatter.put(TemplateInstant.class, new DateTimeFormatter("uuuu-MM-dd hh:mm:ss VV", ZoneOffset.UTC)); formatter.put(TemplateInstant.class, new DateTimeFormatter("uuuu-MM-dd hh:mm:ss VV"); // ... }
Das Mapping ist bei beiden Klassen trivial, denn es werden eigene Modelklassen TemplateInstant
und TemplateZonedDateTime
bereitgestellt.
Als Formatter wird der FreshMarker DateTimeFormatter
verwendet. Etwas verwunderlich erscheint vielleicht die Angabe der Zeitzone UTC für die Instant
. Obwohl die Instant
einen Moment auf den UTC Zeitstrahl definiert, besitzt sie keine Zeitzone. Bei der Formatierung mit Zeitzonenangaben wie VV
wirft das System einen Fehler für Instant
, wenn dem Formatter keine Zeitzone bekannt ist.
Die Implementierung der beiden Modelklassen beinhaltet die Hilfsmethode atZone
mit deren Hilfe eine neue Instanz von TemplateZonedDateTime
mit einer speziellen Zeitzone erstellt werden kann.
public class TemplateInstant extends TemplatePrimitive<Instant> implements TemplateDateTime { public TemplateInstant(Instant value) { super(value); } @Override public TemplateZonedDateTime atZone(ZoneId zoneId) { return new TemplateZonedDateTime(getValue().atZone(zoneId)); } }
Bei den Built-Ins gibt es neben den bisherigen Convenient Methoden (string
und c
) der Konvertierung zwischen den Typen (date_time
, date
und time
) auch noch Methoden für Zeitzonen (at_zone
und zone
).
builtIns.put(INSTANT_BUILDER.of("date_time"), new FunctionalBuiltIn( (TemplateObject x, List<TemplateObject> y, ProcessContext e) -> ((TemplateInstant) x).atZone(ZoneId.systemDefault()).toLocalDateTime())); builtIns.put(INSTANT_BUILDER.of("date"), new FunctionalBuiltIn( (TemplateObject x, List<TemplateObject> y, ProcessContext e) -> ((TemplateInstant) x).atZone(ZoneId.systemDefault()).toLocalDate())); builtIns.put(INSTANT_BUILDER.of("time"), new FunctionalBuiltIn( (TemplateObject x, List<TemplateObject> y, ProcessContext e) -> ((TemplateInstant) x).atZone(ZoneId.systemDefault()).toLocalTime())); builtIns.put(INSTANT_BUILDER.of("c"), new FunctionalBuiltIn( (TemplateObject x, List<TemplateObject> y, ProcessContext e) -> new TemplateString(String.valueOf(x)))); builtIns.put(INSTANT_BUILDER.of("string"), new FunctionalBuiltIn( (TemplateObject x, List<TemplateObject> y, ProcessContext e) -> formatTemporal(y, e, ((TemplateInstant) x).getValue()))); builtIns.put(INSTANT_BUILDER.of("at_zone"), new FunctionalBuiltIn( (TemplateObject x, List<TemplateObject> y, ProcessContext e) -> ((TemplateInstant) x).atZone(getZoneIdName(y, e))));
Für beide Implementierung liefern date_time
, date
und time
die jeweiligen Local
-Varianten. Eine andere Implementierung wäre zwar möglich, aber diese ist die einfachste. Das Built-In at_zone
erzeugt für beide Implementierung eine neue TemplateZonedDateTime
. Auch für TemplateLocalDateTime
existiert ein neues Built-In at_zone
.
Das Built-In zone
liefert die Zeitzone und existiert nur für TemplateZonedDateTime
, denn ein TemplateInstant
besitzt keine Zeitzone.
Damit ist die Erweiterung der FreshMarker Bibliothek um die temporalen Typen Instant
und ZonedDateTime
auch schon skizziert und es fehlt nur noch die Aktualisierung der Dokumentation.
Wie immer findet sich die neueste Version der Bibliothek auf Maven Central.
<dependency> <groupId>de.schegge</groupId> <artifactId>freshmarker</artifactId> <version>0.4.0</version> </dependency>