Генерировать случайные даты со случайным временем между двумя датами для выбранного периода и частоты

#php #algorithm #date #datetime #email

#php #алгоритм #Дата #дата и время #Адрес электронной почты

Вопрос:

Мне нужно создать компонент планирования, который будет планировать электронные письма, которые необходимо отправить. Пользователи могут выбирать время начала, время окончания и частоту. Код должен генерировать случайный момент для каждой частоты, между временем начала и окончания. В нерабочее время.

Параметры:

Пользователь может выбрать период между 01/01/2020 (начало) и 01/01/2021 (конец). В этом случае пользователь выбирает интервал времени ровно в один год.

Пользователь может выбрать частоту. В этом случае пользователь выбирает «2 месяца».

Функция:

Код создает список дат. Общее время (один год) делится на частоту (2 месяца). Мы ожидаем список из 6 дат.

Каждое время даты является случайным моментом в указанной частоте (2 месяца). В рабочее время.

Результат:

Примерный результат для этих параметров может быть следующим, с рассчитанными частотными границами для наглядности:

  • [январь / февраль] 21-02-2020 11.36
  • [mrt / apr] 04-03-2020 16.11
  • [мэй / июнь] 13-05-2020 09.49
  • [июль-август] 14-07-2020 15.25
  • [сентябрь-окт] 02-09-2020 14.09
  • [ноябрь-декабрь] 25-12-2020 13.55

Я думал о том, как реализовать это наилучшим образом, но я не могу найти элегантного решения.

Как это можно сделать с помощью PHP?

Любые идеи, ссылки или пики кода будут высоко оценены. Я действительно застрял на этом.

Ответ №1:

Я думаю, вы просто просите предложения о том, как сгенерировать список повторяющихся (2 еженедельных) дат со случайным временем, скажем, с 9 утра до 5 вечера? Это правильно?

Если это так — что-то вроде этого (непроверенный, псевдокод) может быть отправной точкой:

 $start    = new Datetime('1st January 2021');
$end      = new Datetime('1st July 2021');
$day_start = 9;
$day_end = 17;

$date = $start;
$dates = [$date]; // Start date into array
while($date < $end) {
            
    $new_date = clone($date->modify("  2 weeks"));
    $new_date->setTime(mt_rand($day_start, $day_end), mt_rand(0, 59));
        
    $dates[] = $new_date;
}

var_dump($dates);
 

Комментарии:

1. Это не дает желаемых результатов. Он выбирает случайное время каждые 2 недели с даты начала. Я ищу случайный день и время в течение этих двух недель. Я обновлю исходный вопрос. Спасибо за ваше время, оно ценится.

Ответ №2:

Ответ Стива кажется хорошим, но вы должны рассмотреть 2 дополнительные вещи

  1. проверка праздничных дней в while после первой строки $new_date, например:
 $holiday = array('2021-01-01',    '2021-01-06', '2021-12-25');
if (!in_array($new_date,$holiday))
 
  1. также проверьте, является ли дата рабочим днем или выходным, аналогично приведенному выше, с рабочими днями в виде массива.

Комментарии:

1. Хорошее предложение — но нужно быть осторожным при выполнении проверки in_array, поскольку $new_date будет объектом DateTime, а не строкой. Может быть связано с if(!in_array($new_date->format(‘Y-m-d’), $holiday)) хотя.

2. Умно с вашей стороны указать праздники. Я посмотрю, как я буду с этим справляться. Пожалуйста, ознакомьтесь с обновленным вопросом, если хотите, я лучше объяснил, что я имею в виду под частотой.

3. в наши дни вы должны делать это как объект — это факт. на данный момент я имею в виду только математический расчетный метод с некоторым случайным измененным временем. Некоторое время назад у меня была аналогичная проблема с праздничными днями для управления временем в моем приложении. Но сделал это без объекта и простого массива.

Ответ №3:

Это довольно дерьмовый код, но я думаю, что он будет работать так, как вы пожелаете.

 function getDiffInSeconds(DateTime $start, DateTime $end) : int
{
    $startTimestamp = $start->getTimestamp();
    $endTimestamp = $end->getTimestamp();
    
    return $endTimestamp - $startTimestamp;
}

function getShiftData(DateTime $start, DateTime $end) : array
{
    $shiftStartHour = DateTime::createFromFormat('H:i:s', $start->format('H:i:s'));
    $shiftEndHour = DateTime::createFromFormat('H:i:s', $end->format('H:i:s'));
    
    $shiftInSeconds = intval($shiftEndHour->getTimestamp() - $shiftStartHour->getTimestamp());
    
    return [
        $shiftStartHour,
        $shiftEndHour,
        $shiftInSeconds,
    ];
}

function dayIsWeekendOrHoliday(DateTime $date, array $holidays = []) : bool
{
    $weekendDayIndexes = [
        0 => 'Sunday',
        6 => 'Saturday',
    ];
    
    $dayOfWeek = $date->format('w');
    if (empty($holidays)) {
        $dayIsWeekendOrHoliday = isset($weekendDayIndexes[$dayOfWeek]);
    } else {
        $dayMonthDate = $date->format('d/m');
        $dayMonthYearDate = $date->format('d/m/Y');
        $dayIsWeekendOrHoliday = (isset($weekendDayIndexes[$dayOfWeek]) || isset($holidays[$dayMonthDate]) || isset($holidays[$dayMonthYearDate]));
    }
    
    return $dayIsWeekendOrHoliday;
}

function getScheduleDates(DateTime $start, DateTime $end, int $frequencyInSeconds) : array
{
    if ($frequencyInSeconds < (24 * 60 * 60)) {
        throw new InvalidArgumentException('Frequency must be bigger than one day');
    }
    
    $diffInSeconds = getDiffInSeconds($start, $end);
    
    // If difference between $start and $end is bigger than two days
    if ($diffInSeconds > (2 * 24 * 60 * 60)) {
        // If difference is bigger than 2 days we add 1 day to start and subtract 1 day from end
        $start->modify(' 1 day');
        $end->modify('-1 day');
        
        // Getting new $diffInSeconds after $start and $end changes
        $diffInSeconds = getDiffInSeconds($start, $end);
    }
    
    if ($frequencyInSeconds > $diffInSeconds) {
        throw new InvalidArgumentException('Frequency is bigger than difference between dates');
    }
    
    $holidays = [
        '01/01'         => 'New Year',
        '18/04/2020'    => 'Easter 1st official holiday because 19/04/2020',
        '20/04/2020'    => 'Easter',
        '21/04/2020'    => 'Easter 2nd day',
        '27/04'         => 'Konings',
        '04/05'         =>  '4mei',
        '05/05'         =>  '4mei',
        '24/12'         => 'Christmas 1st day',
        '25/12'         => 'Christmas 2nd day',
        '26/12'         => 'Christmas 3nd day',
        '27/12'         => 'Christmas 3rd day',
        '31/12'         => 'Old Year'
    ];
    
    [$shiftStartHour, $shiftEndHour, $shiftInSeconds] = getShiftData($start, $end);
    $amountOfNotifications = floor($diffInSeconds / $frequencyInSeconds);
    $periodInSeconds = intval($diffInSeconds / $amountOfNotifications);
    $maxDaysBetweenNotifications = intval($periodInSeconds / (24 * 60 * 60));
    // If $maxDaysBetweenNotifications is equals to 1 then we have to change $periodInSeconds to amount of seconds for one day
    if ($maxDaysBetweenNotifications === 1) {
        $periodInSeconds = (24 * 60 * 60);
    }
    
    $dates = [];
    for ($i = 0; $i < $amountOfNotifications; $i  ) {
        $periodStart = clone $start;
        $periodStart->setTimestamp($start->getTimestamp()   ($i * $periodInSeconds));
        $seconds = mt_rand(0, $shiftInSeconds);
        
        // If $maxDaysBetweenNotifications is equals to 1 then we have to check only one day without loop through the dates
        if ($maxDaysBetweenNotifications === 1) {
            $interval = new DateInterval('P' . $maxDaysBetweenNotifications . 'DT' . $seconds . 'S');
            $date = clone $periodStart;
            $date->add($interval);
            
            $dayIsWeekendOrHoliday = dayIsWeekendOrHoliday($date, $holidays);
        } else {
            // When $maxDaysBetweenNotifications we have to loop through the dates to pick them
            $loopsCount = 0;
            $maxLoops = 3; // Max loops before breaking and skipping the period
            do {
                $day = mt_rand(0, $maxDaysBetweenNotifications);
                $periodStart->modify($shiftStartHour);
                $interval = new DateInterval('P' . $day . 'DT' . $seconds . 'S');
                $date = clone $periodStart;
                $date->add($interval);
                
                $dayIsWeekendOrHoliday = dayIsWeekendOrHoliday($date, $holidays);
                
                // If the day is weekend or holiday then we have to increment $loopsCount by 1 for each loop
                if ($dayIsWeekendOrHoliday === true) {
                    $loopsCount  ;
                    
                    // If $loopsCount is equals to $maxLoops then we have to break the loop
                    if ($loopsCount === $maxLoops) {
                        break;
                    }
                }
            } while ($dayIsWeekendOrHoliday);
        }
        
        // Adds the date to $dates only if the day is not a weekend day and holiday
        if ($dayIsWeekendOrHoliday === false) {
            $dates[] = $date;
        }
    }
    
    return $dates;
}

$start = new DateTime('2020-12-30 08:00:00', new DateTimeZone('Europe/Sofia'));
$end = new DateTime('2021-01-18 17:00:00', new DateTimeZone('Europe/Sofia'));
$frequencyInSeconds = 86400; // 1 day

$dates = getScheduleDates($start, $end, $frequencyInSeconds);
var_dump($dates);
 

Вы должны пройти $start , $end и $frequencyInSeconds , как я показал в примере, и тогда вы получите свои случайные даты. Обратите внимание, что у меня $start и $end должны быть часы в них, потому что они используются в качестве начальных и конечных часов для смен. Потому что правило должно возвращать дату в течение времени смены только в рабочие дни. Также вы должны указать частоту в секундах — вы можете вычислить их вне функции или изменить ее, чтобы вычислить их внутри. Я сделал это так, потому что я не знаю, каковы ваши предопределенные периоды.

Эта функция возвращает массив экземпляров DateTime(), чтобы вы могли делать с ними все, что захотите.

ОБНОВЛЕНИЕ 01.08.2020: праздничные дни теперь являются частью вычисления, и они будут исключены из возвращаемых дат, если они передаются при вызове функции. Вы можете передавать их в d/m d/m/Y форматах и из-за праздников, таких как Пасха, и в случае, когда праздник приходится на выходные, но люди получат дополнительный выходной в течение рабочей недели.

ОБНОВЛЕНИЕ 13/01/2020: я создал обновленную версию кода, чтобы устранить проблему с бесконечными циклами, когда $frequencyInSeconds они короче, чем 1 день. В новом коде использовалось несколько функций getDiffInSeconds , getShiftData а dayIsWeekendOrHoliday также вспомогательные методы для уменьшения дублирования кода и создания более чистого и читаемого кода

Комментарии:

1. Супер! Мы приближаемся! Как это связано с 28/30/31-дневными месяцами и праздниками? Спасибо за ваше время и усилия!

2. Я не думаю, что с этим есть проблемы — сначала около 28/30/31 — как вы можете видеть здесь $interval = new DateInterval('P' . $day . 'DT' . $seconds . 'S'); , я установил интервал в дни и секунды, которые добавляются к дате клонированного периода здесь $date->add($interval); с помощью php DateTime класс будет обрабатывать точную дату, и это будет 100% действительная дата 🙂 О праздниках — видите ли вы подобное требование в своем вопросе? Позже я обновлю свой пост, добавив возможность исключать праздники из сгенерированных дат.

3. @user2863494 добавлена обработка праздников, и мой ответ был обновлен

4. Спасибо за ваше время и усилия. Это выглядит хорошо! Я наградил награду за вашу работу. Я проведу дальнейшее тестирование с несколькими входными параметрами и, соответственно, приму ваш ответ как правильный.

5. @user2863494 пожалуйста, отметьте ответ как «принятый ответ»