【问题标题】:Loop back from today to generate a list of shifts/rota from a set template从今天开始循环以从一组模板生成班次/轮班列表
【发布时间】:2021-05-14 10:22:17
【问题描述】:

我有一组工作时间,并且想要生成一个建议轮班列表,所有这些都基于当前的“现在”时间并回溯到过去。

例如小时模板:

$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",
    ]
]);

如果今天是星期四(1 月 4 日),它应该以相反的顺序列出日期:

  • 周四 09:00 - 17:00(1 月 4 日)
  • 周二 09:00 - 17:00(1 月 2 日)
  • 周一 13:00 - 17:00(1 月 1 日)
  • 周一 09:00 - 12:00(1 月 1 日)
  • 周五 09:00 - 17:00(12 月 31 日)
  • 周四 09:00 - 17:00(12 月 30 日)

这段代码感觉很糟糕,但我找不到在不复制代码的情况下重复收集循环的方法。

// 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
        ]);
    });
}

【问题讨论】:

  • 我没看错,这是一个可行的解决方案,而您正在寻求改进?
  • 是的,我已经用我的最新编辑更新了问题,我认为它工作正常。
  • 如果您只想重构/优化工作代码,Code Review 将是更好的选择。
  • 如果这是我,并且 hours 模板对象是轻量级的,我会在一个循环中将 $templateShifts 复制到一个巨大的数组中,直到它至少包含 $total_count 项,然后调用 array_slice at最后确保它的大小正好是$total_count。根据您的样本数据和快速测试,这会占用 35KB 的内存,完全值得。然后你只需应用标准的分页逻辑。如果 hours 模板不是轻量级的,我仍然可能会创建一个 DTO 并这样做,但我也讨厌使用基于日期的逻辑,并且愿意尽可能作弊。

标签: php laravel


【解决方案1】:

首先,您的预期数据似乎有误,因为 1 月 4 日不是星期四(我的生日实际上是星期一)。

这是一个棘手的问题,因为要解决多个问题,我将把它分解为 3 个阶段。

  • 循环数周而不是数天或轮班。
  • 将您的班次数据映射到给定的工作日。
  • 计算距离以找到最接近终点的位移 我们正在循环播放的一周。

第一部分,我的想法是每周循环,找出轮班并减去我们要迭代到下周的日期。这是通过以下代码获得的。减去星期与 Carbon 方法一起使用 -&gt;subWeek()-&gt;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);
}

suggestShifts() 方法如下所示。将所有班次映射到当前周,如果班次大于我们要比较的日期以忽略它,这仅在第一次通过时是正确的。设置轮班日期并计算与我们正在比较的日期的差异,这将被排序并确保建议的下一个班次是当前日期的壁橱。

我们正在处理对象,注意对象是通过引用进行的,因此我使用一个克隆来避免覆盖相同的班次。同时还克隆了日期,以避免一遍又一遍地更改相同的日期。

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();
}

我不确定这是最快的方法,但它的代码相当干净易读,应该能让你朝着正确的方向前进。也不是建议是附有日期的班次对象。我的日期比较是使用直到时间,如果你想改变开始时间,你应该很容易做到这一点。此代码在本地执行,结果如下。像这样打印出来。再次注意,1 月 4 日是星期一。

这段代码也返回 32 个元素,我不觉得数量是问题的核心,当你弄清楚使用这种方法的方式时,你会拼接数组。 $this-&gt;shifts 是您粘贴在属性上的班次结构。

$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

【讨论】:

  • 有多种解决方案可以改善这一点,仅在第一次迭代时运行大于等于检查。排序方式总是相同的,所以我们不必对其进行排序等。也许我会这样做,但我再次觉得这个问题更多是关于清理和如何在相同算法上循环相同数据的方法现在:)
【解决方案2】:

这是常见的循环:

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

也许你想要这个:

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
    ...
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2023-04-10
    • 2015-03-04
    • 2018-11-03
    • 1970-01-01
    • 2021-12-12
    • 1970-01-01
    • 2019-03-20
    • 1970-01-01
    相关资源
    最近更新 更多