Monday, September 2, 2013

Date Formatting in Dart


I love strftime. It has been around for such a long time that it is almost second nature to me. And yet, Dart does not have strftime, which makes me sad.

Even when first reported, the Dart maintainers indicated that they favored an internationalization approach to date formatting. I have remained skeptical ever since. Today, I am going to give it a try. I still think it likely that I will end up writing my own strftime package in Dart, but let's see first.

That said, I have to admit that the default date/time format in Dart is very much to my liking. The only date/time format that I like is ISO 8601, which is just what Dart does. In my HTTP logging code, I have been directly interpolating a DateTime object (now) into a string:
log(req) {
  req.response.done.then((res){
    var now = new DateTime.now();
    print('[${now}] "${req.method} ${req.uri.path}" ${logStatusCode(res)}');
  });
}
The result is a very sensible iso8601 string with millisecond precision:
[2013-09-02 13:32:04.825] "DELETE /widgets/958c9320-f0d0-11e2-c5d0-df3be6475337" 204
[2013-09-02 13:32:04.829] "GET /widgets/958c9320-f0d0-11e2-c5d0-df3be6475337" 404
I am not used to seeing that level of precision in HTTP logs, but I rather like it. Still, what if I wanted to remove it and add the numeric timezone (e.g. -0500) to make the timestamp more Apache-like?

I start by adding the intl packages to the list of dependencies in my project:
name: plummbur_kruk
description: A real pretend server for testing.
#...
dependencies:
  # ...
  intl: any
Then I Pub install my newest dependency (along with its dependencies):
➜  plummbur-kruk git:(master) ✗ pub install   
Resolving dependencies............................
Downloading logging 0.6.19 from hosted...
Downloading unmodifiable_collection 0.6.19 from hosted...
Downloading intl 0.6.19 from hosted...
Downloading analyzer_experimental 0.6.19 from hosted...
Downloading args 0.6.19 from hosted...
Dependencies installed!
Finally, I import the intl package into my code:
library plummbur_kruk;

import 'dart:io';
import 'package:json/json.dart' as JSON;
import 'package:intl/intl.dart';
// ...
Now, to add the timezone to the iso8601 date, I ought to be able to use the default format with the time zone appended. The add_jz() method ought to do the trick:
// ...
final DateFormat _timestamp = new DateFormat().add_jz();

String get timestamp {
  return _timestamp.format(new DateTime.now());
}
// ...
Only that blows up pretty badly:
Uncaught Error: UnimplementedError
Stack Trace:
#0      _DateFormatPatternField.formatTimeZone (package:intl/src/date_format_field.dart:385:5)
#1      _DateFormatPatternField.formatField (package:intl/src/date_format_field.dart:174:38)
#2      _DateFormatPatternField.format (package:intl/src/date_format_field.dart:112:25)
#3      DateFormat.format.<anonymous closure> (package:intl/date_format.dart:231:63)
#4      List.forEach (dart:core-patch/growable_array.dart:182:8)
#5      DateFormat.format (package:intl/date_format.dart:231:26)
#6      timestamp (package:plummbur_kruk/server.dart:226:27)
#7      log.<anonymous closure> (package:plummbur_kruk/server.dart:219:15)
...
After a bit of digging, I find that I have a pretty darn good knack for finding the one thing in libraries that is not quite done:
//  packages/intl/src/date_format_field.dart
// ...
  String formatTimeZoneId(DateTime date) {
    // TODO: implement time zone support
    throw new UnimplementedError();
  }
// ...
Darn it.

Well, I am not going to get exactly what I want from the DateFormat class, but I would still like to get a feel for how it works. As I said, I rather prefer the ISO 8601 format for date formatting, but if I wanted more of a standard Apache log along the lines of:
[02/Sep/2013:09:28:08 -0400]
How easy is it to make?

Actually, it is quite easy:
final DateFormat _timestamp = new DateFormat('dd/MMM/y HH:mm:ss');
This produces:
Now: 02/Sep/2013 22:22:30
Aside from Apache's ridiculous colon-before-the-time thing, this is exactly what I want. But there is no way that I am going to remember those “skeleton” formats.

The thing that strftime always had going for it (and even the date command) was that the hours, minutes, and seconds are all capital letters:
$ date +%H:%M:%S
22:28:15
Anything beyond that and I can pretty much infer given that times are uppercase:
date +'%Y-%m-%d %H:%M:%S'
2013-09-02 22:29:50
The mixing and matching of case in Dart's DateFormat feels awkward in comparison. The documentation makes mention of standards (ICU/JDK), so hopefully it will be comfortable for others. I can live with it as is.

Before calling it a night, I give the internationalization a whirl. The way this works in Dart is that a top-level initializeDateFormat() call is made. The return value is a Future, which completes when the specified locale is loaded (from the filesystem or from a webserver), at which point code can run. For a server-side test, I use:
import 'package:intl/intl.dart';
import 'package:intl/date_symbol_data_local.dart';

main() {
  initializeDateFormatting(null, null).then((_) {
    // timestamp here...
  });
}
The arguments to initializeDateFormatting() are a little different depending on which file is imported. The one that I choose, date_symbol_data_local actually ignores both arguments—hence the two nulls. For other imports, the first argument indicates the locale to be loaded, which cuts down on bandwidth. With the import that I have chosen, I just load all of the locales.

To use a locale, I supply it as the second argument to a DateFormat constructor:
main() {
  initializeDateFormatting(null, null).then((_) {
    print('Now: ${timestamp}');
  });
}

final DateFormat _timestamp = new DateFormat('dd/MMM/y HH:mm:ss', 'fr_FR');

String get timestamp {
  return _timestamp.format(new DateTime.now());
}
The result is:
$ dart date_format_test.dart
Now: 02/sept./2013 23:48:50
Which is (I guess) the short month version of September in French.

I am still unsure about using this in an actual application. Does the entire application have to wait until the locale data is loaded? If not that, then does each individual field get wrapped in that Future? Questions for another day. For now, it is good to know that internationalization is baked into the language from the outset.


Day #862

No comments:

Post a Comment