1

I am trying to compact an expression of individual days into a shorter expression including hyphen-separate ranges.

Examples:

  • mon,tue,wed,thu,fri,sat
    to be:
    mon-sat
  • mon,tue,wed,fri,sat
    to be
    mon-wed,fri-sat

My coding attempt:

function dayrange($days){
    $days = explode(",", str_replace(" ","",$days));
    return reset($days) . "-" . end($days);
}

How can I shorten the multi-day expression so that consecutive days are merged into a range of days?

mickmackusa
  • 43,625
  • 12
  • 83
  • 136
Jason
  • 15,064
  • 15
  • 65
  • 105
  • 1
    Does it need to wrap on the week too? ie. if an input like fri,sat,sun,mon comes in do you expect fri-mon or fri-sun, sun-mon? – Rasika May 10 '11 at 02:49
  • um the week wrapping thing isn't required. it would be a nice to have i suppose but not needed for this project – Jason May 10 '11 at 03:17

3 Answers3

3

Basically, I would approach this by:

  1. Converting the days to corresponding numeric values
  2. Turning the array of numbers into a string with ranges
  3. Converting the numbers in the string back into days of the week

I wrote some code to do that:

/**
 * Convert an array of numbers to a string containing ranges and single values
 * @param array $numbers an array of numbers
 * @return string
 */
function compressNumbers($numbers) {
    $result = array();
    sort($numbers);
    $previousValue = reset($numbers);
    $startValue = $previousValue;
    foreach ($numbers as $value) {
        if ($value > $previousValue + 1) {
            if ($startValue == $previousValue) {
                $result[] = $startValue;
            } else {        
                $result[] = $startValue . '-' . $previousValue;
            }
            $startValue = $value;
        }
        $previousValue = $value;
    }
    if ($startValue == $previousValue) {
        $result[] = $startValue;
    } else {        
        $result[] = $startValue . '-' . $previousValue;
    }
    return implode(',', $result);
}

/*
 * Creates an array with values the three letter representation for days of the 
 * week and keys the corresponding numeric representation.
 *
 * @return array
 */
function createLookupNumberToDay() {
    $date = strtotime('now');
    $lookup = array();
    for ($i = 1; $i <= 7; $i++) {
        $lookup[date('w', $date)] = date('D', $date);
        $date = strtotime('+1 day', $date);
    }
    return $lookup;
}

/*
 * Converts a string listing days separated by commas into 
 * an array with values the numeric value for the corresponding
 * day of the week.
 *
 * @param string $days
 * @return array
 */
function convertDaysToNumbers($days) {
    $result = array();
    $daysArray = explode(",", str_replace(" ","",$days));
    foreach ($daysArray as $day) {
        $result[] = date('w', strtotime($day));
    }
    return $result;
}

/*
 * Converts the numbers in a string to the corresponding 3-letter day of the
 * week abbreviation.
 *
 * @param string $string
 * @return string
 */
function convertNumbersToDays($string) {
    $lookup = createLookupNumberToDay();
    return str_replace(array_keys($lookup), $lookup, $string);
}

function convert($string) {
    return (convertNumbersToDays(compressNumbers(convertDaysToNumbers($string))));
}

echo convert('mon,tue,wed,thu,fri,sat');
echo '<br />';
echo convert('mon,tue,wed,sat');
echo '<br />';
Brian Fisher
  • 23,519
  • 15
  • 78
  • 82
0

Haven't tested this, but should give you a good start. It handles week wrapping as well.

function dayrange($days){
    $wdays = array("mon","tue","wed","thu","fri","sat","sun");

    $indays = explode(",", str_replace(" ","",$days)); // expand the list to an array


    $retstr = array_shift($indays); // get the first date

    $curpos = array_search($retstr, $wdays);  // current position in the wdays array
    $intv = 0;    // interval between days to avoid mon-tue like output

    foreach($indays as $d) {
       if($d == $wdays[$curpos]) {
          $curpos = ($curpos++) % 7; // this will take care of wrapping.
          $intv++;
       } else {
           $retstr.= ($intv > 1 ? "-".$d:",".$d); // use appropriate join
           $intv = 0; // reset interval
       }
    }
    if($intv > 0) {   // if anything was left deal with the end.
        $retstr.= ($intv > 1 ? "-".$d:",".$d);
    } else {
        $retstr.= ",".$d;
    }
    return ($retstr);
}
Rasika
  • 1,980
  • 13
  • 19
  • http://codepad.org/UKCMaRLN ... just returned the same string i gave it? +1 for effort – Jason May 15 '11 at 13:02
  • @Jason a sympathy uv? That does not help researchers to identify good, working solutions. Quite the contrary, answers that do not work should have a negative score. – mickmackusa Aug 29 '22 at 10:06
  • Indeed, [this answer does not return the desired result](https://3v4l.org/FBUY1). – mickmackusa Aug 30 '22 at 05:36
0
  1. Define a lookup as a constant to easily determine the sequential position of each day.

  2. Explode the string on commas and iterate the day values.

  3. If the result string is empty, add the day with no delimiter/glue.

  4. If the day is a consecutively positioned day, then potentially remove the appended yesterday substring if it was attached using a hyphen, then append a hyphen and the day.

  5. If the day is not a consecutively positioned day, then append a comma and the day.

Code: (Demo)

define('DAYS', array_flip(['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']));

function condenseDays(string $days): string
{
    $result = '';
    foreach (explode(',', $days) as $day) {
        if (!$result) {
            $result .= $day;
        } elseif (DAYS[$day] === DAYS[$yesterday] + 1) {
            $result = str_replace("-$yesterday", '', $result) . "-$day";
        } else {
            $result .= ",$day";
        }
        $yesterday = $day;
    }
    return $result;
}
echo condenseDays('mon,tue,wed,thu,fri,sat') . "\n";
echo condenseDays('tue,thu,fri,sun') . "\n";
echo condenseDays('mon,tue,wed,fri,sat,sun') . "\n";
echo condenseDays('mon,thu,sun') . "\n";
echo condenseDays('tue,wed,fri,sat') . "\n";
echo condenseDays('mon,wed,fri,sun') . "\n";
echo condenseDays('mon,tue,thu,fri,sat,sun');

Output:

mon-sat
tue,thu-fri,sun
mon-wed,fri-sun
mon,thu,sun
tue-wed,fri-sat
mon,wed,fri,sun
mon-tue,thu-sun

Alternatively, if you'd rather use a brute-force approach, you can replace commas to hyphens for all neighboring days, then use regex to remove the "guts" of multi-consecutive days.

Code: (Demo)

define(
    'PAIRS',
    [
        [
            'mon,tue',
            'tue,wed',
            'wed,thu',
            'thu,fri',
            'fri,sat',
            'sat,sun'
        ],
        [
            'mon-tue',
            'tue-wed',
            'wed-thu',
            'thu-fri',
            'fri-sat',
            'sat-sun'
        ]
    ]
);

function condenseDays(string $days): string
{
    return preg_replace(
               '/-\K[^,]+-/',
               '',
               str_replace(PAIRS[0], PAIRS[1], $days)
           );
}

Sneakiest / Least-intelligible version where range-worthy commas are identified by their neighboring letter instead of 3-letter days.

Code: (Demo)

function condenseDays(string $days): string
{
    return preg_replace(
               '/-\K[^,]+-/',
               '',
               str_replace(
                   ['n,t', 'e,w', 'd,t', 'u,f', 'i,s', 't,s'],
                   ['n-t', 'e-w', 'd-t', 'u-f', 'i-s', 't-s'],
                   $days
               )
           );
}
mickmackusa
  • 43,625
  • 12
  • 83
  • 136