Wednesday, January 13, 2010

Perl date validation performance

So I was benchmarking some code, and noticed that DateTime was causing a significant portion of the runtime.
Edit: Updated to include more modules.

I was curious to find out what the fastest way to valid dates was, so ran a test using DateTime, Date::Calc, Time::Piece and Time::Local.
The results are below, but they show that Date::Calc was by far and away the quickest method, followed by Time::Piece (even though that one doesn't natively do date checking. (ie. I had to write extra code around it.))

The tests below are for checking "bad" and "good" dates, in case there was a difference. (I expect 99% of dates to be valid, after all.)
As it happens, it only made any difference to Time::Local.

I also tried testing Date::Tiny, DateTime::Tiny, and DateTime::LazyInit, however the first two didn't actually do date validation in any useful way, and the latter failed to install. (And I suspect would just return the same performance as DateTime..)

So, here are the results!

tobyc@arya:~/git/cbt/performance$ ./dates.pl
# Higher numbers are better.
gooddatetime 7328/s
baddatetime 7802/s
badtimelocal 11471/s
goodtimelocal 39665/s
goodtimepiece 63021/s
badtimepiece 63743/s
gooddatecalc 399124/s
baddatecalc 400509/s



My actual code used was:

#!/usr/bin/perl
#
# Benchmark script for Date validation methods.
#
use strict;
use warnings;
use Benchmark qw(:all :hireswallclock);
use DateTime;
use Time::Piece;
use Time::Local qw(timelocal);
use Date::Calc qw(check_date);

my $gooddate = '2009-02-01';
my $baddate = '2009-02-31';
our $dtre = qr/^(\d{4})-(\d\d)-(\d\d)$/;
our $format = '%Y-%m-%d'; # Also %F

# Check these all pass the good date:
die unless datetime($gooddate);
die unless tlocal($gooddate);
die unless timepiece($gooddate);
die unless datecalc($gooddate);

# Check they fail the invalid date:
die if datetime($baddate);
die if tlocal($baddate);
die if timepiece($baddate);
die if datecalc($baddate);

cmpthese(-5, { # ie. 5 seconds each
gooddatetime => sub { datetime($gooddate) },
goodtimepiece => sub { timepiece($gooddate) },
baddatetime => sub { datetime($baddate) },
badtimepiece => sub { timepiece($baddate) },
goodtimelocal => sub { tlocal($gooddate) },
badtimelocal => sub { tlocal($baddate) },
gooddatecalc => sub { datecalc($gooddate) },
baddatecalc => sub { datecalc($baddate) },
});

sub datetime {
my $date = shift;
eval {
$date =~ $dtre;
my $dt = DateTime->new(
year => $1,
month => $2,
day => $3
);
};
return(1) unless $@;
return;
}

sub timepiece {
my $date = shift;
my $match;
eval {
my $tp = Time::Piece->strptime($date, $format);
my $reverse = $tp->strftime($format);
$match = ($date eq $reverse);
};
return $match;
}

sub tlocal {
my $date = shift;
eval {
$date =~ $dtre;
my $time = timelocal(0,0,0, $3, ($2-1), $1);
};
return(1) unless $@;
return 0;
}

sub datecalc {
my $date = shift;
$date =~ $dtre;
return check_date($1, $2, $3);
}