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日'

回答の条件
  • 1人5回まで
  • 13歳以上
  • 登録:2013/08/31 14:01:22
  • 終了:2013/09/04 10:07:47

ベストアンサー

id:windofjuly No.2

うぃんど回答回数2625ベストアンサー獲得回数11492013/08/31 17:56:25

ポイント100pt

一例

※重複とソートは不要ならコメントアウトしてください。
※(サンプルはデータを一部乱してありますのでサンプル稼働の際には必須です。)
※テストは 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日
id:windofjuly

もう一例

<?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;
2013/08/31 20:49:15
id:tenkousei

回答ありがとうございます!
正しく表示することができました。

2013/09/01 12:49:13

その他の回答(4件)

id:dawakaki No.1

だわかき回答回数797ベストアンサー獲得回数1222013/08/31 15:59:14

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

出力結果:
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;
?>

他1件のコメントを見る
id:tezcello

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

2013/08/31 16:42:12
id:tenkousei

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

2013/09/01 12:43:30
id:tenkousei

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

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

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

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

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

id:windofjuly No.2

うぃんど回答回数2625ベストアンサー獲得回数11492013/08/31 17:56:25ここでベストアンサー

ポイント100pt

一例

※重複とソートは不要ならコメントアウトしてください。
※(サンプルはデータを一部乱してありますのでサンプル稼働の際には必須です。)
※テストは 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日
id:windofjuly

もう一例

<?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;
2013/08/31 20:49:15
id:tenkousei

回答ありがとうございます!
正しく表示することができました。

2013/09/01 12:49:13
id:Lhankor_Mhy No.3

Lhankor_Mhy回答回数779ベストアンサー獲得回数2312013/08/31 20:50:07

ポイント100pt
<?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やっぱりキライです……

他7件のコメントを見る
id:tenkousei

回答ありがとうございます!

DateInterval は PHP 5.3 以上ですかね。
あと、環境に寄っては最後の行の第3引数に文字コードを指定しないと文字化けするみたいです。

2013/09/01 12:54:14
id:dawakaki

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

2013/09/01 14:25:41
id:a-kuma3 No.4

a-kuma3回答回数4621ベストアンサー獲得回数19562013/09/01 12:17:59

ポイント100pt

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

<?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

id:tenkousei

回答ありがとうございます!
正しく表示することができました。

2013/09/01 12:56:47
id:tobeoscontinue No.5

tobeoscontinue回答回数214ベストアンサー獲得回数542013/09/01 12:51:10

ポイント100pt
<?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',
);

function date_unique($p, $f) {
  $u = '';
  if ($p[0] != $f[0]) $u .= $f[0].''.$f[1].'';
  elseif ($p[1] != $f[1]) $u .= $f[1].'';
  return $u.$f[2].'';
}
// $pと$fはarray(年,月,日)となっており異なる部分が文字列として返ります。
// bugがあったため
//  if ($p[0] != $f[0]) $u .= $f[0].'年';
//  if ($p[1] != $f[1]) $u .= $f[1].'月';
// を修正しました。
function date_notation($day) {
  $nota = array();
  $to = $s = 0;
  foreach ($day as $t) {
    $stamp = strtotime($t);
    if ($stamp <= $to)
      $nota[$s][1] = $stamp;
    else
      $nota[++$s] = array($stamp, $stamp);
    $to = strtotime('+1 day', $stamp);
  }
// $dayから期間を表現するarray(from, to)のペアーに変換し$notaに格納します。
// 連続しない場合はfromとtoは同一です。
  $d = array();
  $p = array(0,0,0);
  foreach ($nota as $t) {
    $a = explode('-', date('Y-n-j', $t[0])); // $t[0]をarray(年,月,日)に変換します。
    $b = explode('-', date('Y-n-j', $t[1]));
    $d[] = ($t[0] == $t[1]) ? date_unique($p, $b)
		  : date_unique($p, $a).''.date_unique($a, $b);
    $p = $b;
  }
  return implode(', ', $d);
}
// $notaのarray(from, to)を文字に変換します。
// ', 'の挿入が面倒だったのでいったん配列に格納しimplodeで結合しています。
echo date_notation($day)."\n";
?>
  • id:tezcello
    年跨ぎの場合はどうなるのでしょう?
    2013年12月30日~2014年1月3日
    かな?

    重複する日付は存在しないのでしょうか?

    必ず日付順に並んでいるのでしょうか?
  • id:tenkousei
    はい。そうです。
    年跨ぎや月跨ぎの場合はそれぞれに年や月を表示させたいです。
  • id:windofjuly
    うぃんど 2013/09/01 11:56:06
    >うぃんどさんの回答に、だわかきさんからコメントが投稿されました。

    >コメント:
    >「僕の回答やNo.3の方の回答を批判していたコメントを、どうして削除したのですか?
    >はてな社から注意されたからですか?
    >」

    以上のようなメッセージが届きましたが、意味不明です。

    私が何かしているように見せかけたいのでしょうか?
    それとも何かの勘違いで投稿されたのでしょうか?
  • id:dawakaki
    僕がコメントしたように書かれていますが、何のことでしょうか。
    僕はそんなコメントはしていませんよ。
  • id:windofjuly
    うぃんど 2013/09/04 08:59:36
    >だわかき 2013/09/04 06:57:29 Add Star
    >僕がコメントしたように書かれていますが、何のことでしょうか。
    >僕はそんなコメントはしていませんよ。

    事実であれば成りすましですね。はてなに問い合わせてみます。

この質問への反応(ブックマークコメント)

トラックバック

「あの人に答えてほしい」「この質問はあの人が答えられそう」というときに、回答リクエストを送ってみてましょう。

これ以上回答リクエストを送信することはできません。制限について

絞り込み :
はてなココの「ともだち」を表示します。
回答リクエストを送信したユーザーはいません