人力検索はてな
モバイル版を表示しています。PC版はこちら
i-mobile

PHP で複数の日付を「期間表記」に変換するコードを教えてください。

日付の配列があったときに、
$day = array(
'2013-08-30',
'2013-08-31',
'2013-09-01',
'2013-09-02',
'2013-09-05',
'2013-09-06',
'2013-09-10',
'2014-01-01',
)

このように表示したいです。
'2013年8月30日〜9月2日、9月5日〜6日、9月10日、2014年1月1日'


●質問者: 転校生
●カテゴリ:ウェブ制作
○ 状態 :終了
└ 回答数 : 5/5件

▽最新の回答へ

1 ● だわかき
●0ポイント

条件に合うようにスクリプトを変更しました。
試してみてください。

出力結果:
2013年8月30日?9月2日,5日?6日,10日,2014年1月1日 

<?php
//年月日のデータ
$day = array(
 '2013-08-30',
 '2013-08-31',
 '2013-09-01',
 '2013-09-02',
 '2013-09-05',
 '2013-09-06',
 '2013-09-10',
 '2014-01-01',
);

//日付の古い順にソートしておく
sort($day);

$interval = 60 * 60 * 24 * 2; //2日未満なら期間にする

$str = '';
$y0 = 0;
$m0 = 0;
$d0 = 0;
$y1 = 0;
$from = '';
$i = 0;
while (isset($day[$i])) {
 if ($from == '') {
 $str .= ($i > 0) ? '' : '';
 preg_match('/(\d{4})\-(\d{2})\-(\d{2})/', $day[$i], $arr);
 $y1 = $arr[1];
 $m1 = $arr[2];
 $d1 = $arr[3];
 $str .= (($y1 != $y0) ? $y1 + 0 . '' : '') . (($m1 != $m0) ? $m1 + 0 . '' : '') . ($d1 + 0) . '';
 $from = strtotime($day[$i]);
 $y0 = $y1;
 $m0 = $m1;
 $d0 = $d1;
 } else {
 preg_match('/(\d{4})\-(\d{2})\-(\d{2})/', $day[$i], $arr);
 $y2 = $arr[1];
 $m2 = $arr[2];
 $d2 = $arr[3];
 $to = strtotime($day[$i]);
 if ($to - $from >= $interval) {
 if ($flag) {
 $str .= '' . (($y2 != $y1) ? $y2 + 0 . '' : '') . (($m2 != $m1) ? $m2 + 0 . '' : '') . ($d2 + 0) . '';
 } else {
 $str .= '?' . (($y1 != $y0) ? $y1 + 0 . '' : '') . (($m1 != $m0) ? $m1 + 0 . '' : '') . ($d1 + 0) . '' . '' . (($y2 != $y1) ? $y2 + 0 . '' : '') . (($m2 != $m1) ? $m2 + 0 . '' : '') . ($d2 + 0) . '';
 }
 $y0 = $y2;
 $m0 = $m2;
 $d0 = $d2;
 $from = $to;
 $flag = TRUE;
 } else {
 $y1 = $y2;
 $m1 = $m2;
 $d1 = $d2;
 $from = $to;
 $flag = FALSE;
 }
 }
 $i++;
}
echo $str;
?>


転校生さんのコメント
回答ありがとうございます! 単独表示なのはその日付が連続していないからです。 表記ルールに不足があったので補足しました。

tezcelloさんのコメント
> どうして...「9月10日」は単独なのでしょう? どうしてって、連続していないからなのは明らかだと。 9月9日も、9月11も無いでしょ? 「4日以内」は明らかに質問の趣旨と違うでしょ。

転校生さんのコメント
このコードですと、最後の日付が連続していると正しく表示されません。

質問者から

失礼しました。
「期間表記」の条件が不足していたので補足します。

■ 日付の配列の前提
・日付は重複はしない
・日付の配列はソートされている

■ 期間表記の条件
・連続した日付の場合は「?」で繋げる
・連続した日付の「年」が同じであれば「年」の表記は不要
・連続した日付の「月」が同じであれば「月」の表記は不要
・前項と「年」が同じであれば「年」の表記は不要
・前項と「月」が同じであれば「月」の表記は不要

サンプルの訂正です。
上記の条件に当てはめた場合、このように表記したいです。
'2013年8月30日?9月2日、5日?6日、10日、2014年1月1日'

目的としてはイベントなど不定期で開催されるものが、
データベースに「イベント情報」と「日付情報」が紐付いて保存されており、
表示する際に、単独の日付を並べるのではなく、分かりやすく表記したいためです。


2 ● うぃんど
●100ポイント ベストアンサー

一例

※重複とソートは不要ならコメントアウトしてください。
※(サンプルはデータを一部乱してありますのでサンプル稼働の際には必須です。)
※テストは http://codepad.org/ でのみ行っています。
※phpバージョン不明なので無名関数は使わないようにしました。

<?php
# 日付表記
define ( 'date_pat_ymd', 'Y年n月j日' );
define ( 'date_pat_md', 'n月j日' );
define ( 'date_pat_d', 'j日' );
define ( 'join_chr', '?' );
define ( 'deli_chr', '' );

# データ準備
$day = array(
 '2013-08-30',
 '2013-09-01',
 '2013-9-5',
 '2013-09-06',
 '2013-09-10',
 '2013-08-31',
 '2013-9-02',
 '2014-01-01',
);

# UNIXタイムスタンプへの変換(表記誤差吸収のため)
function walk_str2utime( &$p ){
 $p = strtotime( $p );
};
array_walk( $day, 'walk_str2utime' );

# 重複した値の削除
$day = array_unique( $day );

# 並び替え
sort( $day );

# 比較と整形
$s = '';
$c = count( $day );
if ( $c >= 1 ) {
 $year = date( 'Y', $day[0] );
 $year_month = date( 'Ym', $day[0] );
 $s = date( date_pat_ymd, $day[0] );
 for ( $i = 1; $i < $c; $i++ ) {
 if ( $day[$i] - $day[$i - 1] > 86400 ) {
 $s .= deli_chr;
 if ( date( 'Ym', $day[$i] ) == $year_month ) {
 $s .= date( date_pat_d, $day[$i] );
 } elseif ( date( 'Y', $day[$i] ) == $year ) {
 $s .= date( date_pat_md, $day[$i] );
 } else {
 $s .= date( date_pat_ymd, $day[$i] );
 }
 $year = date( 'Y', $day[$i] );
 $year_month = date( 'Ym', $day[$i] );
 } elseif( $i == $c - 1 || $day[$i + 1] - $day[$i] > 86400 ) {
 $s .= join_chr;
 if ( date( 'Ym', $day[$i] ) == $year_month ) {
 $s .= date( date_pat_d, $day[$i] );
 } elseif ( date( 'Y', $day[$i] ) == $year ) {
 $s .= date( date_pat_md, $day[$i] );
 } else {
 $s .= date( date_pat_ymd, $day[$i] );
 }
 $year = date( 'Y', $day[$i] );
 $year_month = date( 'Ym', $day[$i] );
 }
 }
}

echo $s;

出力結果

2013年8月30日?9月2日、5日?6日、10日、2014年1月1日

うぃんどさんのコメント
もう一例 >|php| <?php # 日付表記 define ( 'date_pat_ymd', 'Y年n月j日' ); define ( 'date_pat_md', 'n月j日' ); define ( 'date_pat_d', 'j日' ); define ( 'join_chr', '?' ); define ( 'deli_chr', '、' ); # データ準備 $day = array( '2013-08-30', '2013-09-01', '2013-9-5', '2013-09-06', '2013-09-10', '2013-08-31', '2013-9-02', '2014-01-01', ); # UNIXタイムスタンプへの変換(表記誤差吸収のため) function walk_str2utime( &$p ){ $p = strtotime( $p ); }; array_walk( $day, 'walk_str2utime' ); # 重複した値の削除 $day = array_unique( $day ); # 並び替え sort( $day ); # 比較と整形 $day_next = $day_prev = $day; array_pop( $day_prev ); array_shift( $day_next ); array_shift( $day_next ); $year = date( 'Y', $day[0] ); $year_month = date( 'Ym', $day[0] ); $s = date( date_pat_ymd, array_shift( $day ) ); function map_utime2str( $p, $d, $n ){ global $s, $year, $year_month; if ( $d - $p > 86400 ) { $s .= deli_chr; $s .= date( ( date( 'Ym', $d ) == $year_month ? date_pat_d : ( date( 'Y', $d ) == $year ? date_pat_md : date_pat_ymd ) ), $d ); $year = date( 'Y', $d ); $year_month = date( 'Ym', $d ); } elseif ( $n == '' || $n - $d > 86400 ) { $s .= join_chr; $s .= date( ( date( 'Ym', $d ) == $year_month ? date_pat_d : ( date( 'Y', $d ) == $year ? date_pat_md : date_pat_ymd ) ), $d ); $year = date( 'Y', $d ); $year_month = date( 'Ym', $d ); } }; array_map( 'map_utime2str', $day_prev, $day, $day_next ); echo $s; ||<

転校生さんのコメント
回答ありがとうございます! 正しく表示することができました。

3 ● Lhankor_Mhy
●100ポイント
<?php
//表示用関数
function show($date, $prev){
 if ( $date->format('Y') != $prev->format('Y') ){
 return $date->format('Y年n月j日');
 } elseif ( $date->format('m') != $prev->format('m') ) {
 return $date->format('n月j日');
 } else {
 return $date->format('j日');
 }
}

$day = array(
'2013-08-30',
'2013-08-31',
'2013-09-01',
'2013-09-02',
'2013-09-05',
'2013-09-06',
'2013-09-10',
'2014-01-01',
);

//日付パース
foreach ($day as $i => $value){
 $day[$i] = date_create($value);
}


sort($day);
$prev= clone $day[0];
$interval = new DateInterval('P1D');
//期間用配列のリスト
$spanList = array();
//期間用配列
$span = array( clone $day[0]);

foreach ($day as $i => $value){
 if ($prev->add($interval) < $value){ //前の日付より1日以上後
 $prev->sub($interval);
 $span[1] = $prev;
 array_push($spanList, $span);
 $span = array(clone $value);
 }
 $prev = clone $value;
}
$span[1] = $prev;
$prev = $spanList;
array_push($spanList, $span);

//テキスト化
$prev = date_create('1970-01-01');
$str = '';
foreach ($spanList as $span){
 $str .= show($span[0],$prev);
 $prev = $span[0];
 if ($span[0]->diff($span[1])->days){
 $str .= '?';
 $str .= show($span[1],$prev);
 $prev = $span[1];
 }
 $str .= '';
}
echo mb_substr($str, 0, -1);

PHPやっぱりキライです……


Lhankor_Mhyさんのコメント
あれ?サンプルが訂正されてたのか…… >|| $str .= show($span[0],$prev,true); ||< を >|| $str .= show($span[0],$prev,false); ||< に変更で。つーか、フラグいらんのか。

うぃんどさんのコメント
あえて、この場所で失礼…。 Lhankor_Mhyさん http://codepad.org/ DateIntervalの環境依存で動きませんでした。 http://ideone.com/ 動きましたが何も出力されませんでした。 環境依存度が高すぎるのだと思います。 だわかきくん http://codepad.org/ preg_matchの環境依存で動きませんでした。 http://ideone.com/ 動きましたが2014-01-02を追加しても2014年1月1日までしか出力されませんでした。 それぞれのコードに下記を追加して経過時間を計ってみましたが、 処理時間の有意差はありませんでした。 先頭 $time_start = microtime(true); 末尾 $time_end = microtime(true); echo $time_end - $time_start;

Lhankor_Mhyさんのコメント
Datetimeオブジェクトを使っていますから5.4laterですね。あと、タイムゾーンが設定されてないのかも。 やっぱりPHPはキライだなあw

Lhankor_Mhyさんのコメント
あー、分かりました。 最後の行を >|| echo substr($str, 0, -1); ||< から >|| echo mb_substr($str, 0, -1); ||< に変更します。回答編集します、失礼しました。 http://ideone.com/ では、さらにmb_internal_encodingを設定してやらないと動かないですが、その程度の環境依存はいいでしょう?

だわかきさんのコメント
ふつうのPHP環境ではないサイトを持ち出されて「環境依存」と言われても困るんですけど。

うぃんどさんのコメント
>ふつうのPHP環境ではない 「ふつう」って何ですか? 具体的な線引きをできないものは比較対象にできません。 「普通」であれば普遍的に通じるという意味ですから、 「普通」の環境でテストしておられるとおっしゃるなら、 環境依存で引っかかるはずもありませんので、 あなたのほうが普通のPHP環境ではないということになります。 2014年1月2日が出力されなかった件はコメント以前の時点でのコードです。 もしも私の当該コメント以後に回答を編集なさったなら、未確認です。

Lhankor_Mhyさんのコメント
>うぃんどさん ご意見は分かるんですが、CodePadのPCREライブラリのバージョンが 3.9 02-Jan-2002 となっており、現行の8.33からすると相当古く、どちらかというとCodePadのビルドの問題ではないのかなと思うところです。 うぃんどさんが「この環境で動作しないとダメだよ」とおっしゃっている訳ではなく「環境はチェックしていますか?」とおっしゃっているのは重々承知しているのですが、それでもCodePadのようなビルドで動作確認、というのはちょっと厳しい感じもします。JavaScriptでの回答がECMAScriptの実行環境で動作する人はおそらくないでしょうし、IE5.5で確認している人もまれでしょう。 もちろん、僕の回答のようにエッジなバージョンで記述した場合は注釈を加えなきゃいけないでしょうね。反省しています。

転校生さんのコメント
回答ありがとうございます! DateInterval は PHP 5.3 以上ですかね。 あと、環境に寄っては最後の行の第3引数に文字コードを指定しないと文字化けするみたいです。

だわかきさんのコメント
僕の指摘に逆ギレされても困るんですけど。 Lhankor_Mhyさんが書いているような意味で普通ではありません。

4 ● a-kuma3
●100ポイント

良かった、まだコンテストは終わってないようですね。
一応、ぼくもエントリーをば。

<?php
$day = array(
 '2013-08-30',
 '2013-08-31',
 '2013-09-01',
 '2013-09-02',
 '2013-09-05',
 '2013-09-06',
 '2013-09-10',
 '2013-09-30',
 '2013-10-01',
 '2013-10-02',
 '2014-01-01',
 '2014-12-30',
 '2014-12-31',
 '2015-01-01',
);

function format_date($t, $prev) {
 $local_t = localtime($t, true);
 $local_prev = localtime($prev, true);
 $fmt = 'Y年n月j日';
 if ($local_t['tm_year'] == $local_prev['tm_year']) {
 $fmt = 'n月j日';
 if ($local_t['tm_mon'] == $local_prev['tm_mon']) {
 $fmt = 'j日';
 }
 }
 return date($fmt, $t);
}


$t_prev = strtotime($day[0]);
$t_start = $t_end = $t_prev;
$t_end_prev = strtotime("1970-4-1");
$output = '';
$sep = '';

$n = count($day);
for ($i = 1 ; $i <= $n ; $i++) {
 if ($i != $n) {
 $t = strtotime($day[$i]);
 $diff = $t - $t_prev;
 if ($diff == 24*60*60) {
 $t_end = $t;
 $t_prev = $t;
 continue;
 }
 }

 $output .= $sep . format_date($t_start, $t_end_prev);
 if ($t_start != $t_end) {
 $output .= '?' . format_date($t_end, $t_start);
 }

 $t_end_prev = $t_end;
 $t_start = $t_end = $t;
 $t_prev = $t;
 $sep = '';
}

echo $output;

?>

ちょっとデータを足してますが、こんな感じで出力されます。

2013年8月30日?9月2日、5日?6日、10日、30日?10月2日、2014年1月1日、12月30日?2015年1月1日


ideone.com で試したのがこちらです。
http://ideone.com/9KRnZQ


転校生さんのコメント
回答ありがとうございます! 正しく表示することができました。

1-5件表示/6件
4.前の5件|次5件6.
関連質問

●質問をもっと探す●



0.人力検索はてなトップ
8.このページを友達に紹介
9.このページの先頭へ
対応機種一覧
お問い合わせ
ヘルプ/お知らせ
ログイン
無料ユーザー登録
はてなトップ