Displaying a sequence of days
Asked Answered
H

4

8

Let's say I have these arrays of numbers which correspond to the days in a week (starting on Monday):

/* Monday - Sunday */
array(1, 2, 3, 4, 5, 6, 7)

/* Wednesday */
array(3)

/* Monday - Wednesday and Sunday */
array(1, 2, 3, 7)

/* Monday - Wednesday, Friday and Sunday */
array(1, 2, 3, 5, 7)

/* Monday - Wednesday and Friday - Sunday */
array(1, 2, 3, 5, 6, 7)

/* Wednesday and Sunday */
array(3, 7)

How could I effectively convert these arrays to the desired strings as shown in the C-style comments? Any help would be greatly appreciated.

Heterophyte answered 19/7, 2014 at 15:32 Comment(9)
Will they always be in order?Freeforall
Yep. If not, they could be sorted.Heterophyte
Interesting question, but if you can supply your attempt with questions, it is appreciated in the PHP tag (and probably elsewhere on the site too). +3 for a question with no attempt is surprisingly high - these sometimes attract downvotes, just so you know :)Ashtonashtonunderlyne
@Ashtonashtonunderlyne I voted for the apparent simplicity of the problem but real technical difficulty of writing an elegant way to do it. Plus op passed some kind of bare minimal understanding test when commenting they could be sorted :)Kapellmeister
@Félix, it is certainly an interesting challenge. Nevertheless, IMO all posters should be made aware of the idea of offering code in the first instance - if only to "get stuck in" and make themselves a better programmer. There is, I think, a consensus here that giving it a go oneself is far more educational than having a high quality answer presented.Ashtonashtonunderlyne
I agree with you @Ashtonashtonunderlyne (though I posted... too late!). I am not used to explain coding to other people but I should have tried to comment here and give some "clues" rather than offering code. Thank you for that piece of advice. :)Jaymejaymee
This is not specific to PHP. Would it be appropriate to remove the php tag?Lemniscate
@rvighne, I think no, it's a PHP question.Ashtonashtonunderlyne
@halfer: Agreed, I should've tried to figure it out on my own and maybe ask how I could improve upon that code. I think I'll give it a try anyway, since I'd like to get better at coding.Heterophyte
C
4

The following code should work:

<?php
// Create a function which will take the array as its argument
function describe_days($arr){
$days = array("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday");
// Begin with a blank string and keep adding data to it
$str = "";
// Loop through the values of the array but the keys will be important as well
foreach($arr as $key => $val){
// If it’s the first element of the array or ...
// an element which is not exactly 1 greater than its previous element ...
    if($key == 0 || $val != $arr[$key-1]+1){
        $str .= $days[$val-1]."-";
    }
// If it’s the last element of the array or ...
// an element which is not exactly 1 less than its next element ...
    if($key == sizeof($arr)-1){
        $str .= $days[$val-1];
    }
    else if($arr[$key+1] != $val+1){
        $str .= $days[$val-1]." and ";
    }
}
// Correct instances of repetition, if any
$str = preg_replace("/([A-Z][a-z]+)-\\1/", "\\1", $str);
// Replace all the "and"s with commas, except for the last one
$str = preg_replace("/ and/", ",", $str, substr_count($str, " and")-1);
return $str;
}

var_dump(describe_days(array(4, 5, 6)));      // Thursday-Saturday
var_dump(describe_days(array(2, 3, 4, 7)));   // Tuesday-Thursday and Sunday
var_dump(describe_days(array(3, 6)));         // Wednesday and Saturday
var_dump(describe_days(array(1, 3, 5, 6)));   // Monday, Wednesday and Friday-Saturday
?>
Cerebrovascular answered 19/7, 2014 at 15:49 Comment(4)
Instead of giving 'Monday - Wednesday and Friday', your example gives 'Monday - Friday'.Heterophyte
I like your solution, seems less "painful" than mine. But could you comment it a little? I don't get everything. (I am currently looking at it to understand what is done, but would have been easier if there had already been some informations. :) )Jaymejaymee
It doesn't work for 1,3,5,7. It gives a result of: Monday-Wednesday and Friday and Sunday, which should be Monday and Wednesday and Friday and Sunday.Concinnous
Same for 2,4,6. It gives Tuesday-Thursday and Saturday instead of Tuesday and Thursday and Saturday.Concinnous
A
3

We can translate the array-problem to a string problem, and use a regular expression (regex) solution... Indirectly it checks all conditions and cases by a finite automata.

PS: when we have short arrays and small number of cases, this kind of algorithm construction is safe and offer complete solution.

Oh yeah, we can also translate week-numbers to another languages (see $lang parameter)!

Multilingual and regex solution

function weekNumbers_toStr($days, $lang='en') {
     $and = array('pt'=>'e', 'en'=>'and');
     $strWeek = array(     // config with more langs!
        'pt'=>array("Segunda", "Terça", "Quarta", "Quinta", "Sexta", 
                  "Sábado", "Domingo"),
        'en'=> array("Monday", "Tuesday", "Wednesday", "Thursday", 
                  "Friday", "Saturday", "Sunday")
     );
     $days = array_unique($days);
     sort($days);
     $seq = preg_replace_callback(  // Split sequence by ",":
        '/0246|024|025|026|135|146|246|02|03|04|05|06|13|14|15|16|24|25|26|35|36|46/',
        function ($m) { return join( ',' , str_split($m[0]) ); },
        join('',$days)
     );
     // split two or more days by "-":
     $seq = preg_replace('/(\d)\d*(\d)/', '$1-$2', $seq);
     $a = explode(',',$seq);
     $last = array_pop($a);
     $n = count($a); 
     // Formating and translating:
     $seq = $n? join(", ",$a): $last;
     if ($last && $n) $seq = "$seq $and[$lang] $last";
     return preg_replace_callback(
        '/\d/',
        function ($m) use (&$strWeek,$lang) { 
           return $strWeek[$lang][$m[0]]; 
        },
        $seq
     );
 } // func

Testing:

print "\n".weekNumbers_toStr(array(6,1,2,3,6),'en'); // corrects order and dups
print "\n".weekNumbers_toStr(array(0,1,2,3,6));      // Monday-Thursday and Sunday

print "\n".weekNumbers_toStr(array(3,4,6),'pt');  // Quinta-Sexta e Domingo
print "\n".weekNumbers_toStr(array(3,4,6));       // Thursday-Friday and Sunday

print "\n".weekNumbers_toStr(array(2,3,4,6));  // Wednesday-Friday and Sunday
print "\n".weekNumbers_toStr(array(3,5));      // Thursday and Saturday
print "\n".weekNumbers_toStr(array(0,2,4,6));  // Monday, Wednesday, Friday and Sunday
print "\n".weekNumbers_toStr(array(0));        // Monday
print "\n".weekNumbers_toStr(array());         // (nothing)
Addiel answered 19/7, 2014 at 17:32 Comment(5)
I have to read it quietly to understand what your code is doing. Not the same way of vizualize things than me! More elegant than mine as you say. Is it also more efficient in terms of execution time/server resources? The thing is, I find my answer more understandable -to me of course-, I do not want to say I find mine better (not at all!), in fact I want to know if yours is much better and why. OK for multilingual. When you say safe, mine isn't? If yes, why? Real questions, I want to understand and improve my PHP skills, I'm not here to say "uh MY code is good". :)Jaymejaymee
@caCtus, sorry, I edited, now you can test. Is like a parser... Your algorithm is ok (!), this here is only a exotic one, to illustrate a "regex solution". Perhaps the performance is worst (can you do a benchmark?) and, yes, "understandable": only better for show cases of "sequence break" and "two or more days" (see comments).Addiel
Thank you for all these explanations! I'm not very familiar with regular expressions (I use it only if it is obvious I need them, e.g. when I have to check an email address) so this solution (as well as @SharanyaDutta's) is very interesting to me. :)Jaymejaymee
About performance. When benchmarking, the CPU times are all near same, with @caCtus's displayDays() faster. Using displayDays as unit of CPU time (more is worst) we have: my weekNumbers_toStr()=2.4; @SharanyaDutta's describe_days()=1.3. @Otanaught's makeMeHappy() is 10 times slower than displayDays. My is stable, describe_days() and displayDays() are faster when input have less elements. PS: to compare with mine, need to remove multilingual, sort and array_unique lines.Addiel
@caCtus, you are welcome. Now, reading all, I think @SharanyaDutta is the more "understandable" ;-) When using jddayofweek() runs with near same time, 1.17 displayDays.Addiel
C
3
/* Monday - Sunday */
$days1 = [1, 2, 3, 4, 5, 6, 7];

/* Monday - Wednesday and Sunday */
$days2 = [1, 2, 3, 7];

/* Wednesday and Sunday */
$days3 = [3, 7];

/* Monday - Wednesday and Friday - Sunday */
$days4 = [1, 2, 3, 5, 6, 7];

/* Monday - Wednesday, Friday and Sunday */
$days5 = [1, 2, 3, 5, 7];

/* Monday, Wednesday, Friday and Sunday */
$days6 = [1, 3, 5, 7];

// creates logic groups out of flat array
function splitIntoSequences(array $days)
{
    $collection = [];
    do {
        $seq = extractSequence($days);
        $collection[] = $seq;

        $days = array_diff($days, $seq);
    }
    while (!empty($days));

    return $collection;
}

// filters the next sequence
function extractSequence(array $days)
{
    sort($days);

    $seq = [];
    foreach ($days as $day) {
        // seq break
        if (!empty($seq) && $day - 1 != $seq[count($seq) - 1]) {
            return $seq;
        }

        $seq[] = $day;
    }

    return $seq;
}

// removes all unneeded steps from sequences
function cleanupSequences(array $collection)
{
    $cleanedCollection = [];
    foreach ($collection AS $seq) {
        if (count($seq) == 1) {
            $cleaned = [array_pop($seq)];
        } else {
            $cleaned = [array_shift($seq), array_pop($seq)];
        }

        $cleanedCollection[] = $cleaned;
    }

    return $cleanedCollection;
}

// convert whole collection to daynames
function convertToDaynames(array $collection)
{
    $daynameCollection = [];
    foreach ($collection AS $seq) {
        $days = [];
        foreach ($seq as $day) {
            $days[] = numberToDay($day);
        }

        $daynameCollection[] = $days;
    }

    return $daynameCollection;
}

// number to dayname
function numberToDay($weekday)
{
    $relative = sprintf('next Sunday + %d day', $weekday);
    $date = new \DateTime($relative);

    return $date->format('l');
}

// format output
function format(array $collection)
{
    $t = [];
    foreach ($collection as $seq) {
        $t[] = implode(' - ', $seq);
    }

    if (count($t) == 1) {
        return array_pop($t);
    }

    $last = array_pop($t);

    return sprintf('%s and %s', implode(', ', $t), $last);
}

Test calls:

function makeMeHappy(array $seq)
{
    $splitted = splitIntoSequences($seq);
    $cleaned = cleanupSequences($splitted);
    $daynames = convertToDaynames($cleaned);
    return format($daynames);
}


var_dump(makeMeHappy($days1));
// string(15) "Monday - Sunday"

var_dump(makeMeHappy($days2));
// string(29) "Monday - Wednesday and Sunday"

var_dump(makeMeHappy($days3));
// string(20) "Wednesday and Sunday"

var_dump(makeMeHappy($days4));
// string(38) "Monday - Wednesday and Friday - Sunday"

var_dump(makeMeHappy($days5));
// string(37) "Monday - Wednesday, Friday and Sunday"

var_dump(makeMeHappy($days6));
// string(36) "Monday, Wednesday, Friday and Sunday"
Canonicals answered 19/7, 2014 at 19:34 Comment(0)
J
2

I tried and tested this :

/* Monday - Sunday */
$days1 = array(1, 2, 3, 4, 5, 6, 7);

/* Monday - Wednesday and Sunday */
$days2 = array(1, 2, 3, 7);

/* Wednesday and Sunday */
$days3 = array(3, 7);

/* Monday - Wednesday and Friday - Sunday */
$days4 = array(1, 2, 3, 5, 6, 7);

/* Monday - Wednesday, Friday and Sunday */
$days5 = array(1, 2, 3, 5, 7);

/* Monday, Wednesday, Friday and Sunday */
$days6 = array(1, 3, 5, 7);

function displayDays($days = array()) {

    // 1: Create periods and group them in arrays with starting and ending days
    $periods = array();

    $periodIndex = 0;

    $previousDay = -1;
    $nextDay = -1;

    foreach($days as $placeInList => $currentDay) {     
        // If previous day and next day (in $days list) exist, get them.
        if ($placeInList > 0) {
            $previousDay = $days[$placeInList-1];
        }
        if ($placeInList < sizeof($days)-1) {
            $nextDay = $days[$placeInList+1];
        }

        if ($currentDay-1 != $previousDay) {
        // Doesn't follow directly (in week) previous day seen (in our list) = starting a new period
            $periodIndex++;
            $periods[$periodIndex] = array($currentDay);
        } elseif ($currentDay+1 != $nextDay) {
        // Follows directly previous day, and isn't followed directly (in week) by next day (in our list) = ending the period       
            $periods[$periodIndex][] = $currentDay;
            $periodIndex++;
        }
    }
    $periods = array_values($periods);


    // Arrived here, your days are grouped differently in bidimentional array.
    // print_r($periods); // If you want to see the new array's structure

    // 2: Display periods as we want.
    $text = '';
    foreach($periods as $key => $period) {
        if ($key > 0) {
        // Not first period
            if ($key < sizeof($periods)-1) {
            // Not last period either
                $text .= ', ';
            } else {
            // Last period
                $text .= ' and ';
            }
        }

        if (!empty($period[1])) {
        // Period has starting and ending days
            $text .= jddayofweek($period[0]-1, 1).' - '.jddayofweek($period[1]-1, 1);
        } else {
        // Period consists in only one day
            $text .= jddayofweek($period[0]-1, 1);
        }
    }

    echo $text.'<br />';
}

displayDays($days1);
displayDays($days2);
displayDays($days3);
displayDays($days4);
displayDays($days5);
displayDays($days6);

jddayofweek() returns the day of week. In that function 0 is Monday, 6 is Sunday, that's why the "-1" here each time I used it : your Monday is 1 and your Sunday is 7.

Jaymejaymee answered 19/7, 2014 at 16:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.