Convert array of `YYYMMDD` date strings to a delimited string of days grouped by month

157 Views Asked by At

PHP Dates are wonderful things +1 year for example. However I'm getting myself in a bit of a mess. I have three dates spanning two months and I want to make an elegant string "28, 29 February & 1 March" from an array of 3 dates in the form YYYYMMDD. The exact number of dates is not known, minimum 1 probable max 4

Other than some rather tortuous str_replace manipulation, how do I get from the output of this to my desired format?

NB: no more than 2 months, maybe all in one month.

$dates_array = ['20240228', '20240229', '20240301'];

foreach ($dates_array as $date) :
  $datetimes[]  = new DateTime($date);
endforeach;

// print_r($datetimes);

foreach ($datetimes as $key=>$date) :
  $month = $date->format('F');
  $day = $date->format('j');                                    
  $date_array[]= $day." ".$month." & ";
endforeach;

$date_string =  join( ' ', $date_array ) ;

echo $date_string;

// 28 February & 29 February & 1 March & 
6

There are 6 best solutions below

1
KIKO Software On BEST ANSWER

I managed to create this:

$dates = ['20240228', '20240229', '20240301'];

function formatDates(array $datesInput) {
    foreach ($datesInput as $datesKey => $date) {
        $date  = new DateTime($date);
        $month = $date->format('F');
        $day   = $date->format('j');
        if ($datesKey == 0) {
            $datesOutput = $day;
        } elseif ($currentMonth <> $month) {
            $datesOutput .= " $currentMonth & $day";
        } else {
            $datesOutput .= ", $day";
        }
        $currentMonth = $month;
    }
    return "$datesOutput $month";
}

echo formatDates($dates);

This returns:

28, 29 February & 1 March

It is not all that complicated. I did however put everything in a function because I would otherwise have to create quite a few global variables, and that's seen as bad practice.

For a demo see: https://3v4l.org/gUWih

0
Vincent Decaux On

You can try this small script, I advise you to make a nice test to be sure it works with different use cases.

<?php
$dates = ['20240228', '20240229','20240301'];
$countDates = count($dates) - 1;
$output = [];

foreach ($dates as $date) :
  $datetimes[]  = new DateTime($date);
endforeach;

foreach ($datetimes as $i => $datetime) :
  $dateOutput = [$datetime->format('j')];
  $isNextMonthTheSame = isset($datetimes[$i+1]) && $datetime->format('m') === $datetimes[$i+1]->format('m');
  if (!$isNextMonthTheSame) {
    $dateOutput []= ' '. $datetime->format('F');  
    $dateOutput []= ' & ';
  } else {
    $dateOutput []= ', ';
  }
  if ($i === $countDates) {
    array_pop($dateOutput);
  }
  $output []= $dateOutput;
endforeach;

echo implode(array_merge(...$output));

PS: the spread operator (...) has been implemented in PHP 7.4:

0
ADyson On

As you loop through the $datetimes you'd have to check whether the next value in the list is in the same month or not, in order to know whether to include the month name in the formatted output for that date or not.

This is one way to do that. The broad assumption is that the dates will always be supplied in ascending order, but it does also check if the next date is in the same year, as well as the same month, in case there's a large gap between dates.

$dates_array = ['20241203', '20240115', '20240116', '20240228', '20240229', '20240301'];

foreach ($dates_array as $date)
{
  $datetimes[]  = new DateTime($date);
}

foreach ($datetimes as $key=>$date) 
{
  $dateStr = $date->format('j');
  
  //check if there's another entry after this one
  if (isset($datetimes[$key+1]))
  {
      //check the if following entry is in the same month and year as this one
      if ($datetimes[$key+1]->format("ym") == $date->format("ym")) {
          $dateStr .= ",";
      }
      else
      {
          $dateStr .= " ".$date->format("F")." & ";
      }
  }
  else {
      $dateStr .= " ".$date->format("F");
  }
  
  $date_array[]= $dateStr;
}

$date_string =  join( ' ', $date_array ) ;

echo $date_string;

Live demo: https://3v4l.org/tWrhe

4
CBroe On

One more possible way to approach this, grouping the dates by month in an array first, and joining it all together with implode:

$dates_array = ['20240228', '20240229', '20240301'];

foreach ($dates_array as $date) {
    $date = new DateTime($date);
    $datesByMonth[$date->format('F')][] = $date->format('j');
}

foreach($datesByMonth as $month => $dates) {
    $datesByMonth[$month] = implode(', ', $dates) . ' ' . $month;
}

echo implode(' & ', $datesByMonth);

https://3v4l.org/6snHe

Obviously won't work if you had dates for the same month in different years, but I guess that's not a requirement(?).

0
Salman A On

Here is another stab at this:

Iterate over the dates, in each iteration check if previous date is contiguous and belongs to same month.

<?php
$dates_array = ["20240214", "20240228", "20240229", "20240301"];

$datetime_array = array_map(function($date_string) {
    return DateTime::createFromFormat("Ymd", $date_string);
}, $dates_array);

$groups = array_reduce($datetime_array, function($groups, $curr) {
    $key1 = array_key_last($groups);
    if ($key1 === null) {
        $key1 = 0;
    } else {
        $key2 = array_key_last($groups[$key1]);
        $prev = $groups[$key1][$key2];
        if ($prev->diff($curr)->d !== 1 || $prev->format("n") !== $curr->format("n")) {
            $key1 += 1;
        }
    }
    $groups[$key1][] = $curr;
    return $groups;
}, []);

$result = array_map(function($group) {
    $key1 = array_key_last($group);
    return $key1 === 0 ? $group[$key1]->format("j F") : ($group[0]->format("j") . "-" . $group[$key1]->format("j F"));
}, $groups);

echo implode(", ", $result);

// 14 February, 28-29 February, 1 March
0
mickmackusa On

I find CBroe's script to be very lean and readable, I might write it this way:

I might use array_map() for the second looping task so that data can be conveniently implode()ed twice.

Code: (Demo)

$dates = ['20240228', '20240229', '20240301'];

$groups = [];
foreach ($dates as $date) {
    $date = new DateTime($date);
    $groups[$date->format('F')][] = $date->format('j');
}

echo implode(
         ' & ',
         array_map(
             fn($j, $F) => implode(', ', $j) . " $F",
             $groups,
             array_keys($groups)
         )
     );

/* or
echo implode(
         ' & ',
         array_reduce(
             array_keys($groups),
             fn($result, $k) => $result + [$k => implode(', ', $groups[$k]) . " $k"],
             []
         )
     );
*/

As for a string-builder script like KIKO's, my script will either append text to the end when a new month is encountered, or inject a new "last day" for a re-encountered month.

Code: (Demo)

$dates = ['20240228', '20240229', '20240301'];

$text = '';
foreach ($dates as $i => $date) {
    $date = new DateTime($date);
    $month = $date->format('F');
    $day = $date->format('j');
    $pos = strpos($text, $month);
    if ($pos !== false) {
        $text = substr_replace($text, ", $day", $pos - 1, 0);
    } else {
        $text .= ($text ? ' & ' : '') . "$day $month";
    }
}

echo $text;