QR code

Date/Time Printing Can Be Elegant Too

  • St.Petersburg, Russia
  • comments

Javajava OOPoop

I owe my pretty high StackOverflow reputation to this question in particular, which I asked a few years ago: How do you print an ISO 8601 date in Java? It managed to collect a lot of upvotes since then and 20+ answers, including my own one. Seriously, why didn’t Java, such a rich ecosystem, have a built-in out-of-the-box simple solution for this primitive task? I believe this is because the designers of the Java SDK were 1) smart enough not to create a print() method right in the class Date, and 2) not smart enough to give us an extendable set of classes and interfaces to parse and print dates in an elegant way.

Fargo, Season 2 (2014) by Noah Hawley
Fargo, Season 2 (2014) by Noah Hawley

There are basically three ways to split the responsibility of parsing and printing in JDK (to my knowledge):

DTO + Utility Class

The first one is when something is responsible for printing and parsing while the object is just a data holder. There is a class SimpleDateFormat, which has to be configured first, with the right time zone and the formatting pattern. Then it has to be used to print:

DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'");
df.setTimeZone(TimeZone.getTimeZone("UTC"));
String iso = df.format(new Date());

To parse it back, there is the method parse():

Date date = df.parse("2007-12-03T10:15Z");

It’s a classic combination of a DTO and a utility class. The DTO is the Date object and the utility class is the SimpleDateFormat. The date-object exposes all required data attributes through a number of getters and the utility class prints the date. The date-object has no influence on this process. It’s not actually an object, but merely a data container. This is not object-oriented programming at all.

The Object

Java 8 introduced the class Instant with the method toString(), which returns time in ISO-8601 format:

String iso = Instant.now().toString();

To parse it back there is a static method parse() in the same class Instant:

Instant time = Instant.parse("2007-12-03T10:15:30Z");

This approach looks more object-oriented, but the problem here is that it’s impossible to modify the printing pattern in any way (for example, remove the milliseconds or change the format entirely). Moreover, the method parse() is static, which means that there can be no polymorphism—we can’t change the logic of parsing either. We also can’t change the printing logic, since Instant is a final class, not an interface.

This design sounds OK if all we need is ISO 8601 date/time strings. The moment we decide to extend it in some way, we are in trouble.

The Ugly Mix

There is also DateTimeFormatter in Java 8, which introduces the third way of dealing with date/time objects. To print a date to a String we make an instance of the “formatter” and pass it to the time-object:

LocalDateTime date = LocalDateTime.now(ZoneId.of("UTC"));
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(
  "yyyy-MM-dd'T'HH:mm:ss'Z'"
);
String iso = time.format(formatter);

To parse back, we have to send the formatter to the static method parse() together with the text to parse:

LocalDateTime time = LocalDateTime.parse(
  "2007-12-03T10:15:30Z", formatter
);

How do they communicate, LocalDateTime and DateTimeFormatter? The time-object is a TemporalAccessor, with a method get() allowing anyone to extract whatever is inside. In other words, again, a DTO. The formatter is still a utility class (not even an interface), which expects the DTO to arrive, extracts what’s inside, and prints.

How do they parse? The method parse() reads the template, and builds and returns another TemporalAccessor DTO.

What about encapsulation? “Not this time,” JDK designers say.

The Right Way

Here is how I would design it instead. First, I would make a generic immutable Template with this interface:

interface Template {
  Template with(String key, Object value);
  Object read(String key);
}

It would be used like this:

String iso = new DefaultTemplate("yyyy-MM-dd'T'HH:mm'Z'")
  .with("yyyy", 2007)
  .with("MM", 12)
  .with("dd", 03)
  .with("HH", 10)
  .with("mm", 15)
  .with("ss", 30)
  .toString(); // returns "2007-12-03T10:15Z"

This template internally decides how to print the data coming in, depending on the encapsulated pattern. Here is how the Date would be able to print itself:

class Date {
  private final int year;
  private final int month;
  private final int day;
  private final int hours;
  private final int minutes;
  private final int seconds;
  Template print(Template template) {
    return template
      .with("yyyy", this.year)
      .with("MM", this.month)
      .with("dd", this.day)
      .with("HH", this.hours)
      .with("mm", this.minutes)
      .with("ss", this.seconds);
  }
}

This is how parsing would work (it’s a bad idea in general to put code into the constructor, but for this experiment it’s OK):

class Date {
  private final int year;
  private final int month;
  private final int day;
  private final int hours;
  private final int minutes;
  private final int seconds;
  Date(Template template) {
    this.year = template.read("yyyy");
    this.month = template.read("MM");
    this.day = template.read("dd");
    this.hours = template.read("HH");
    this.minutes = template.read("mm");
    this.seconds = template.read("ss");
  }
}

Let’s say we want to print time as “13-е января 2019 года” (it’s in Russian). How we would do this? We don’t create a new Template, we decorate the existing one, a few times. First, we make an instance of what we have:

new DefaultTemplate("dd-е MMMM yyyy-го года")

This one will print something like this:

12-е MMMM 2019-го года

The Date doesn’t send the value of MMMM into it, that’s why it doesn’t replace the text correctly. We have to decorate it:

class RussianTemplate {
  private final Template origin;
  RussianTemplate(Template t) {
    this.origin = t;
  }
  @Override
  Template with(String key, Object value) {
    Template t = this.origin.with(key, value);
    if (key.equals("MM")) {
      String name = "";
      switch (value) {
      case 0:
        name = "января";
        break;
      case 1:
        name = "февраля";
        break;
        // etc...
      }
      t = t.with("MMMM", name);
    }
    return t;
  }
}

Now, to get a Russian date from a Date object we do this:

String txt = time.print(
  new RussianTemplate(
    new DefaultTemplate("dd-е MMMM yyyy-го года")
  )
);

Let’s say we want to print the date in a different time zone. We create another decorator, which intercepts the call with the "HH" and deducts (or adds) the time difference:

class TimezoneTemplate {
  private final Template origin;
  private final int zone;
  TimezoneTemplate(Template t, int z) {
    this.origin = t;
    this.zone = z;
  }
  @Override
  Template with(String key, Object value) {
    Template t = this.origin.with(key, value);
    if (key.equals("HH")) {
      t = t.with("MM", Integer.cast(value) + this.z);
    }
    return t;
  }
}

This code will print Moscow (UTC+3) time in Russian:

String txt = time.print(
  new TimezoneTemplate(
    new RussianTemplate(
      new DefaultTemplate("dd-е MMMM yyyy-го года")
    ),
    +3
  )
);

We can decorate as much as we need, making the Template as powerful as it needs to be. The elegance of this approach is that the class Date is completely decoupled from the Template, which makes them both replaceable and polymorphic.

Maybe someone will be interested in creating an open source date and time printing and parsing library for Java with these principles in mind?

sixnines availability badge   GitHub stars