Date/Time Printing Can Be Elegant Too
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.
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:
1 2 3 | 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()
:
1 | 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:
1 | String iso = Instant.now().toString(); |
To parse it back there is a static method parse()
in the same class Instant
:
1 | 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:
1 2 3 4 5 | 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:
1 | 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:
1 2 3 4 | interface Template { Template with(String key, Object value); Object read(String key); } |
It would be used like this:
1 2 3 4 5 6 7 8 | 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:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 | 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):
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 | 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.with( "MM" ); this .day = template.with( "dd" ); this .hours = template.with( "HH" ); this .minutes = template.with( "mm" ); this .seconds = template.with( "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:
1 | new DefaultTemplate( "dd-е MMMM yyyy-го года" ) |
This one will print something like this:
1 | 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:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | class RussianTemplate { private final Template origin; RussianTemplate(Template t) { this .origin = t; } @Override Template with(String key, Object value) { Template t = this .origin.with( "MM" , 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:
1 2 3 4 5 | 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:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 | class TimezoneTemplate { private final Template origin; private final int zone; RussianTemplate(Template t, int z) { this .origin = t; this .zone = z } @Override Template with(String key, Object value) { Template t = this .origin.with( "MM" , 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:
1 2 3 4 5 6 7 8 | 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?
Published on Java Code Geeks with permission by Yegor Bugayenko, partner at our JCG program. See the original article here: Date/Time Printing Can Be Elegant Too Opinions expressed by Java Code Geeks contributors are their own. |