PHPで日付の表記ゆれを修正する例
数年前に作ったWebシステムの話なのですが、日付の入力項目をテキストボックスにしていたため、「2022/1/1」だったり、「22/1/1」だったり、何なら「2022年1月頃」や「2022年1月~2月」みたいな様々な日付が入力されてしまったシステムがあります。
なんでそんなことをしたのか。プルダウンで選ばせれば良かったのに、と鼻で笑う前にちょっと聞いてほしいんです。
数値入力をプルダウンにするシステムはクソだと思うんだ
(嘘です。ちょっと言い過ぎました。)
日付入力をプルダウンにする例
↓これ
年 月 日
入力しやすいですか。こういう日付入力のフォームは山ほどあるので、「慣れ」からこれでいいや、という人はいると思いますが、これこそが日付入力における最適解だ!とまでは決して言えないと思うのですよ。
完全にマウスしか使われないフォームならまだしも、日付の入力が必要な状況においては同時に住所氏名など他のテキスト入力を伴うケースも多いと予想されます。その状況でプルダウンを持ってくるとキーボードとマウスを行ったり来たりしないといけません。
もちろんキーボードの矢印キーでプルダウンの上下移動は出来ますが、月だけでも12種、日に至っては31種あります。また、上記のフォームでは手抜きして西暦を2021~2023年の範囲にしていますが、誕生日の入力フォームで西暦欄がプルダウンだったときの苛立たしさは筆舌に尽くしがたいものがあります。
プルダウンにフォーカスをあてた状態で数字を入力すればそれが選ばれるだろうって? そのとおり。その機能があるからギリギリ我慢できているだけであって、最初からひとつのテキストボックスにしておいたほうが使いやすいとぼくは考えます。コピペもできるし。
コピペもできるし!(大事なことなので2回書きました)
誕生日の入力を思い浮かべるとコピペできることの利点がいまいちわかりづらいかもですが、購入日や発送日だったらいかがでしょうか。
購入した商品のユーザー登録フォームとか、普通に購入日を入力するケースがあると思います。片方はAmazonの注文履歴、もう片方にメーカーの登録フォームを開き、商品の型番をコピペ。そういうケースもありますよね。このとき、購入日がプルダウンだとコピペできなくてイラっとします。
そうだ、type="date"を使おう
昔は対応ブラウザが限られていたので避けていたのですが、今やほとんどのブラウザが対応しているtype属性dateがあります。
■ソース
<input type="date" value="2022-01-26">
■実行結果
これも正直ビミョーではあります。
キーボードとマウスのどちらでの入力も可能じゃないか、と一瞬喜んだものの、ブラウザごとにUIが違うというのと、結局コピペは使えないという残念さがあります。
それでもまぁ、いちいちJavaScriptで作りこまなくてもカレンダーの表示もしてくれるし、特に直近の日付を入力したい場合(配送日指定とか)では実際便利だと思うんですよ。
ですので、今後はただのテキストボックスではなく、様子見しながら徐々にtype="date"へ変更していこうかな、と考え中です。
人は自由入力欄に日付をどのように書くのか
ここからが本題です。フロントエンド側がプルダウンだろうと、カレンダー式だろうと日付の入力チェックや、表記の訂正をサーバー側で実施しなければならないことに変わりはありません。
2022-02-31等、存在しない日付入れられても困るわけです。………まぁシステムによっては大して困らんけどもw
少し興味があったため、趣味のサイトにおいて日付の入力欄をテキストボックスにしてみて、ユーザーがどんな入力をするのか調べてみました。
調査期間は1年ちょっと。対象ユーザー数は2000人くらい。入力件数も2000件程度です。趣味のサイトでの調査なので件数が少ないのはご容赦ください。
2022年01月01日 |
2022年1月1日 |
22年1月1日 |
2022年1月頃 |
2022年1月ごろ |
2022年1月初旬 |
2022年1月はじめ |
2022年1月~2月頃 |
2022年1月、2月 |
2022年1月 2022年2月 |
2022/1/1 |
2022/01/01 |
20220101 |
202201 |
2022_01_01 |
2022.01.01 |
'22.01.01 |
22.1/1 |
いやぁ~、バラエティ豊かだし、何よりこれを全て2022年01月01日と認識できる人間の脳みそってすごいなと感心します。
人間が見てわかる表記なんだから別に統一する必要はない。それもひとつの考え方ですが、ソート機能を使おうとなると話が変わってきます。
これが商品の購入日だったとして、新しい順や古い順に並び変えたい、という需要もあることでしょう。
ならば、入力は自由でも、実際のデータベースに格納する日付形式は整えなければなりません。
サーバーサイド(PHP)で日付の表記ゆれに対応する
PHPソース
ではサクっと表記ゆれを修正するソースをどうぞ。
//////////////////////////////////////////////////////////////////////////
//設定
$dsn = 'sqlite:shop.db';
$table_name = 'orders';
$field_name = 'order_date';
$key_field = 'seq';
//それ以降は無視する記号(例:「2022年1月、2022年2月」等と区切られていた場合は「、」以降は無視する
$separator_ignore = array(',', '、', '~', '頃', 'ごろ', 'はじめ', '初旬', '中旬', '下旬', ' ');
//////////////////////////////////////////////////////////////////////////
$db = new PDO($dsn);
$db->beginTransaction();
$st = $db->query("SELECT * FROM {$table_name} WHERE {$field_name} <> ''");
$stUpd = $db->query("UPDATE {$table_name} SET {$field_name}=? WHERE {$key_field}=?");
while ($row = $st->fetch()) {
$new_date = $row[$field_name];
//それ以降は無視する記号
foreach ($separator_ignore as $separator) {
$pos = mb_strpos($new_date, $separator);
if ($pos !== false) {
$new_date = mb_substr($new_date, 0, $pos);
}
}
//日付っぽい区切り文字をハイフンに統一
$new_date = mb_convert_kana($new_date, 'n'); //全角数字を半角数字へ変換
$new_date = str_replace('年', '-', $new_date);
$new_date = str_replace('月', '-', $new_date);
$new_date = str_replace('日', '', $new_date);
$new_date = str_replace('/', '-', $new_date);
$new_date = str_replace('.', '-', $new_date);
$new_date = str_replace('_', '-', $new_date);
$new_date = str_replace('?', '', $new_date);
$new_date = str_replace('?', '', $new_date);
$tempArray = explode('-', $new_date);
if (count($tempArray) == 1) {
if (is_numeric($new_date)) {
if (strlen($new_date) == 8) { //8桁のケース(例:20220101)
$tempArray[0] = substr($new_date, 0, 4);
$tempArray[1] = substr($new_date, 4, 2);
$tempArray[2] = substr($new_date, 6, 2);
} else if (strlen($new_date) == 6) { //8桁のケース(例:202201)
$tempArray[0] = substr($new_date, 0, 4);
$tempArray[1] = substr($new_date, 4, 2);
$tempArray[2] = '01';
} else {
$new_date = '';
}
} else {
$new_date = '';
}
}
if (is_numeric($tempArray[0])) {
if (!isset($tempArray[1]) || $tempArray[1] == '') { //月が不明な場合は1月とする
$tempArray[1] = '01';
}
if (!isset($tempArray[2]) || $tempArray[2] == '') { //日が不明な場合は1日とする
$tempArray[2] = '01';
}
if (substr($tempArray[0], 0, 1) == "'") { //西暦2桁対応
$y = substr($tempArray[0], 1);
if (strlen($y) == 2 && is_numeric($y)) {
$tempArray[0] = '20'.$y;
}
} else if (strlen($tempArray[0]) == 2 && is_numeric($tempArray[0])) {
$tempArray[0] = '20'.$tempArray[0];
}
if (mktime(0, 0, 0,$tempArray[1], $tempArray[2], $tempArray[0]) === false) {
echo '<span style="color:red;">error</span>';
}
//数値の0埋め(2022-1-1 → 2022-01-01)
$new_date = sprintf('%04d-%02d-%02d', $tempArray[0], $tempArray[1], $tempArray[2]);
}
if ($row[$field_name] != $new_date) {
echo $row[$key_field].':['.$row[$field_name].']→['.$new_date.']<br>';
$stUpd->execute(array($new_date, $row[$key_field]));
}
}
$db->commit();
解説
ソースの最初に設定項目を設けてあり、データベース接続情報はもちろんのこと、対象となるテーブル名やフィールド名も指定できるようにしています。
上記の例ではSQLiteのshop.dbにある、ordersテーブルを対象とし、日付の項目名はorder_dateとなっています。そして各レコードを識別するためのキーはseqという名称ですね。
日付の表記ゆれの修正ルールは以下のとおり。
2022年1月~2月という表記では「~」以降を無視する |
2022年1月、2月という表記では「、」以降を無視する |
「2022年1月頃」「2022年1月?」等、日付が不明な場合は1日と判断する |
上記同様、初旬中旬下旬でも1日とする |
2022.01.01は2022-01-01と判断する |
2022_01_01も2022-01-01と判断する |
22年01月01日や22-01-01では西暦を2000年代と判断する |
上記同様 '22-01-01 も2022年と判断する |
20220101と8桁の場合は2022-01-01と判断する |
220101と6桁の場合も2022-01-01と判断する |
これで完璧ってことはないですけども、少なくともうちのサイトでテストした2000件のデータにおいては全て問題なく YYYY-MM-DD 形式への変換が完了しました。
ソース後半でmktime関数を使っているところがありますが、変換結果が日付的に正しくない場合はエラーを表示するようにしているので、うちより更にカオスな日付入力があったとしても、ちょちょいと修正できることでしょう。
まとめ
- 日付入力をプルダウンにすると入力が面倒なのでテキストボックスにして運営していた
- 運営から1年ほど経ってから、その日付でのソート機能が欲しくなり、日付の表記ゆれを修正する必要に迫られた
- 2000件のデータを調査し、日付の表記ゆれを統一するPHPプログラムを書いてみた
といったところでしょうか。
ほーら、プルダウン入力にしておいたほが楽だったじゃないか~、という声が聞こえてきそうですが、プログラマー側が楽でも入力側は楽じゃないと思うんですよねー。…………たぶん。
どちらにせよ、今後は <input type="date"> で様子を見ようかなーと思っております。
こんな細かいことで悩むケースはそんなにないかもですが、たまたまここにたどり着いたどこかのプログラマーさんのお役に立てたなら幸いです。
あ、この記事に限りませんが、ぼくが書いたコードは好きに使って頂いて大丈夫です。大したコードは掲載していないので書くまでもないと思っていましたが、「このコード、同人ゲームで使っても良いですか」なんて問い合わせを頂いたこともあるので、一応明言しておきます。ご自由にどうぞ~。