Mehr Zeit mit FreshMarker (2)

„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>

Leave a Comment