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
}
Related
I hope someone can help with this, i think perhaps the issue is i am overwriting the DateTime value in my second for loop, because its not outputting correct values, but not entirely sure.
<?php
$begin_from = new DateTime( "2023-01-01" );
$end_from = new DateTime( "2023-12-31" );
$begin_to = new DateTime( "2023-01-31" );
$end_to = new DateTime( "2023-12-31" );
for($i = $begin_from; $i <= $end_from; $i->modify('+1 month')){
for($k = $begin_to; $k <= $end_to; $k->modify('first day of')->modify('+1 month')->modify('last day of')){
echo $i->format("Y-m-d"),'..',$k->format("Y-m-d");
echo "\n";
}
}
From the code above its outputting this:
2023-01-01..2023-01-31
2023-01-01..2023-02-28
2023-01-01..2023-03-31
2023-01-01..2023-04-30
2023-01-01..2023-05-31
2023-01-01..2023-06-30
2023-01-01..2023-07-31
2023-01-01..2023-08-31
2023-01-01..2023-09-30
2023-01-01..2023-10-31
2023-01-01..2023-11-30
2023-01-01..2023-12-31
But, if you run these for loops separately you will get the correct values like below.
2023-01-01..2023-01-31
2023-02-01..2023-02-28
2023-03-01..2023-03-31
2023-04-01..2023-04-30
2023-05-01..2023-05-31
2023-06-01..2023-06-30
2023-07-01..2023-07-31
2023-08-01..2023-08-31
2023-09-01..2023-09-30
2023-10-01..2023-10-31
2023-11-01..2023-11-30
2023-12-01..2023-12-31
Can anyone tell what i am doing wrong here?
Mutable objects are really hard to reason about. In particular, you need to be aware that $bar = $foo makes $bar point at the same object as $foo, not at a new object with the same value.
With that in mind, let's "unroll" your loops (never write code like this!):
$begin_from = new DateTime( "2023-01-01" );
$end_from = new DateTime( "2023-12-31" );
$begin_to = new DateTime( "2023-01-31" );
$end_to = new DateTime( "2023-12-31" );
$i = $begin_from;
outerforloop:
if ( $i <= $end_from ) {
$k = $begin_to;
innerforloop:
if ( $k <= $end_to ) {
echo $i->format("Y-m-d"),'..',$k->format("Y-m-d");
echo "\n";
$k->modify('first day of')->modify('+1 month')->modify('last day of');
goto innerforloop;
}
$i->modify('+1 month');
goto outerforloop;
}
In particular, look at the start and end of the inner loop:
$k = $begin_to; will make $k point at the same object as $begin_to
$k->modify(...) will then modify that object, meaning both $k and $begin_to have moved forward by a month
Next time around the loop, we check $k <= $end_to, with the expected result
However, when we come back around the outer loop, we run $k = $begin_to; again, expecting this to reset the value; but $k and $begin_to already point at the same object, which has been modified; the assignment doesn't do anything
So now when we check $k <= $end_to, it will already be false: we won't go into the loop at all
To actually copy the value of object, you can use the clone keyword, e.g. $k = clone $begin_to;
However, this particular case is why the DateTimeImmutable class was created. With DateTimeImmutable, you never change the value of an existing object, and instead always assign the result somewhere. In short, replace $i->modify(...) with $i = $i->modify(...) and $k->modify(...) with $k = $k->modify(...):
$begin_from = new DateTimeImmutable( "2023-01-01" );
$end_from = new DateTimeImmutable( "2023-12-31" );
$begin_to = new DateTimeImmutable( "2023-01-31" );
$end_to = new DateTimeImmutable( "2023-12-31" );
for($i = $begin_from; $i <= $end_from; $i = $i->modify('+1 month')){
for($k = $begin_to; $k <= $end_to; $k = $k->modify('first day of')->modify('+1 month')->modify('last day of')){
echo $i->format("Y-m-d"),'..',$k->format("Y-m-d");
echo "\n";
}
}
That fixes your for loops ... but it doesn't give the results you wanted. That's because if you have two nested loops with 12 iterations each, the result is going to be 144 iterations - think of filling out a grid with 12 columns and 12 rows.
What you actually wanted was a single loop, which controls both the start and the end date. There are a few ways to write that, but the most similar to your existing code is probably to keep the loop for $i, then define $k based on it:
$begin_from = new DateTimeImmutable( "2023-01-01" );
$end_from = new DateTimeImmutable( "2023-12-31" );
for($i = $begin_from; $i <= $end_from; $i = $i->modify('+1 month')){
$k = $i->modify('last day of');
echo $i->format("Y-m-d"),'..',$k->format("Y-m-d");
echo "\n";
}
I'm not sure why your code produces that output, but it can be simplified using a while loop. You just need one date to manually modify within each loop, and one target date (DateTimeImmutable is recommended for datetimes that are meant to remain unchanged)
$i_date = new DateTime( "2023-01-01" );
$end_date = new DateTimeImmutable( "2023-12-31" );
while($i_date <= $end_date){
// echo first of month..
echo $i_date->format("Y-m-d..");
// goto last of month
$i_date->modify('last day of');
// echo last of month \n
echo $i_date->format("Y-m-d") . "\n";
// goto first of next month for the next iteration
$i_date->modify('first day of')->modify('+1 month');
}
The above will produce the following in PHP v7+:
2023-01-01..2023-01-31
2023-02-01..2023-02-28
2023-03-01..2023-03-31
2023-04-01..2023-04-30
2023-05-01..2023-05-31
2023-06-01..2023-06-30
2023-07-01..2023-07-31
2023-08-01..2023-08-31
2023-09-01..2023-09-30
2023-10-01..2023-10-31
2023-11-01..2023-11-30
2023-12-01..2023-12-31
PHP v5.6.40 requires you set the timezone or the default timezone like date_default_timezone_set('UTC'); before creating the DateTimes, but then you get the same output as above.
Run it live here.
To use datetime objects in a loop like #ArleighHix's answer, I'd only modify the start date and use a calls of min() and max() in conjunction with 01 and t to ensure that date boundaries are not exceeded.
max() and min() are safe in this case because Y-m-d is a "big-endian" date format -- this means that the string can be evaluated as a simple string.
Code: (Demo)
$start_date = new DateTime("2023-01-03");
$end_date = new DateTime("2023-12-15");
while ($start_date <= $end_date) {
printf(
"%s..%s\n",
max($start_date->format("Y-m-01"), $start_date->format("Y-m-d")),
min($end_date->format("Y-m-d"), $start_date->format("Y-m-t"))
);
$start_date->modify('+1 month first day of');
}
If your actual project requirement is to have non-full date ranges each month, then a refactored approach would be necessary. Please provide a more challenging example by editing your question if this is a real concern/possibility.
If your main goal is to output the start and end of each month within a given date range, inclusive of the end month, you can simplify this down to:
$from = new DateTime("2023-01-01");
$to = new DateTime("2023-12-31");
while ($from <= $to) {
echo $from->format("Y-m-01") . ".." . $from->format("Y-m-t") . "\n";
$from->add(new DateInterval("P1M"));
}
If you need to respect the $from and $to date, that is; if you need first date range to start at $from and the last date range to end at $to, you can adjust the above code slightly using the max() and min() functions:
$from = new DateTime("2023-01-05");
$to = new DateTime("2023-12-25");
$current = new DateTime($from->format("Y-m-01"));
while ($current <= $to) {
echo max($from->format("Y-m-d"), $current->format("Y-m-01")) . ".." . min($to->format("Y-m-d"), $current->format("Y-m-t")) . "\n";
$current->add(new DateInterval("P1M"));
}
How can I count occurrences of 14th of a month between two dates
For example between 07.05.2018 and 04.07.2018
I have 2 occurrences of the 14th
Try this. Note that I've changed your date format, but you can just do a createFromFormat if you're really keen on your own format.
$startDate = new DateTime('2018-05-07');
$endDate = new DateTime('2018-07-04');
$dateInterval = new DateInterval('P1D');
$datePeriod = new DatePeriod($startDate, $dateInterval, $endDate);
$fourteenths = [];
foreach ($datePeriod as $dt) {
if ($dt->format('d') == '14') { // Note this is loosely checked!
$fourteenths[] = $dt->format('Y-m-d');
}
}
echo count($fourteenths) . PHP_EOL;
var_dump($fourteenths);
See it in action here: https://3v4l.org/vPZZ0
EDIT
This is probably not an optimal solution as you loop through every day in the date period and check whether it's the fourteenth. Probably easier is to modify the start date up to the next 14th and then check with an interval of P1M.
You don't need to loop at all.
Here's a solution that does not loop at all and uses the less memory and performance hungry date opposed to DateTime.
$start = "2018-05-07";
$end = "2018-07-04";
$times = 0;
// Check if first and last month in the range has a 14th.
if(date("d", strtotime($start)) <= 14) $times++;
if(date("d", strtotime($end)) >= 14) $times++;
// Create an array with the months between start and end
$months = range(strtotime($start . "+1 month"), strtotime($end . "-1 month"), 86400*30);
// Add the count of the months
$times += count($months);
echo $times; // 2
https://3v4l.org/RevLg
I'm working on a website where the user can create some events every X days (where X is the name of a day in the week). Then, he needs to enter the number of events he wants to create in the future.
For example, the user selects every Monday and Tuesday and decides to create 150 events.
Here is the code I have made until now :
// Init the date counter
$cpt_date_found = 0;
// Number of date to find
$rec_occ = 150;
// Init an ending date far in the future
$endDate = strtotime('+10 years', time());
// Loop over the weeks
for($i = strtotime('Monday', strtotime(date("d.m.Y"))); $i <= $endDate; $i = strtotime('+1 week', $i)) {
// -- Monday date found, create the event in the database
$cpt_date_found++;
// Break the loop if we have enough dates found
if($cpt_date_found == $rec_occ) {
break;
}
}
This code finds the date of every Monday in the future and breaks the loop once we have reached the number of occurrences the user specified.
I have entered an ending date far in the future to make sure I can break the loop before the end of the occurrences count specified by the user.
First I'm not sure about the "quality" of my code... I know that breaking the loop is not the best idea and am wondering if another solution would better fit my needs.
Then, instead of repeating the loop more times if the user specified several days (let's say, Monday, Tuesday and Friday), is there a way to loop one time for every provided days?
Thanks!
The following code will loop over a period of 5 years. For each week in those 5 years it will generate a DatePeriod containing each day of that week. It will compare each of those days to your preset array with days you are looking for. You can then generate your event after which the code will countdown for a certain amount of times. If the counter hits zero, you are done.
$searchDates = array('Mon', 'Tue', 'Fri');
$amountOfTimes = 27;
$startDate = new DateTime();
$endDate = new DateTime('next monday');
$endDate->modify('+5 years');
$interval = new DateInterval('P1W');
$dateRange = new DatePeriod($startDate, $interval ,$endDate);
// Loop through the weeks
foreach ($dateRange as $weekStart) {
$weekEnd = clone $weekStart;
$weekEnd->modify('+6 days');
$subInterval = new DateInterval('P1D');
// Generate a DatePeriod for the current week
$subRange = new DatePeriod($weekStart, $subInterval ,$weekEnd);
foreach ($subRange as $weekday) {
if (in_array($weekday, array('Mon', 'Fri', 'Sun'))) {
// Create event
// Countdown
$amountOfTimes--;
}
if ($amountOfTimes == 0) {
break;
}
}
}
I run an automated process via PHP cron job every day which generates a report for a number of months. The date generation section of the code is below with a bit of hard coding for simplicity.
On the last day of the month it appears to repeat the months; i.e., rather than a series of from-to date pairs that make sense, I get the same date pairs repeated. All I can think of is that I'm doing something in the start and end count calculations that's only an issue on the last day of a month.
Results expected are:
From 2013-10-01 to 2013-10-31
From 2013-11-01 to 2013-11-30
...
From 2016-09-01 to 2016-09-30
The results I get when running the report on the last day of the month are a bit random. Here are the dates produced on 31/05 (note that only the TO date is displayed for simplicity. I know the FROM date is the first of the relevant month because the report data is correct).
31/10/2013
31/10/2013
31/12/2013
31/12/2013
31/01/2014
31/03/2014
31/03/2014
31/05/2014
31/05/2014
31/07/2014
31/07/2014
31/08/2014
31/10/2014
31/10/2014
31/12/2014
31/12/2014
31/01/2015
31/03/2015
31/03/2015
31/05/2015
31/05/2015
31/07/2015
31/07/2015
31/08/2015
31/10/2015
31/10/2015
31/12/2015
31/12/2015
31/01/2016
31/03/2016
31/03/2016
31/05/2016
31/05/2016
31/07/2016
31/07/2016
31/08/2016
31/10/2016
Here's the code:
<?php
$reportBegin = new DateTime("2013-10-01"); // start of report
$reportEnd = new DateTime("2016-09-01"); // end of report
$nowRef = new DateTime();
$startCount = (($nowRef->diff($reportBegin)->m) + ($nowRef->diff($reportBegin)->y*12)) * -1; // int * -1 to make neg
$endCount = (($nowRef->diff($reportEnd)->m) + ($nowRef->diff($reportEnd) ->y*12)) + 1; // int and add 1
$count = $startCount;
// do all months
while($count <= $endCount){
$from = date('Y-m-1', strtotime("$count months"));
$to = date('Y-m-t', strtotime($from));
print ("From $from to $to<br />");
$count++;
} // done all months
?>
Can anyone give me a steer? I'm not really sure how to test it.
Edit: The reason I'm mixing DateTime() and date() is because in production, there's a section of code that in some environments replaces the DateTime() section. This replacement code sets $startCount and $endCount as integers.
Here's a PHP5.3 version of the answer in my comment:
<?php
$reportBegin = new DateTime("2013-10-01"); // start of report
$reportEnd = new DateTime("2016-09-01");
$reportEnd->add(new DateInterval('P1M')); // end of report
$interval = new DateInterval('P1M');
$period = new DatePeriod($reportBegin, $interval, $reportEnd);
foreach ($period as $month) {
printf("From %s to %s<br />",
$month->format('Y-m-01'),
$month->format('Y-m-t')
);
}
?>
Demo
PHP 5.4+
<?php
$reportBegin = new DateTime("2013-10-01"); // start of report
$reportEnd = (new DateTime("2016-09-01"))->add(new DateInterval('P1M')); // end of report
$interval = new DateInterval('P1M');
$period = new DatePeriod($reportBegin, $interval, $reportEnd);
foreach ($period as $month) {
printf("From %s to %s<br />",
$month->format('Y-m-01'),
$month->format('Y-m-t')
);
}
?>
Demo
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.