I have an array of words and each word is valid from 5PM that day until 5PM the next day, then the next word is valid. Except on weekends in which a word for Friday lasts until Monday at 5PM.
Now what I am trying to do is determine if the users inputted word is valid for that period of time. I have a working example which works fine, but my problem is the weekends mess everything up. And I can't seem to figure out how to make them work.
I got a function to figure out how many weekends occur between two timestamps
// Figure out how many weekends occured between two dates
function weekends($date1, $date2) {
$d1 = new DateTime('#' . $date1);
$d2 = new DateTime('#' . $date2);
// Compare the two dates
$diff = $d1->diff($d2);
// Get number of days between them
$days = $diff->format('%a');
// Find out day of the week we start on
$start = $d1->format('w');
// Verify we are not on a weekend
if($start == 0 || $start == 6)
return false;
// Number of days until weekend
$until_weekend = 7 - $start; // (6 is Saturday but we are counting today)
// Find out how many days are left between the first weekend and the end
$left = $days - $until_weekend;
// How many weekends
$weekends = floor($left / 7) + 1;
return $weekends;
}
And then I got a function to determine if the word is valid for that date range
// Keyword Validation
function keyword_validate($keywords = array()) {
if(empty($keywords)) return false;
// Break into values
$keyword = $keywords['keyword'];
$contest = $keywords['contest'];
$keywords = $contest['keywords'];
// Get some dates
$now = new DateTime('now');
$start = new DateTime('#' . $contest['start_time']);
$s1 = new DateTime('#' . $contest['start_time']); // value for timestamps
$s2 = new DateTime('#' . $contest['end_time']); // value for timestamps
$end = new DateTime('#' . $contest['end_time']);
// Verify keyword exists
if(in_array($keyword, $keywords) === FALSE)
return false;
// Get index
$index = array_search($keyword, $keywords);
// See if we somehow got more then one keyword
if(count($index) != 1)
return false;
// get number of weekends
$weekends = weekends($start->getTimestamp(), $end->getTimestamp());
// Based on index get the two container timestamps
$s = $s1->add(new DateInterval('P' . $index + $weekends . 'D'));
// Verify start doesn't equal Friday or a Weekend
$e = $s2->add(new DateInterval('P' . $index + $weekends + 1 . 'D'));
if($s === FALSE || $e === FALSE)
return false; // Something really bad happened
// Based on dates find out if the keyword works.
print $s->getTimestamp();
print $e->getTimestamp();
// Get the current time
}
As you can seem the keyword function doesn't work atm. What I am doing atm is matching the index of the keyword to day, but if it is say Tuesday (2 weekends after) how can I make it so the index is increased by 4. Sorry if this doesn't make any sense, I'm a little lost.
Try redefining the problem to make it simpler. Instead of trying to do math with funny exceptions to figure out which item is associated with which day, perhaps try creating a new array with a value of the word for each day. It could look like this:
array(
'cake',
'pie',
'pudding',
'happy', //friday
'happy', //saturday
'happy', //sunday
'nothappy',
...
);
Building this array should be simpler than the things you're trying to do now. Once you have this array, checking should be trivial.
Could you not just have an array that can contain seven spaces? When the "friday" index is changed, set the "Saturday"/"Sunday" to mirror it.
Related
I have to create a scheduling component that will plan e-mails that need to be sent out. Users can select a start time, end time, and frequency. Code should produce a random moment for every frequency, between start and end time. Outside of office hours.
Paramaters:
User can select a period between 01/01/2020 (the start) and 01/01/2021 (the end). In this case user selects a timespan of one exactly year.
User can select a frequency. In this case user selects '2 months'.
Function:
Code produces a list of datetimes. The total time (one year) is divided by frequency (2 months). We expect a list of 6 datetimes.
Every datetime is a random moment in said frequency (2 months). Within office hours.
Result:
An example result for these paramaters might as follows, with the calculated frequency bounds for clarity:
[jan/feb] 21-02-2020 11.36
[mrt/apr] 04-03-2020 16.11
[mei/jun] 13-05-2020 09.49
[jul-aug] 14-07-2020 15.25
[sep-okt] 02-09-2020 14.09
[nov-dec] 25-12-2020 13.55
--
I've been thinking about how to implement this best, but I can't figure out an elegant solution.
How could one do this using PHP?
Any insights, references, or code spikes would be greatly appreciated. I'm really stuck on this one.
I think you're just asking for suggestions on how to generate a list of repeating (2 weekly) dates with a random time between say 9am and 5pm? Is that right?
If so - something like this (untested, pseudo code) might be a starting point:
$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);
Steve's anwser seems good, but you should consider 2 additional things
holiday check, in the while after first $new_date line, like:
$holiday = array('2021-01-01', '2021-01-06', '2021-12-25');
if (!in_array($new_date,$holiday))
also a check if date is a office day or a weekend in a similar way as above with working days as an array.
It's kind of crappy code but I think it will work as you wish.
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);
You have to pass $start, $end and $frequencyInSeconds as I showed in example and then you will get your random dates. Notice that I $start and $end must have hours in them because they are used as start and end hours for shifts. Because the rule is to return a date within a shift time only in working days. Also you have to provide frequency in seconds - you can calculate them outside the function or you can change it to calculate them inside. I did it this way because I don't know what are your predefined periods.
This function returns an array of \DateTime() instances so you can do whatever you want with them.
UPDATE 08/01/2020:
Holidays now are part of calculation and they will be excluded from returned dates if they are passed when you are calling the function. You can pass them in d/m and d/m/Y formats because of holidays like Easter and in case when the holiday is on weekend but people will get additional dayoff during the working week.
UPDATE 13/01/2020:
I've made updated code version to fix the issue with infinite loops when $frequencyInSeconds is shorter like 1 day. The new code used few functions getDiffInSeconds, getShiftData and dayIsWeekendOrHoliday as helper methods to reduce code duplication and cleaner and more readable code
I'm creating a function to check whether 2 dates equal a full month or part of a month.
I'm trying with this inside my if statement:
$result["from_date"] = $_GET["from_date"];
$result["to_date"] = $_GET["to_date"];
$start = strtotime($result["from_date"]);
$stop = strtotime($result["to_date"]);
$diff = ($stop - $start); //Diff in seconds
$timespan = ($diff / 86400) + 1;
echo 'timespan - '.$timespan.'<br>';
if(cal_days_in_month(CAL_GREGORIAN, ChangeDateFormat($result["from_date"], 'm'), ChangeDateFormat($result["from_date"], 'Y')) == $timespan) {
return true;
}
I've used the dates 2018-10-01 and 2018-10-31 which is returning the timespan as 31.041666666667 meaning its returning true in the above if.
I have also tried the dates 2018-08-01 and 2018-08-30 which does not return true, so thats working correctly.
I'ts just October that is playing up and I cannot work out why.
As I wrote in comments this is due to daylight savings.
You can use DateTime to calculate the number of days between to dates (and add one).
$result["from_date"] = "2018-10-01";
$result["to_date"] = "2018-10-31";
$start = new DateTime($result["from_date"]);
$stop = new DateTime($result["to_date"]);
$diff = $stop->diff($start)->format("%a") +1;
echo 'timespan - '.$diff.' days<br>';
I'm trying to create a filter, whereby if days (Monday, Tuesday etc) are NOT found in a list, I want that specific DateTime to be removed from my DatePeriod. This is so that I can say, I work Monday and Tuesday. If you find that the day is Thursday, continue out of this loop and don't include it.
However, I cannot seem to do this as when I iterate through a DatePeriod, I cannot unset anything, as it does not count it as an array. Is there a way to do this? The code can be found below:
//Get all the start and end dates of the holidays (This will be run iteratively)
$get_st_date = $row['h_s_date'];
$get_end_date = $row['h_e_date'];
//Convert them to approprariate format to be used with DatePeriod
$get_begin = new DateTime( "$get_st_date" );
$get_end = new DateTime( "$get_end_date");
//Add an extra day or else it will be omitted from the following process.
$get_end = $get_end->add(new DateInterval('P1D'));
//Count per day
$get_interval = DateInterval::createFromDateString('1 day');
$get_period = new DatePeriod($get_begin, $get_interval, $get_end);
//Iteration Count
$iter = 0;
foreach($get_period as $get_dt){
//Find if date is Saturday or Sunday. If it is, break that current loop.
$iter++;
$str_result = $get_dt->format('l');
if($str_result == "Saturday") {continue;}
elseif($str_result == "Sunday") {continue;}
elseif(!preg_match("($str_result)", $e_d_w_p_w)){
echo "<br>Don't count this day" . $str_result;
unset($get_period[$iter]);
continue;
}
Then close the end tags (I haven't included it here as I do some other stuff.
From the above code, I get the following error:
"Fatal error: Uncaught Error: Cannot use object of type DatePeriod as array"
Is there a workaround to this?
For Clarification: $e_d_w_p_w is "Employee Days Worked Per Week"
$e_d_w_p_w is formatted like so "Monday;Tuesday;" etc
The problem is that DatePeriod is not an array, just like the error says. It just has the properties required so as to make the list of days required, but it doesn't store them, so you can't unset() a specific day from the list.
What you can do to accomplish this is create a new array, and instead of removing the days that do not match the criteria from the DatePeriod, only add the days that do to this new array:
<?php
$get_st_date = "2017-09-01";
$get_end_date = "2017-09-20";
//Convert them to approprariate format to be used with DatePeriod
$get_begin = new DateTime( "$get_st_date" );
$get_end = new DateTime( "$get_end_date");
//Add an extra day or else it will be omitted from the following process.
$get_end = $get_end->add(new DateInterval('P1D'));
//Count per day
$get_interval = DateInterval::createFromDateString('1 day');
$get_period = new DatePeriod($get_begin, $get_interval, $get_end);
$e_d_w_p_w = "Monday;Tuesday;";
$workDays = [];
//Iteration Count
$iter = 0;
foreach($get_period as $get_dt) {
//Find if date is Saturday or Sunday. If it is, break that current loop.
$iter++;
$str_result = $get_dt->format('l');
if($str_result == "Saturday") {continue;}
elseif($str_result == "Sunday") {continue;}
elseif(preg_match("($str_result)", $e_d_w_p_w)){
$workDays[] = $get_dt;
}
}
var_dump($workDays);
Demo
Also, I think it might be a bit cleaner (and faster; avoid regular expressions whenever possible) to transform $e_d_w_p_w to an array and check if the current day is in that array:
$e_d_w_p_w = "Monday;Tuesday;";
$days = explode(";", $e_d_w_p_w); // transform to an array, separating by ;
array_pop($days); // remove the last element (assuming you always have a trailing ;
and then
elseif(in_array($str_result, $days)){
$workDays[] = $get_dt;
}
I was researching ways to iterate over certain days within a DatePeriod and Google led me here. I ended up writing a class for it - hopefully this helps the OP.
DatePeriod_Filter gist
To address the OP needs, you may use it like so:
$e_d_w_p_w = "Monday;Tuesday;";
//Add an extra day or else it will be omitted from the following process.
$filter = new DatePeriod_Filter(
new DateTime( '2017-09-01' ),
new DateInterval( 'P1D' ),
new DateTime( '2017-09-20 + 1 day' )
);
foreach( explode( ';', strtolower( trim( $e_d_w_p_w, ';' ) ) ) as $day )
$filter->$day();
foreach( $filter as $date )
{
// do something here
}
Can someone help me figuring out how to get if a current week is inside an occurrence .
I have the following vars : start_date , end_date , current_date week_occurrence .
and i have a function that return the # of occurrence
// will return the number of weeks between start - end
function get_weeks_count($start , $end) {
return floor(abs(strtotime($start) - strtotime($end)) / 604800);
}
now i have to know if a current date is a valid date .
I have an entry with occurrence = every N weeks . How to know that N is valid .
Less abstract : If we are in December and the occurrence is every 3 weeks , start_date is 1st and end_date is 30 December)
It will return :
TRUE for 1st week
FALSE for the second week
FALSE for the third week
TRUE for the last week
Here's how I would approach the problem - this applies for an occurrence every $n weeks.
$n = $week_occurrence;
$occurrence = false;
// To begin, get the number of weeks between the start and current dates.
$weeks = get_weeks_count($start_date , $current_date); // Using the function you already have
// Now check if $weeks == 0
if ($weeks == 0) {
$occurrence = true;
// If not, check if $weeks is divisible by $n without any remainder
} else if ($weeks % $n == 0) {
$occurrence = true;
}
If $occurrence is still false then the current week does not fall within the the correct occurrence, if it's true then the week does fall within the scope.
Effectively all we're doing here is checking that the current number of weeks since the start date is either equal to zero (we're still in the first week) or is divisible by the ocurrence without a remainder.
I hope this helps.
P.S. I've only answered the specific question that you asked. However, if you would like to know more about how this premiss could be used for scheduling etc., then feel free to ask and I'll expand on my answer accordingly
A combination of DateTime and DateInterval should help you achieve this easily.
function get_occurences(DateTime $start, DateTime $end, DateInterval $period) {
$weeks = array();
$cursor = clone $start;
$rate = DateInterval::createFromDateString('1 week');
do {
/* We can check to see if it's within the occurrence period */
if ($cursor == $start) {
$isOccurrence = true;
$start->add($period); // Move the start period up
} else {
$isOccurrence = false;
}
$weeks[$cursor->format('Y-m-d')] = $isOccurrence;
} while($cursor->add($rate) < $end);
return $weeks;
}
$period = DateInterval::createFromDateString('3 week');
$start = new DateTime('2012-12-01');
$end = new DateTime('2012-12-30');
/* From this array you can get both the number of occurrences as well as their respective dates*/
var_dump(get_occurences($start, $end, $period));
/** Output:
array(5) {
["2012-12-01"]=>
bool(true)
["2012-12-08"]=>
bool(false)
["2012-12-15"]=>
bool(false)
["2012-12-22"]=>
bool(true)
["2012-12-29"]=>
bool(false)
}
*/
I am developing a web application which revolves around dates.
I need to calculate numbers based around days elasped, for example - pseudo code
$count_only = array('monday', 'wednesday', 'friday'); //count only these days
$start_date = 1298572294; // a day in the past
$finish_date = 1314210695; //another day
$var = number_of_days_between($start_date, $finish_date, $count_only);
Is there a way determine how many full days have elapsed, while only counting certain days?
You can simplify this considerably by calculating how many complete weeks fall between the two specified dates, then do some math for the beginning/end partial weeks to account for dangling dates.
e.g.
$start_date = 1298572294; // Tuesday
$finish_date = 1314210695; // Wednesday
$diff = 1314210695-1298572294 = 15638401 -> ~181 days -> 25.8 weeks -> 25 full weeks.
Then it's just a simple matter of checking for the dangling dates:
Tuesday -> add 2 days for Wednesday+Friday to get to the end of the week
Wednesday -> add 1 day for Monday to get to the beginning on the week
Total countable days = (25 * 3) + 2 + 1 = 75 + 3 = 78 countable days
You could create a loop which goes to the next day in the $count_only array, from the $start_date and stopping (returning from the function) upon reaching the $end_date.
function number_of_days_between($start_date, $finish_date, $count_only) {
$count = 0;
$start = new DateTime("#$start_date");
$end = new DateTime("#$finish_date");
$days = new InfiniteIterator(new ArrayIterator($count_only));
foreach ($days as $day) {
$count++;
$start->modify("next $day");
if ($start > $end) {
return $count;
}
}
}
Of course there is a way :-)
The days that have been elapsed is simply
$elapsed_days = floor(($finish_date-$start_date) / 86400);
This will not get the result you need. What you could do is the following (pesudo)code:
$elapsed_days = floor(($finish_date-$start_date) / 86400);
for(int $i=0;$i<$elapsed_days;$i++){
$act_day_name = strtolower(date('l',$start_date+$i*86400));
if(in_array($act_day_name,$count_only){
// found matching day
}
}
What I do:
I iterate over every day which is between the both dates, get the day-name with date('l'); and check if it's within the array.
There may be some fine tuning need to be done, but this should get you going.
Just a bit faster approach than "iterating through all days":
$count_only = array(1, 3, 5); // days numbers from getdate() function
$start_date = 1298572294;
$finish_date = 1314210695;
function days($start_date, $finish_date, $count_only)
{
$cnt = 0;
// iterate over 7 days
for ($deltaDays = 0; $deltaDays < 7; $deltaDays++)
{
$rangeStart = $start_date + $deltaDays * 86400;
// check the weekday of rangeStart
$d = getDate($rangeStart);
if (in_array($d['wday'], $count_only))
{
$cnt += ceil(($finish_date - $rangeStart) / 604800);
}
}
return $cnt;
}
The idea is to count number of weeks using some additional offsets for mondays, tuesdays, wednesdays etc.