RSSなどに使われるW3C形式の日時をパースする正規表現。
この形式は、Date and Time Formats(W3C)で定められていて、この文書ではそう呼んでいないものの、一般にはW3C形式とかW3CDTF、W3C-DTF等と呼ばれているようです。 RSS(RDF Site Summary、RSS-DEVWorkingGroup/TheWebKANZAKI参照)の時刻(dc:date、Dublin-coreモジュールのdate要素)ではこの書式が使われていて、これを読みやすい記述に変換するために作成しました。
W3C形式の日時から、年、月、日、時、分、秒などを抜出せますので、あとはprintf関数やsprintf関数で整形するなり、Time::localモジュールやTime::gmモジュールで時間に直すなり、自由に扱えるでしょう。
日時の記述は、以下の正規表現でパースできます。
(\d{4})(?:-(\d{2})(?:-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?(Z|([+-]\d{2}):(\d{2}))?)?)?)?
日時の記述を上の正規表現でマッチングすると、$1から順に、年、月、日、時、分、秒(整数部)、秒(小数部)、タイムゾーン、オフセット(時間)、オフセット(分)が入ります。
Date and Time Formats(W3C)はインターネットで使用される時間の記法として、ISO 8601のサブセットとして定義されているようです。 これによると、以下の記法を使うことができます。
| 記法 | 記述例 |
| [年] | 1975 |
| [年]-[月] | 1975-01 |
| [年]-[月]-[日] | 1975-01-09 |
| [年]-[月]-[日]T[時]-[分][タイムゾーン] | 1975-01-09T12:23+09:00 |
| [年]-[月]-[日]T[時]-[分]-[秒][タイムゾーン] | 1975-01-09T12:23:35+09:00 |
| [年]-[月]-[日]T[時]-[分]-[秒].[秒(小数部)][タイムゾーン] | 1975-01-09T12:23:35.9+09:00 |
各部の書式は次のようになっています。
| 年 | 4桁の正数 |
| 月、日、時、分、秒 | 2桁の正数 |
| 秒(小数部) | 1桁以上の正数 |
| タイムゾーン | +hh:mmか-hh:mm。または'Z'。 |
タイムゾーンを'Z'とした場合にはUTC(標準時、日本時間より9時間遅れる)を示します。 タイムゾーンの省略した場合については特に記述が見つからないのですが、実際にはサーバーまたは時刻記述の提供者(サイトのオーナなど)のローカルタイムとして扱われることが多いように見受けられます。
上の正規表現は、以下の様にして、端っこから少しずつ作成しました。
my $deci = '\\.(\\d)';
my $num = '\\d{2}';
my $tzd = "Z|([+-]$num):($num)";
my $sec = "($num)(?:$deci)?";
my $min = "$num";
my $hour = "$num";
my $time = "T($hour):($min)(?::$sec)?($tzd)?";
my $day = "-($num)(?:$time)?"; # 「日」を省略した場合、「時刻」以降も省略なので、まとめる。
my $month = "-($num)(?:$day)?"; # 「月」を省略した場合、「日」以降も省略なので、まとめる。
my $year = '\\d{4}';
my $pattern = "($year)(?:$month)?";
print $pattern, "\n\n";
スクリプト中でこの正規表現を使う時には、上のサンプルスクリプトのように直接正規表現を記述してしまう代わりに、この1ブロックを(print行抜きで)埋め込むのでも、もちろん構いません。
Date and Time Formatsで定められている各記法の日時をパースします。
my $pattern = '(\d{4})(?:-(\d{2})(?:-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d))?)?(Z|([+-]\d{2}):(\d{2}))?)?)?)?';
my @dates = (
'1997-07-16T19:20:30.4Z',
'1997-07-16T19:20:30.4+01:00',
'1997-07-16T19:20:30+01:00',
'1997-07-16T19:20+01:00',
'1997-07-16',
'1997-07',
'1997',
);
foreach my $date (@dates) {
$date =~ /$pattern/;
my %result = (
'year' => $1,
'month' => $2 ? $2 : '01',
'day' => $3 ? $3 : '01',
'hour' => $4 ? $4 : 0,
'min' => $5 ? $5 : 0,
'sec' => $6 ? $6 : 0,
'deci' => $7 ? $7 : 0,
'tgz' => $8 ? $8 : '',
'+hour' => ($8 eq 'Z') ? '+00' : $9 ? $9 : '',
'+min' => ($8 eq 'Z') ? '00' : $10 ? $10 : '',
);
print $date, "\n";
print "\t", join(', ', map { $result{$_} } qw(year month day hour min sec deci tgz +hour +min)), "\n";
}
タイムゾーンが'Z'の時は、'+hour'と'+min'に明示的に'+00'、'00'を入れています。 また、月、日が省略された場合はそれぞれ1を、時、分、秒が省略された場合は、それぞれ0を入れています。 この辺りは、後の処理の都合で空文字列にしても良いでしょう。
W3C形式からYukiWiki/WalWikiの日付形式に変換します。 YukiWiki/WalWikiの日付形式はコメント欄やRecentChangesで使われている、「YYYY-MM-DD (WDay) hh:mm:ss」と言う形式です。
処理過程では、まずオフセットをなくすために時間を表す秒数を求め、localtime関数でローカルタイムに変換、曜日も取得、その後sprintf文で整形しています。
use Time::Local;
# $dateにW3C形式の日時をセットする。
if ($date =~ /^(\d{4})(?:-(\d{2})(?:-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d))?)?(Z|([+-]\d{2}):(\d{2}))?)?)?)?$/) {
my ($year, $month, $day, $hour, $min, $sec, $wday) = ($1, ($2 ? $2 : 1), ($3 ? $3 : 1), $4, $5, $6);
my $offset = (abs($9) * 60 + $10) * ($9 >= 0 ? 60 : -60) if ($8);
my $time = ($8) ? &Time::Local::timegm($sec, $min, $hour, $day, $month - 1, $year) - $offset
: &Time::Local::timelocal($sec, $min, $hour, $day, $month - 1, $year) - $offset;
($sec, $min, $hour, $day, $month, $year, $wday) = localtime($time);
$wday = (qw(Sun Mon Tue Wed Thu Fri Sat))[$wday];
$date = sprintf('%04d-%02d-%02d (%s) %02d:%02d:%02d', $year + 1900, $month + 1, $day, $wday, $hour, $min, $sec);
}
[[#rcomment]]