
![]() |
nutballs
I need to make a function that returns the date of a "complex" day.
example: 1st thursday of january 2008 2nd wednesday of feb 2008 last wednesday of march 2008 ugh... I am completely stumped. I've never actually gotten stumped on how the logic should work to do something in code so this is very aggravating. I can test a date to see if it meets the requirements, but that wont work for random requirements since I would have to test all the days in a month. though I guess as a last resort I can do that. just loop through them testing each date for matching the requirements. Now that I think about it, that may be the only way to do it? I also cant seem to figure out how to search for an answer in the disaster that is google. any suggestions? perkiset
Definitely loop it, but start from an anchor date.
IE: $theDate = mktime(0,0,0,2,1,2007); // Feb 1, 2007 ... loop till date('w', $theDate) == 3 (0==Sunday) (add 86400 to get the next day) Add 7 * 86400 to get the SECOND Wed in that month. Or start on first of NEXT month, minus one day then loop while dow != 0..7 Is that what you're looking for? (Sorry, moving fast and gotta run out and get a script for PinkHat, back in a bit) nutballs
I guess I was thinking there would be something out there already.
I will just loop forwards or backwards, depending on the requirements of the date setting. I thinks its just a "being dumb" issue for me. nutballs
Once I got my head out of my ass, it was a piece of cake. This is unrefined, and probably will be eventually refined, but for now it works, so I move on.
this is a bit specialized to what I am working on, but the idea is still good. I pass in $timeframe which is formatted like this: s2-3-8 translates into: s=start 2=2nd occurrence 3=wednesday, since I use ISO standard representation of the days of the week 1-7 8=august so.... 2nd wednesday of august. <? php$dArr = explode('-',$timeframe); if ($dArr[0]{0}=='s') { //start from begining of month $t=mktime(0,0,0,$dArr[2],1,date('Y',$time)); while (date('N', $t)!=$_POST['day']) { $t=DateAdd('d',1,$t); } $week=1; while ($week!=$dArr[0]{1}) { $week+=1; $t=DateAdd('w',1,$t); } if ($t < $time)//was in the past, do it again, but 1 year from now. { $t=mktime(0,0,0,$dArr[2],1,date('Y',DateAdd('y',1,$time))); while (date('N', $t)!=$_POST['day']) { $t=DateAdd('d',1,$t); } $week=1; while ($week!=$dArr[0]{1}) { $week+=1; $t=DateAdd('w',1,$t); } } } if ($dArr[0]{0}=='l') { //start from end of month $t=mktime(0,0,0,$dArr[2],1,date('Y',$time)); $t=DateAdd('m',1,$t); //push up 1 month, to be able to count down from $t=DateAdd('d',-1,$t); //subtract 1 day to get down to the last day of the correct month. while (date('N', $t)!=$_POST['day']) { $t=DateAdd('d',-1,$t); } $week=1; while ($week!=$dArr[0]{1}) { $week+=1; $t=DateAdd('w',-1,$t); } if ($t < $time) { $t=mktime(0,0,0,$dArr[2],1,date('Y',DateAdd('y',1,$time))); $t=DateAdd('m',1,$t); //push up 1 month, to be able to count down from $t=DateAdd('d',-1,$t); //subtract 1 day to get down to the last day of the correct month. while (date('N', $t)!=$_POST['day']) { $t=DateAdd('d',-1,$t); } $week=1; while ($week!=$dArr[0]{1}) { $week+=1; $t=DateAdd('w',-1,$t); } } } $nextdate = date('Y-m-d',$t); ?> DateAdd function (handles leapyear and other weirdness better than just adding seconds, in theory at least.) <? phpfunction DateAdd($interval, $number, $time) { switch ($interval) { case 'y': $timestamp = strtotime($number.' years', $time); break; case 'm': $timestamp = strtotime($number.' months', $time); break; case 'd': $timestamp = strtotime($number.' days', $time); break; case 'w': $timestamp = strtotime($number.' weeks', $time); break; case 'h': $timestamp = strtotime($number.' hours', $time); break; case 'n': $timestamp = strtotime($number.' minutes', $time); break; case 's': $timestamp = strtotime($number.' seconds', $time); break; } return $timestamp; } ?> perkiset
I got back and wanted to do this as well just as a test, so I put together these for consideration. This first function will return a date value based on the offset, dow of a month and year. For example, the second Monday of March 2007 would be specialDate(2, 1, 3, 2007). Inverse or "last dates" work as well - the last thursday of February 2008 would be specialDate(-1, 4, 2, 200
![]() php's date() function does)<? phpfunction specialDate($offset, $dow, $month, $year) { if (!$offset) { return false; } if (($dow < 0) or ($dow > 6)) { return false; } if (($month < 1) or ($month > 12)) { return false; } if (abs($offset) > 5) { return false; } $origMonth = $month; $aDay = 86400; // OK, on with the show: if the offset is < 0 then start with the month AFTER // the month passed to me, otherwise start with the first of the month passed to me... if ($offset < 0) { $month++; } if ($month == 13) { $month = 1; $year++; } // Make the anchor date - if we are going backwards, then subtract 1 // day to get to the last day of the correct month: $anchor = mktime(0, 0, 0, $month, 1, $year); if ($offset < 0) { $anchor -= $aDay; } // Move will be either +1 or -1 day in seconds... $move = ($offset < 0) ? -$aDay : $aDay; // Now find the first matching DOW... while (date('w', $anchor) <> $dow) { $anchor += $move; } // If offset is 1 then I'm done. if not, jump to the correct one... if (abs($offset) > 1) { // Move the offset towards zero by 1... if ($offset < 0) { $offset++; } else { $offset--; } $anchor += $move * 7; // Make sure that the offset has not pushed me out of the target month... if (date('m', $anchor) <> $origMonth) { return false; } } return $anchor; } This next one will evaluate a date against the special date for you, returning either a -1 (less than the target date) 0 (equal to) or +1 (greater than the target date): <? phpfunction evalSpecialDate($testDate, $offset, $dow, $month, $year) { // Strip the time out of the testdate... preg_match('/([0-9]{4})([0-9]{2})([0-9]{2})/', date('Ymd', $testDate), $parts); $testDate = mktime(0, 0, 0, $parts[2], $parts[1], $parts[3]); if (!$evalDate = specialDate($offset, $dow, $month, $year)) { return false; } if ($testDate < $evalDate) { return -1; } if ($testDate > $evalDate) { return 1; } return 0; } ?> Some examples: <? phpecho "The first Monday in March 2008 is: ", date('m/d/Y', specialDate(1, 1, 3, 200 ![]() echo "The second Friday in March 2008 is: ", date('m/d/Y', specialDate(2, 5, 3, 200 ![]() echo "The last Wednesday in March 2008 is: ", date('m/d/Y', specialDate(-1, 3, 3, 200 ![]() echo "The second to last Thursday in February 2008 is: ", date('m/d/Y', specialDate(-2, 4, 2, 200 ![]() ?> ![]() /p nutballs
cool perk
I like your method a bit better, I just "upgraded" it to use my dateadd function, plus compacted some stuff down (also I switched it back to ISO dayofweek ![]() The only other thing I was considering doing was changing if (date('m', $time) <> $month) { return false; } TO if (date('m', $time) <> $month) { $time = DateAdd('w',-1*$move,$time); } That would snap the date back to the correct month, rolled back or forward 1 week (opposite of your offset), getting the last or first occurrence of that day. It would make the function Non-Failing. The other option is to force a max offset of 4 instead of 5. since if you want the 5th monday of a month, only a few months will have a 5th monday. ALL have a 4th monday. What you really want probably is the LAST monday in that case. <? phpfunction specialDate($offset, $dayofweek, $month, $year) { if ((abs($offset) > 5) or ($offset == 0)) { return false; } if (($dayofweek < 1) or ($dayofweek > 7)) { return false; } if (($month < 1) or ($month > 12)) { return false; } //make a base time assume the first of the month $time = mktime(0,0,0,$month,1,$year); $move = 1; //1 DAY and WEEK offset //if we want the the day from the end of the month, //increase 1 month and then decrease 1 day. puts you on last day of month if ($offset < 0) { $time = DateAdd('m', 1, $time); $time = DateAdd('d', -1, $time); $move = -1; //going backward, so move is set to -1 DAY and WEEK } // Now find the first matching DayOFWeek... while (date('N', $time) <> $dayofweek) { $time = DateAdd('d',$move,$time); } $week=1; //start week counter while ($week < abs($offset)) //check if week counter is less than the offset of weeks requested { $week++; //increment week counter $time=DateAdd('w',$move,$time); //add a week // Make sure that the offset has not pushed me out of the target month... if (date('m', $time) <> $month) { return false; } } return $time; } ?> BTW i learned something from your code. I didnt know you could us "or" in an if/while, i thought it was only || LOLEdit:big typo.nutballs
Oh and as an addition
if you wanted to enforce a future date, just add a bool parameter to the function and recursively call the function increasing the year until the resulting date is in the future. last line in the function before the return. <? phpfunction specialDate($offset, $dayofweek, $month, $year, $forcefuture) { if ((abs($offset) > 5) or ($offset == 0)) { return false; } if (($dayofweek < 1) or ($dayofweek > 7)) { return false; } if (($month < 1) or ($month > 12)) { return false; } //make a base time assume the first of the month $time = mktime(0,0,0,$month,1,$year); $move = 1; //1 DAY and WEEK offset //if we want the the day from the end of the month, //increase 1 month and then decrease 1 day. puts you on last day of month if ($offset < 0) { $time = DateAdd('m', 1, $time); $time = DateAdd('d', -1, $time); $move = -1; //going backward, so move is set to -1 DAY and WEEK } // Now find the first matching DayOFWeek... while (date('N', $time) <> $dayofweek) { $time = DateAdd('d',$move,$time); } $week=1; //start week counter while ($week < abs($offset)) //check if week counter is less than the offset of weeks requested { $week++; //increment week counter $time=DateAdd('w',$move,$time); //add a week // Make sure that the offset has not pushed me out of the target month... roll back or forward a week to get back into month if (date('m', $time) <> $month) { $time = DateAdd('w',-1*$move,$time); } } //if we are forcing a future date, we recursively call this function with 1 year greater if (($forcefuture) and ($time < time())) {$time = specialDate($offset, $dayofweek, $month, $year+1, $forcefuture);} return $time; } ?> perkiset
Nice adds and tightens NBs... here's my next revision.
And BTW, I learned something here as well: the reference manual I keep by my desk does not have the 'N' parameter/format for DOW - it only as 'w' - I didn't know about the ISO setting - thanks!I do not like lots of function calls inside of function calls because it can just thrash the stack. As you can see in this example I use defines so that I can do simple math, which will be a lot quicker. Also, in refactoring I noticed that we are evaluating the sine of the offset too much, so by doing <that> work in the beginning I actually got a lot of performance enhancement out of it. Note how the offset is handled now with a single piece of simple arithmetic. Also I modified the forceFuture feature just a bit to ensure in a single pass that it would be done, rather than relying on the natural recursion that would have corrected it. I am also using exceptions rather than false for the failures at the beginning because that's really what I'd want... if I make such a bonehead move I'd like the process to halt hard. I also added the default value to forceFuture so that the processor won't pass back a warning if you forget that parameter. I also am not consistent with my sub-block syntax here because I wanted to code to be readable at the forum ![]() <? phpfunction specialDate($offset, $dayofweek, $month, $year, $forcefuture=false) { if ((abs($offset) > 5) or ($offset == 0)) throw new Exception('specialDate: Parameter 0 (offset) must be -4..-1 or 1..4'); if (($dayofweek < 1) or ($dayofweek > 7)) throw new Exception('specialDate: Parameter 1 (Day Of Week) must be 1..7'); if (($month < 1) or ($month > 12)) throw new Exception('specialDate: Parameter 2 (Month) must be 1..12'); if (!defined('dtDAY')) { define('dtDAY', 86400); define('dtWEEK', 604800); } if ($offset > 0) { // Make a base time the first of the target month $move = dtDAY; $time = mktime(0, 0, 0, $month, 1, $year); $offset = (--$offset * 7) * dtDAY; } else { // Make a base time the last of the target month $move = -dtDAY; $time = mktime(0, 0, 0, $month + 1, 1, $year) - dtDAY; $offset = (++$offset * 7) * dtDAY; } // Now find the first matching DayOfWeek... while (date('N', $time) <> $dayofweek) { $time += $move; } // Move the pointer by the offset week... $time += $offset; $newMonth = date('m', $time); if ($newMonth < $month) { $time += dtWEEK; } if ($newMonth > $month) { $time -= dtWEEK; } // If we are forcing a future date then call <me> again with <this year> plus one... if (($forcefuture) and ($time < time())) $time = specialDate($offset, $dayofweek, $month, date('Y', time()) + 1); return $time; } ?> Hey... we're not too bad at this ![]() nutballs
And another iteration
I still use the DateAdd function and didnt do the exceptions. Actually because thats the programmers problem and not the point of the function, I just nuked the error checking and instead put in comments. mine obviously has my own error handling. i collapse your 1st day of month last day of month routine down into an assumption of starting on the first of the month. I then just add 1 month and subtract 1 day to get to the last day of the month. Same principle, but shorter, especially if you do it with your non-function method. new magic... Because i have already landed on the first or last occurrence in the month, I can just do this to determine how many days are left of the offset. $weeks=($offset/abs($offset))*(abs($offset)-1); I then just add the $weeks, which is positive, negative, or zero (which would leave things as they are) only reason in the recursive call that I pass $forcefuture, and the $year as it was passed in, is because there might be a threshold issue of some date combinations. for example if it is currently the second Thursday of October 2007, and you try to get the Second Friday of November 2006, which would be tomorrow, your function would return the second friday of november 2008, even though the next occurrence is actually this year. <? phpfunction specialDate($offset, $dayofweek, $month, $year, $forcefuture=false) { // $offset: 1-5 // $dayofweek: 1-7 ISO day of week number // $month: 1-12 // $year: 4 digit year //make a base time assume the first of the month $time = mktime(0,0,0,$month,1,$year); $move = 1; //positive 1. just a single unit variable for plus or minus calcs //increase 1 month and then decrease 1 day. puts you on last day of month if ($offset < 0) { $time = DateAdd('m', 1, $time); $time = DateAdd('d', -1, $time); $move = -1; //going backward, so move is set negative } // Now find the first matching DayOFWeek... while (date('N', $time) <> $dayofweek) { $time = DateAdd('d',$move,$time); } //determine how many weeks to add, $offset -1 //but since offset can be negative, we need to do funky math to reduce the offset value towards zero. $time=DateAdd('w',($offset/abs($offset))*(abs($offset)-1),$time); // Make sure that the offset or $weeks has not pushed me out of the target month... //roll back or forward a week to get back into month. invert $move if (date('m', $time) <> $month) { $time = DateAdd('w',-$move,$time); } //if we are forcing a future date, we recursively call this function with 1 year greater if (($forcefuture) and ($time < time())) { $time = specialDate($offset, $dayofweek, $month, $year+1, $forcefuture); } return $time; } ?> perkiset
quote author=nutballs link=topic=553.msg3624#msg3624 date=1192123517 I still use the DateAdd function and didnt do the exceptions. Actually because thats the programmers problem and not the point of the function, I just nuked the error checking and instead put in comments. mine obviously has my own error handling. I am so used to fishing up my own code that I always try to protect myself LOL. The DateAdd demonstrates a difference in the way we code, which is cool because PHPsupports us equally well.Although my way is better ![]() quote author=nutballs link=topic=553.msg3624#msg3624 date=1192123517 i collapse your 1st day of month last day of month routine down into an assumption of starting on the first of the month. I then just add 1 month and subtract 1 day to get to the last day of the month. Same principle, but shorter, especially if you do it with your non-function method. It looks shorter, but in reality the payment in terms of processing time is the same, or worse in your example because you always do the first code and sometimes do the second... where in my example I always ONLY do one or the other. And using the offset < or > 0 thing there allows me to handle the offset weeks equation simply as well. Which illustrates another pretty cool point: it's clear that you're more comfortable with maths than I am, because your equation for offset makes you happy but creeps me out. Unfortunately, being a math idiot, I have to see things in terms of clear procedural steps rather than formulaically. So $offset = (++$offset * 7) * dtDAY; makes more sense to me than $time=DateAdd('w',($offset/abs($offset))*(abs($offset)-1),$time); Again, the beauty of diversity and all... quote author=nutballs link=topic=553.msg3624#msg3624 date=1192123517 new magic... Because i have already landed on the first or last occurrence in the month, I can just do this to determine how many days are left of the offset. $weeks=($offset/abs($offset))*(abs($offset)-1); I then just add the $weeks, which is positive, negative, or zero (which would leave things as they are) I actually really like the way you did that, but I also know that I'd not be able to re-read it as quickly in the future. I get the feeling that you'll be expanding my math horizons in the future... : ![]() quote author=nutballs link=topic=553.msg3624#msg3624 date=1192123517 only reason in the recursive call that I pass $forcefuture, and the $year as it was passed in, is because there might be a threshold issue of some date combinations. for example if it is currently the second Thursday of October 2007, and you try to get the Second Friday of November 2006, which would be tomorrow, your function would return the second friday of november 2008, even though the next occurrence is actually this year. Dood you COMPLETELY left me in the dust on that one. 2nd thurs Oct 2007, 2nd Fri Oct 2006 is tomorrow, I'll return 2008 but you wanted 2007? ![]() nutballs
quote author=perkiset link=topic=553.msg3625#msg3625 date=1192124892 quote author=nutballs link=topic=553.msg3624#msg3624 date=1192123517 only reason in the recursive call that I pass $forcefuture, and the $year as it was passed in, is because there might be a threshold issue of some date combinations. for example if it is currently the second Thursday of October 2007, and you try to get the Second Friday of November 2006, which would be tomorrow, your function would return the second friday of november 2008, even though the next occurrence is actually this year. Dood you COMPLETELY left me in the dust on that one. 2nd thurs Oct 2007, 2nd Fri Oct 2006 is tomorrow, I'll return 2008 but you wanted 2007? ![]() no problem, its confusing, but I am assuming the people plugging in these dates are going to be completely retarded. current date is: the Second Thursday of October 2007 Want: The 3rd Saturday of October 1999 If not $forcefuture, then return the correct date for that past time. IF $forcefuture is true you want the NEXT occurrence from TODAY. SO, if you were to set the year to: date('Y', time()) + 1 you end up with 1 year greater than the year as it is TODAY. As a result, your way will give you the occurrence that is at least 1 FULL year in the future, as opposed to the NEXT occurrence. Make better sense? the only reason for my offset math is because I i think you are moving 1 extra week. you need to subtract or add 1 week to bring it 1 week closer to zero. So i you call your function with an offset of 1, i think you end up with the second occurrence, because you add the offset, after you have already found the first occurrence. I think... baby is crying so I might not be seeing it. perkiset
Gotcha -
So my next and probably final iteration: I dumped the forceFuture param because it clouded the functionality for me - I think you were right about 5 posts ago when you talked about <>the 4th or 5th (x) in a month</i> actually meaning the last - this is the real intention of the function. For me, I wanted to clarify it's usage so I trimmed it's functionality. For example, you can pass any offset except 0 because >= abs(4) I'm going to trim it to the (last or first) anyway. Using your math style, I did that in the one line that resets the offset in the beginning. @ offset by one too many - I do think you're missing it - note this line: $weeks = --$offset * 604800; that decrements $offset by 1 BEFORE processing the rest of the line. It's an old C thang. Finally, in the interest of efficiency I just tighted up the rear end a bit as well as ditched the defines - because I am comfy enough with the time magic numbers that I don't need them... did that for readability by n00bs here. <? phpfunction specialDate($offset, $dayofweek, $month, $year) { if (!$offset) throw new Exception('specialDate: Parameter 0 (offset) must not be zero'); if (($dayofweek < 1) or ($dayofweek > 7)) throw new Exception('specialDate: Parameter 1 (Day Of Week) must be 1..7'); if (($month < 1) or ($month > 12)) throw new Exception('specialDate: Parameter 2 (Month) must be 1..12'); // Invert the offset such that if it's >=4 it becomes -1, or <=-4 it becomes 1... $offset = (abs($offset) >= 4) ? (($offset / abs($offset)) * -1) : $offset; if ($offset > 0) { // Make a base time the first of the target month $time = mktime(0, 0, 0, $month, 1, $year); $move = 86400; $weeks = --$offset * 604800; } else { // Make a base time the last of the target month $time = mktime(0, 0, 0, $month + 1, 1, $year) - 86400; $move = -86400; $weeks = ++$offset * 604800; } // Now find the first matching DayOfWeek... while (date('N', $time) <> $dayofweek) { $time += $move; } // return the time, offset by weeks... return $time + $weeks; } ?> nutballs
cool. i missed that --$offset line. Just wasnt seeing it in the noise.
So on a side note, have you ever had a problem with dates by using the seconds method? I guess if you always do the math on the timestamp, seconds value, then you won't have a problem. However, how do you deal with adding 1 month? or adding 1 year when a leap year is involved? thats the reason I made my DateAdd function. because I couldnt figure out how to deal with those at a math (seconds) level. perkiset
Months and years suck because they step out of seconds math. But, since most of my work is days or less, I use seconds math for most of it. No sense using a heavier function for the (most of the time) scenarios.
I like the way that you did it - I think that's plenty good - I've done it a variety of ways, but I think that's the most efficient: <? phpfunction deltaMonth($inDate, $offset) { if (!preg_match('/[-]*[0-9]+/', $offset)) throw new Exception('deltaMonth: Param 1 (Offset) must be a numeric'); $offsetStr = ($offset > 0) ? "+$offset" : $offset; return strtotime("$offsetStr month", $inDate); } function deltaYear($inDate, $offset) { if (!preg_match('/[-]*[0-9]+/', $offset)) throw new Exception('deltaYear: Param 1 (Offset) must be a numeric'); $offsetStr = ($offset > 0) ? "+$offset" : $offset; return strtotime("$offsetStr year", $inDate); } ?> nutballs
Obviously there are other ways than how I did it, i just like it because you would have to be a total dumb shit to not understand the DateAdd function. Also because many projects I have worked on span YEARS of data, i have always assumed that to be needed since along time ago. So my experience has been the opposite, and as a result my needs. Kindof a little cool interestingness thing.
why kind of overhead does a function call end up causing in PHP? InASPit didnt seem to make any difference at all (though I never actually tested it straight out), so thats why I functionalize almost everything.So now the next part of this challenge.... How to do wacky ass holidays, like EASTER - WTF First Sunday after the first full moon on or after March 21 there are others... perkiset
quote author=nutballs link=topic=553.msg3637#msg3637 date=1192138653 why kind of overhead does a function call end up causing in PHP? InASPit didnt seem to make any difference at all (though I never actually tested it straight out), so thats why I functionalize almost everything.No greater really than any other language, but again, just like any language, if you don't HAVE to and it makes sense to do something inline then it will perform better. And my personal bias - which is if I have a tight little method and don't need to go elsewhere to see how it all works the better for me. If I need more complicated than that then by-and-large I'll objectify. quote author=nutballs link=topic=553.msg3637#msg3637 date=1192138653 How to do wacky ass holidays, like EASTER - WTF First Sunday after the first full moon on or after March 21 I use my own function, excludeUselessHolidays() and it works out great ![]() nutballs
lol
nutballs
as an additional note. the ISO day of week 'N' is only in
php5.1 and up. i just found that out...perkiset
Ah... that would explain it missing in my hard reference here.
Sidenote: make sure you're on 5.2+ 5.1 was a charlie foxtrot. |

Thread Categories

![]() |
![]() |
Best of The Cache Home |
![]() |
![]() |
Search The Cache |
- Ajax
- Apache & mod_rewrite
- BlackHat SEO & Web Stuff
- C/++/#, Pascal etc.
- Database Stuff
- General & Non-Technical Discussion
- General programming, learning to code
- Javascript Discussions & Code
- Linux Related
- Mac, iPhone & OS-X Stuff
- Miscellaneous
- MS Windows Related
- PERL & Python Related
- PHP: Questions & Discussion
- PHP: Techniques, Classes & Examples
- Regular Expressions
- Uncategorized Threads