A Date with CPAN, Part 1: State of the Union
[This is the first post in a new, probably long-ass, series. I do not promise that the next post in the series will be next week. Just that I will eventually finish it, someday. Unless I get hit by a bus.]
The topic arose at $work
recently: what do the cool kids use for dates these days? Our sysadmin was looking for a simple way to get “tomorrow.” Of course, the cool kids are theoretically using DateTime, right? So, how do we get “tomorrow” out of DateTime? The answer came back in our chat room:
Datetime->now->add(days=>1)
Well, okay ... that would work. But it’s not exactly what I’d call “easy.”
This got me pondering the state of dates in Perl, not for the first time. In fact, this is a topic I regularly revisit. Like every Perl hacker, I occasionally have to deal with dates. And like what I bet is most Perl hackers, I’m regularly unsatisfied with my options for dealing with dates. So every once in a while I go poking around CPAN, looking to see if something new has cropped up that I haven’t noticed before. In fact, the topic of dates came up in a personal project not too many weeks ago, so I still had this somewhat fresh in my mind. And, as we went back and forth a bit in the chat room about why this solution was not ideal and what might make it better, I laid out my criticisms of all the existing Perl date modules out there. It made me want to revive my idea of creating the Perfect Perl Date Module, I quipped. And several of my coworkers actually encouraged me to pursue that. And now here we are.1
So the first thing we better cover is, what exactly do I want in a date module, and why don’t any of the many (many) existing ones scratch my itch? I thought about this very carefully and deeply: if I want to add Yet Another Date Module to the already insane number of choices on CPAN, I better have my justifications laid out pretty neatly before proceeding. So here’s what I want a “perfect” date module to offer me:
- Speed: It needs to be moderately fast.
- Size: It needs to be moderately compact.
$last_job
, we were using DateTime and Date::Manip—
These two are often conflicting goals, of course. My “perfect” date module doesn’t have to be the fastest, or the smallest. It just better not be the slowest or the biggest.
- Stability: People need to have confidence in it.
- Functionality: It needs to be able to fulfill your common date needs.
- Immutability: The value of a date shouldn’t change.
These, I think, are the things that pretty much everyone wants out of a date module. Not being everyone, I personally want more.
- Truncation: I want to be able to deal with a date without having to worry about a time (at least sometimes).
- Conversion: I want to be able to convert an arbitrary human-readable string to a date.
And then you have conversion. This is a major failing with many date modules. Either they have no solution whatsoever, or they offer you a way to convert a string to a date, but only if you know the format ahead of time. One of the main reasons I want a quick, easy date solution is to have scripts that can accept a command line argument that represents a date. I don’t want to have to constrain my user to one particular date format. I want them to put in whatever they want and the script Just Works. This is not necessarily an easy thing to do, granted. But what frustrates me is that there are good, standard, accepted solutions to this problem out there. I’ve been using at least two of them for years and years. And yet, most date modules ignore this problem, apparently in the hopes that it will go away.
As long as we’re dreaming, there’s two more things I want out of a perfect date module:
- Implementation: The internal storage of the date should make it easy to convert to other modules.
- Interface: The interface offered by the module should make it easy to do the most common things.
Now, that’s a whopping nine things I want out of a module, and that’s a lot. So I better be willing to give something up. And I am.
- No Historical Dates: In 19 years as a Perl programmer, I have never—
not once!— needed to work with a date that couldn’t be represented in epoch seconds. I know some people out there need that. I’m sure there are ... umm ... Perl-programming historians, I guess? ... that need to store the date (and time?) that the Magna Carta was signed, or the span of the Ming Dynasty, or the rule of Ramesses II, or what have you. If my module can’t be used to solve those problems, I can live with that.
- Limited Timezone Support: I can live without support for arbitrary timezones. I’m willing to be satisfied with two timezones: UTC, and whatever timezone I happen to be in. If you think about it, this is exactly what’s built into Perl itself:
gmtime
andlocaltime
.
Now that I know what I want, and what I’m willing to give up to get it, let’s look at why none of the existing date solutions in Perl—
Date::Manip One of the oldest of the Perl date Swiss army knives, Date::Manip has a lot going for it: conversion is its specialty, it’s chock full of functionality, it deals with epoch seconds, which makes it a win in terms of both implementation and immutability, and the interface isn’t completely awful. Unfortunately, I have to ding it a bit on stability, since it was recently rewritten from scratch and it broke a lot of backwards-compatibility.2 Certainly it was stable up to that point though. Its big problems, however, are that it’s slow and bloated. No options for truncation either.
DateTime DateTime was supposed to be the answer to all our problems. And it is, in many ways. The interface is very nice, for most things, and the functionality is top-notch. Its implementation is not particularly standard ... except that it’s managed to make itself into the new standard, so I will give it full marks there despite that. The stability is excellent. It makes an effort at truncation, but that effort is often not quite workable, mainly because of its biggest problem: it’s mutable. Some methods, sometimes, change the underlying values. That’s bad. On top of that, it’s slow, and bloated. I used to say that DateTime was almost as big as Date::Manip, but I checked just now and it’s actually surpassed it, even loading Date::Manip’s object-oriented module: DateTime is about 15% bigger than Date::Manip::Date.3 But probably my biggest gripe with DateTime is that it doesn’t address conversion at all. If you want to convert human-readable dates to DateTime, you have to write your own parser class ... not meaning that you have to write an actual parser, of course, but you have to write a separate little class, and you have to be able to predict the format in advance. Since conversion is often literally the only thing I need to do with a date, this is an Epic Fail as far as I’m concerned.
DateTime::Moonpig I was introduced to DateTime::Moonpig by MJD’s article in the 2013 advent calendar. DateTime::Moonpig fixes the mutability problem with DateTime. Unfortunately, it doesn’t fix any of the other problems. (Update from comments: DateTimeX::Immutable is another module that does what DateTime::Moonpig does.)
Time::Piece Time::Piece is almost criminally underutilized, perhaps due to a simple lack of press. I first became aware of it via a blog post on Perl Tricks,4 and that was less than 2 years ago. And yet it’s been part of Perl core since 5.9.5, written by Matt Sergeant and Jarkko Hietaniemi, based on an interface by Larry himself, and currently maintained by RJBS. That’s a hell of a pedigree. It’s moderately fast, moderately small (under 15% of the size of DateTime), immutable, stored using epoch seconds, with gobs of functionality and a nice, clean interface. Being part of core Perl, stability is not an issue either. In fact, the only two things Time::Piece doesn’t have are the two things I want the most: conversion and truncation. Converting a known format to a Time::Piece is a bit easier than a DateTime, but it still can’t convert an arbitrary format.
Time::Moment Yet another blog post introduced me to Time::Moment, an alternative to Time::Piece. Its main claim to fame is that it’s blazingly fast. It’s also small (just a touch smaller than Time::Piece), immutable, and seems to have a decent amount of functionality.5 And, for the first time since the venerable Date::Manip, it handles arbitrary conversion: hallellujah. I’m not sure about the implementation, since that’s buried in XS somewhere—at_midnight
method to truncate a date. So I suppose that’s at least as good as DateTime.)
Date::Piece The last time I went on a hunt for the perfect date module, I stumbled across Date::Piece. The interface is gorgeous, it’s immutable, has a fair amount of functionality, and it handles truncation, in a sense: it only deals with dates, and punts to Time::Piece for datetimes. It’s about 2.6 times the size of Time::Piece, but that actually includes loading both Time::Piece and Date::Simple as well. And it’s still only 35% of the size of DateTime, so it’s not too bad. Is it stable? Well, hard to say ... Date::Piece being a moderately thin wrapper around Date::Simple, it’s really Date::Simple we need to evaluate on that score. And Date::Simple is not as new as Time::Moment—
That’s all I’ve evaluated. And there are many promising candidates here: as I said, a lot of great modules, each of which does part of what I want. A lot of people have asked me through the years, what do I use for dates? Actually, what I most commonly use is Date::Format and Date::Parse. Occasionally I also throw in Time::ParseDate if I need to handle even arbitrarier formats, like “next Thursday.” I can load all 3 of those for about half the memory of Time::Piece. Of course, I can’t do any date math with them, but you’d be surprised how often I don’t need to do any date math at all. If I do, I can throw in Date::Calc and still be in the same ballpark in terms of RAM as Date::Piece. Of course, then the problem is that they don’t particularly play nicely together. The first 3 deal with epoch seconds, but Date::Calc annoyingly wants your date passed as an array of 3 (or 6, for datetimes) elements. I’ve actually called Date::Format’s time2str
3 times to get the day, month, and year, or resorted to things like
str2time(join('-', Add_Delta_Days(split('-', time2str("%Y-%m-%d", $date)), $inc)))
... which is sort of silly. So this sort of cobbled-together solution has acceptable size and speed, good implementation and stability, immutability, and arbitrary conversion. But the functionality is average at best, the interface is fairly horrible, and it still doesn’t handle truncation.6
So that’s the state of dates in Perl today, and I say—
Next time, I’ll try to figure out how.
__________
1 Yes, I’m totally throwing my coworkers under the bus here. If the idea of creating another Perl date module irks you, you should definitely blame them, not me.
2 At $last_job
, this was a major problem, so I’m probably a bit bitter.
3 Please keep in mind that, like benchmarks, memory consumption tests will vary wildly from machine to machine. They’re mostly only good for relative comparisons, one against another, when measured on the same machine with the same version of Perl under the same phase of the moon, etc. I use a variant of the perlbloat utility from the horse book, and I will report only sizes relative to each other, using whatever versions of the modules I have installed, in my current favorite version of Perl (which happens to be 5.14.4). Take with as much salt as you need.
4 I actually feel like I read a blog post by RJBS about it a few months prior to that, but I can’t find it now.
5 The POD says “it’s not time zone aware,” but I think that just means it doesn’t handle arbitrary timezones, which I already said I was okay with. Nothing else jumped out at me as being obviously missing.
6 Well, not really. Things often get truncated when passing through Date::Calc. But that’s more accidental than anything else.
Hi Buddy
I just love these comparison articles, and I've added it to my review page:
Collected Reviews.
$many x $thanx;
Ahhhhh. Clicking twice on Preview to do two checks/updates of the text auto-logged me out. MegaSigh.
I've always enjoy your writing, and I'm looking forward to the rest of the series. I use Date::Parse and Date::Format a lot, and my needs are similar to yours. Looking forward to what you come up with.
Current short version: Use Time::Moment for performance, DateTime for the ecosystem, or Time::Piece for ease of fatpacking.
If you want to make CPAN better, a pure perl implementation of Time::Moment or starting to port some of the DateTime ecosystem would both be really helpful.
Other options: Class::Date, Panda::Date, Date::Handler.
API-wise they all have a large overlap with Time::Piece (which was once Time::Object, which is still on CPAN), and Panda::Date is apparently intended as a sort of XS rewrite of Class::Date to make it very fast.
I cannot really vouch for them explicitly as I’ve not used any of them directly, but based on its maintainer history, Class::Date seems to have some community trust. And the author of Panda::Date has submitted good patches to p5p so he seems above-average competent.
For your purposes, though, none of the modules seem to handle truncation at all, and I don’t know if they’re any good with parsing, except that Class::Date notably integrates Date::Parse if you ask it to.
I find Class::Date’s API a bit wonky though.
(Personally I’ve yet to need something Time::Piece doesn’t do, and anywhere my code goes it’s right there already, so that’s what I use. The only reason I’d ever use DateTime is if I needed to handle e.g. an unusual calendar or stuff like that: on CPAN these things are usually found as DateTime extensions.)
No historical dates? You've never put dates-of-birth into a database?
Time::Moment doesn't have an explicit method for truncating, but you can truncate to day by calling the method ->at_midnight. You can also truncate to other calendar components by chaining the ->at_midnight call with a ->with_day_of_xxxx method call. Examples:
Time::Moment supported range is 0001-01-01T00:00:00.000000000Z to 9999-12-31T23:59:59.999999999Z, regardless if Perl is compiled with 32-bit or 64-bit integers.
P.S. I'm the author of Time::Moment.
--
chansen
So you never plan to write code that presents information to the user in their local time zone? Really?
I think good timezone support is pretty crucial to most applications that have to interact with users outside of a single office.
Also don't forget locale support, which is also quite useful for any app that has to support a wide variety of users.
One more to consider:
DateTimeX::Immutable
. Like Moonpig, it makes DateTime immutable, but it leaves the rest of it unchanged. It can often serve as a drop-in replacement for DateTime. Used withaliased
you might be able to change just one line of code and never be bitten by mutating dates:use aliased 'DateTimeX::Immutable' => 'DateTime';
Plus, you can make all your DBIx::Class date objects immutable with
DBIx::Class::InflateColumn::DateTimeX::Immutable
.
Of course, this doesn't address the size or conversion issues with DateTime. I would really like to see an excellent DateTime::Format module to parse dates. For arbitrary dates, I usually end up using DateTime::Format::DateManip, but then you're doubling the size issue.
If you don't have to interact with existing code that is using DateTime, I would give Time::Moment careful consideration.
Lastly, I am a bit biased since I'm the author of
DateTimeX::Immutable
.I'm in the same boat. I rarely deal with times, I often have to deal with dates.
I sometimes feel like a freak because I rely on Date::Parse and especially Date::Format so much.
Literally all the dates I deal with are well past 1970.
I let the JavaScript in our front-end deal with locales and time zones.
Plus the length of docs for the popular DateTimePiecMoment modules scares me off.
Thanks everyone for the great comments! I’ve been swamped at
$work
, so I haven’t had a chance to respond until now. I’m going to answer everyone’s thoughts here in one post instead of trying to reply to each individually. Hopefully everyone’s okay with that.First of all, the compliments. Ron Savage and Bull Ruppert had nice things to say, and I think them for the kind words and the encouragement. confuseAcat popped by to commiserate; it seems like we have similar perspectives, so hopefully this series will interesting to him or her.
Next, the suggestions. Aristotle offered me some other modules to investigate, so I will do that and possibly review them next installment. Christian Hansen and Mark Grimes added some useful thoughts; I’ve updated the post with brief asides based on their comments.
Finally, but probably most importantly, the criticisms. I welcome these, so definitely don’t be shy about posting them. I’ll tackle each one individually.
mst writes:
Thanks for the suggestions Matt! I do generally like to make CPAN a better place. But that’s not my primary goal in this instance. What I’m aiming for here is a way to make it easier to use dates in a casual manner— how many of us need to use them most of the time— for quick solutions. One of the things I love about Perl is that it’s so easy to do so many things. Currently I feel like dates are not easy. And they’re so commonly needed that they ought to be. So this is my attempt to address that perceived need.
Michael J writes:
Of course. But my versions of both Date::Parse and Time::Local will go back to 1/1/1000. So far I haven’t encountered anyone born before then. :-D If you’re on a 32-bit machine, I suppose you’re limited to folks born after 12/13/1901, but that still seems like plenty. Now, I know there are some historical machines out there that can’t handle negative epoch seconds (or at least I’ve read about them in various changelogs), but surely this is not still an issue for people in the modern day, is it? If it is, then I suppose I’ll fall back on the next answer.
Dave Rolsky writes:
Well, if I’m writing a script which will present information to the user in their local timezone, then that’s
localtime
from their perspective, when they’re running the script. So I’m covered there. I’m sure there are lots of folks who have to handle this type of thing in web server code, where they really would have to convert to an arbitrary timezone. I’ve just never personally had to deal with that in my decades-long career. But certainly other people are going to have different experiences. (Although I see confuseAcat at least has a similar experience to mine: let the JavaScript handle it! :-D )But ultimately the real answer is this: I’m not looking to supplant DateTime. I mean, obviously I couldn’t even if I wanted to, but I don’t even want to. If that’s the need you have, then my new date module is not going to be for you. And that’s okay. My module doesn’t have to be all things to all people to be useful.
Dave also adds:
Yes, the locale thing is a bit trickier. I think my module will still be able to be useful to many people without it, and I’m definitely not planning to try to tackle it in the initial version. But it’s something that I’d be interested into looking into further, and there’s even some basic (crude) support for it in both Date::Parse and Time::ParseDate. I don’t think it will ever be the module of choice for anything that has to do serious I18N, but I’d love to eventually get to the point where it can handle simple things like parsing strings where the names of months or days are in a language other than English. But that’s definitely a goal for the future.
Thanks again for the great comments everyone! Please know that I’m always reading them even if I don’t always respond right away. Hopefully I’ll be ready for another installment in this series next week.