Search code examples
phpdateformattingdate-range

Echo durations from multiple dates (ex. Aug 25-27, Sep 3)


I'm dealing with events that can either take place on a single day (ex. 5/20) or multiple days (ex. 8/25, 8/26, 8/27, 9/3).

Given, for example, a 4-day event taking place on 8/25, 8/26, 8/27, and 9/3, I'd like to echo this:

Aug 25-27, Sep 3

I'd like the code to:

  • Handle single day events (ex. Oct 3)
  • Group consecutive days (ex. Jan 25-27)
  • Separate non-consecutive days with a comma (ex. Sep 19, 22)
  • Handle a range of dates spanning multiple months (ex. Feb 28-Mar 2)
  • Handle multiple ranges if necessary (ex. Apr 2-4, Jun 10-13)
  • Avoid redundant date info (ex. Dec 1-3, Dec 8)

This is easy to do with only a single-day event using date() formatting, but is it possible to intelligently produce formatting like this using multiple dates when necessary?


Solution

  • I made a function that should output the required string based on an array of DateTime objects. I placed some inline comments to indicate what is happening at a given time in the function.

    function produceDateString(array $dates): string
    {
        // sort the dates
        sort($dates);
    
        // create an array of arrays that contain ranges of consecutive days
        $ranges = [];
        $currentRange = [];
        foreach ($dates as $date) {
            if(empty($currentRange) || consecutive(end($currentRange), $date)) {
                $currentRange[] = $date;
            } else {
                $ranges[] = $currentRange;
                $currentRange = [$date];
            }
        }
        $ranges[] = $currentRange;
    
        // create the output string
        $output = '';
        $previous = null;
        foreach ($ranges as $range) {
            // add a comma between each range
            if (!empty($output)) {
                $output .= ', ';
            }
    
            // the long format should be used on the first occurrence of the loop
            // or when the month of first date in the range doesn't match 
            // the month of the last date in the previous range
            $format = $previous === null || end($previous)->format('m') !== reset($range)->format('m')
                ? 'M. j'
                : 'j';
    
            // the output differes when there are 1 or multiple dates in a range
            if (count($range) > 1) {
                // the output differs when the end and start are in the sane month
                $output .= sameMonth(reset($range), end($range))
                    ? reset($range)->format($format).'-'.end($range)->format('j')
                    : reset($range)->format('M. j').'-'.end($range)->format('M. j');
            } else {
                $output .= reset($range)->format($format);
            }
    
            $previous = $range;
        }
    
        return $output;
    }
    
    function consecutive(DateTime $t1, DateTime $t2): bool
    {
        $t1->setTime(0, 0, 0, 0);
        $t2->setTime(0, 0, 0, 0);
        return(abs($t2->getTimestamp() - $t1->getTimestamp()) < 87000);
    }
    
    function sameMonth(DateTime $t1, DateTime $t2): bool
    {
        return $t1->format('Y-m') === $t2->format('Y-m');
    }
    

    I made a small 3v4l to show you how it works exactly. Don't hesitate to ask if you might have any questions about how this works.