Loop back from today to generate a list of shifts/rota from a set template

Viewed 152

I have a set of working hours and want to generate a list of suggested shifts, all based on the current 'now' time and working backwards into the past.

E.g hours template:

$templateShifts = collect([
    (object)[
        "id" => 1,
        "from_day" => 1, // mon
        "from_time" => "09:00:00",
        "until_day" => 1,
        "until_time" => "12:00:00",
    ],
    (object)[
        "id" => 2,
        "from_day" => 1, // mon
        "from_time" => "13:00:00",
        "until_day" => 1,
        "until_time" => "17:00:00",
    ],
    (object)[
        "id" => 3,
        "from_day" => 2, // tue
        "from_time" => "09:00:00",
        "until_day" => 2,
        "until_time" => "17:00:00",
    ],
    (object)[
        "id" => 4,
        "from_day" => 4, // thur
        "from_time" => "09:00:00",
        "until_day" => 4,
        "until_time" => "17:00:00",
    ],
    (object)[
        "id" => 5,
        "from_day" => 5, // fri
        "from_time" => "09:00:00",
        "until_day" => 5,
        "until_time" => "17:00:00",
    ]
]);

If today is thursday(4th jan) it should list date in reverse order:

  • Thur 09:00 - 17:00 (4th jan)
  • Tue 09:00 - 17:00 (2nd jan)
  • Mon 13:00 - 17:00 (1st jan)
  • Mon 09:00 - 12:00 (1st jan)
  • Fri 09:00 - 17:00 (31st dec)
  • Thur 09:00 - 17:00 (30th dec)
  • etc

This code feels horrible but I couldn't find a way of repeating a collection loop once it reaches the end without copying the code.

// Paging
$total_count = 1000;
$on_page = 30;
$page = $request->get('page') ?: 1;
$skip = $on_page * ($page - 1);

// Find closest match in the past from now
$date = now();
$year = $date->year;
$weekNo = $date->weekOfYear;

$shifts = collect();

// Decrement week no
if ($skip) {
    $weekNo -= $skip;

    // Change year till positive weekNo
    while ($weekNo < 1) {
        --$year;
        $date->subYear();
        $weekNo += $date->isoWeeksInYear;
    }
}

// First page
if (!$skip) {
    // First search for shift today
    $firstShiftIndex = $templateShifts->search(function ($item, $key) use ($date) {
        return $item->from_day == $date->format('N') && $item->from_time <= $date->format('H:i:s');
    });

    $currentWeekNo = false;
    if ($firstShiftIndex === false) {
        // Loop back from today till find a matching day
        for ($i = 1; $i <= 7; $i++) {
            $date = $date->clone()->subDays(1);

            $firstShiftIndex = $templateShifts->search(function ($item, $key) use ($date) {
                return $item->from_day == $date->format('N');
            });

            if ($firstShiftIndex !== false) {
                $year = $date->year;
                $weekNo = $date->weekOfYear;
                break;
            }
        }
    }

    $templateShifts->slice($firstShiftIndex)->each(function ($item, $key) use ($shifts, $date, $year, $weekNo) {
        $shift_from = $date->clone()->setISODate($year, $weekNo, $item->from_day);
        list($hour, $minutes) = explode(':', $item->from_time);
        $shift_from->setTime($hour, $minutes);

        // Crosses into next week/maybe year too
        if ($item->until_day < $item->from_day) {
            $weekNo++;
            if ($weekNo < 1) {
                --$year;
                $date->subYear();
                $weekNo = $date->isoWeeksInYear;
            }
        }

        $shift_until = $date->clone()->setISODate($year, $weekNo, $item->until_day);
        list($hour, $minutes) = explode(':', $item->until_time);
        $shift_until->setTime($hour, $minutes);

        if ($shift_from->toDateString() == $shift_until->toDateString()) {
            $name = $shift_from->format('d/m/Y D H:i') . ' - ' . $shift_until->format('H:i');
        } else {
            $name = $shift_from->format('d/m/Y D H:i') . ' - ' . $shift_until->format('D H:i');
        }

        $shifts->push([
            'id' => $item->id,
            'text' => $name
        ]);
    });
}

//
for($i=0; $i < $on_page; $i++) {
    --$weekNo;

    if ($weekNo < 1) {
        --$year;
        $date->subYear();
        $weekNo = $date->isoWeeksInYear;
    }

    $templateShifts->each(function ($item, $key) use ($shifts, $date, $year, $weekNo) {
        $shift_from = $date->clone()->setISODate($year, $weekNo, $item->from_day);
        list($hour, $minutes) = explode(':', $item->from_time);
        $shift_from->setTime($hour, $minutes);

        // Crosses into next week/maybe year too
        if ($item->until_day < $item->from_day) {
            $weekNo++;

            if ($weekNo < 1) {
                --$year;
                $date->subYear();
                $weekNo = $date->isoWeeksInYear;
            }
        }

        $shift_until = $date->clone()->setISODate($year, $weekNo, $item->until_day);
        list($hour, $minutes) = explode(':', $item->until_time);
        $shift_until->setTime($hour, $minutes);

        if ($shift_from->toDateString() == $shift_until->toDateString()) {
            $name = $shift_from->format('d/m/Y D H:i') . ' - ' . $shift_until->format('H:i');
        } else {
            $name = $shift_from->format('d/m/Y D H:i') . ' - ' . $shift_until->format('D H:i');
        }

        $shifts->push([
            'id' => $item->id,
            'text' => $name
        ]);
    });
}
2 Answers

First of all, your expected data seems wrong, as January 4th is not an thursday (My birthday actually and that was a monday).

This is a tricky one, as there is multiple problems to tackle, i will break it down into 3 stages.

  • Looping through weeks instead of days or shifts.
  • Mapping your shift data, to the given week days.
  • Calculating the distance to find the shift closest to the end of the week we are looping.

The first part, my idea is to loop through each week, figure out the shifts and subtract the date we are iterating to the next week. This is obtained with the following code. Subtracting the week is used with the Carbon methods ->subWeek()->endOfWeek(Carbon::SUNDAY).

$now = Carbon::parse('2021-01-04')->endOfDay();
$suggestions = collect();

while ($suggestions->count() <= 30) {
    $suggestedShifts = $this->suggestShifts($now);

    $suggestions->push(... $suggestedShifts);

    $now->subWeek()->endOfWeek(Carbon::SUNDAY);
}

The suggestShifts() method looks the following. Mapping all shifts to the current week, if the shift is greater than the date we are comparing to ignore it, this is only true on the first pass thou. Set the date on the shift and calculate the difference to the date we are comparing too, this will be sorted by and securing that the next shift suggested is the closets to the current date.

We are working with objects, be careful with objects are by reference, therefor i'm using one clone to avoid overwriting the same shifts. While also cloning the dates, to avoid changing the same dates over and over.

private function suggestShifts(Carbon $date)
{
    return $this->shifts->map(function ($shift) use ($date) {
        if ($shift->until_day > $date->dayOfWeekIso) {
            return null;
        }

        $untilTime = explode(':', $shift->until_time);
        $shift->date = $date->clone()->subDays($date->dayOfWeekIso - $shift->until_day)->setTime($untilTime[0], $untilTime[1], $untilTime[2]);
        $shift->diff = $date->diff($shift->date)->h;

        return clone $shift;
    })->filter()
        ->sortByDesc(function ($shift) {
            return $shift->date->timestamp;
        })->all();
}

I'm not certain this is the quickest route, but the code for this is fairly clean and readable and should get you going in the right direction. Also not that suggestions is a shift object with an date attached. My date comparison is using the until times, if you want that change to starting time you should easily be able to do that. This code was executed locally with the following results. Printing it out like so. Again note, that january 4th is a monday.

This code also returns 32 elements, i don't feel the amount is the core of the problem and you will splice the array when you figure out which way to use this approach. And $this->shifts, is your shift structure pasted in on a property.

$suggestions->each(function ($shift) {
    $this->info('Shift: ');
    $this->info('starting - ' . $shift->date->clone()->setHour(explode(':', $shift->from_time)[0])->toIso8601ZuluString());
    $this->info('ending - ' . $shift->date->toIso8601ZuluString());
});

Shift:
starting - 2021-01-04T13:00:00Z
ending - 2021-01-04T17:00:00Z
Shift:
starting - 2021-01-04T09:00:00Z
ending - 2021-01-04T12:00:00Z
Shift:
starting - 2021-01-01T09:00:00Z
ending - 2021-01-01T17:00:00Z
Shift:
starting - 2020-12-31T09:00:00Z
ending - 2020-12-31T17:00:00Z
Shift:
starting - 2020-12-29T09:00:00Z
ending - 2020-12-29T17:00:00Z
Shift:
starting - 2020-12-28T13:00:00Z
ending - 2020-12-28T17:00:00Z
Shift:
starting - 2020-12-28T09:00:00Z
ending - 2020-12-28T12:00:00Z
Shift:
starting - 2020-12-25T09:00:00Z
ending - 2020-12-25T17:00:00Z
Shift:
starting - 2020-12-24T09:00:00Z
ending - 2020-12-24T17:00:00Z
Shift:
starting - 2020-12-22T09:00:00Z
ending - 2020-12-22T17:00:00Z
Shift:
starting - 2020-12-21T13:00:00Z
ending - 2020-12-21T17:00:00Z
Shift:
starting - 2020-12-21T09:00:00Z
ending - 2020-12-21T12:00:00Z
Shift:
starting - 2020-12-18T09:00:00Z
ending - 2020-12-18T17:00:00Z
Shift:
starting - 2020-12-17T09:00:00Z
ending - 2020-12-17T17:00:00Z
Shift:
starting - 2020-12-15T09:00:00Z
ending - 2020-12-15T17:00:00Z
Shift:
starting - 2020-12-14T13:00:00Z
ending - 2020-12-14T17:00:00Z
Shift:
starting - 2020-12-14T09:00:00Z
ending - 2020-12-14T12:00:00Z
Shift:
starting - 2020-12-11T09:00:00Z
ending - 2020-12-11T17:00:00Z
Shift:
starting - 2020-12-10T09:00:00Z
ending - 2020-12-10T17:00:00Z
Shift:
starting - 2020-12-08T09:00:00Z
ending - 2020-12-08T17:00:00Z
Shift:
starting - 2020-12-07T13:00:00Z
ending - 2020-12-07T17:00:00Z
Shift:
starting - 2020-12-07T09:00:00Z
ending - 2020-12-07T12:00:00Z
Shift:
starting - 2020-12-04T09:00:00Z
ending - 2020-12-04T17:00:00Z
Shift:
starting - 2020-12-03T09:00:00Z
ending - 2020-12-03T17:00:00Z
Shift:
starting - 2020-12-01T09:00:00Z
ending - 2020-12-01T17:00:00Z
Shift:
starting - 2020-11-30T13:00:00Z
ending - 2020-11-30T17:00:00Z
Shift:
starting - 2020-11-30T09:00:00Z
ending - 2020-11-30T12:00:00Z
Shift:
starting - 2020-11-27T09:00:00Z
ending - 2020-11-27T17:00:00Z
Shift:
starting - 2020-11-26T09:00:00Z
ending - 2020-11-26T17:00:00Z
Shift:
starting - 2020-11-24T09:00:00Z
ending - 2020-11-24T17:00:00Z
Shift:
starting - 2020-11-23T13:00:00Z
ending - 2020-11-23T17:00:00Z
Shift:
starting - 2020-11-23T09:00:00Z
ending - 2020-11-23T12:00:00Z

This is common loop:

for ($i = 0; $i < count($array); $i++) {
    echo $array[$i]['id'];
    echo $array[$i]['from_day'];
    ...
}

This one maybe you want:

for ($i = 0; $i < asYouLike; $i++) {
    
    $j = $i % count($array);           // this is the trick
    
    echo $array[$j]['id'];             //note use j for index
    echo $array[$j]['from_day'];       // j for index
    ...
}
Related