I would like to determine the current Fiscal week period and year with a function when you pass it an empty argument, or the week, period and year of a datestring you include in the argument.
I have a function like this so far.
function FiscalDates($date){
$basedate=date_create("2018-04-29");
$querydate=date_create($date);
$diff=date_diff($basedate,$querydate);
$days = $diff->format('%a');
if(fmod($days,7)==0){
$weekno = ($days/7)+1;
} else {
$weekno = ceil($days/7);
}
if(fmod($weekno,4)==0){
$period = ($weekno/4);
} else {
$period = ceil($weekno/4);
}
if(fmod($period,13)==0){
$year = ($period/13)-1;
} else {
$year = (ceil($period/13))-1;
}
$period = $period-(13*$year);
$year = $year+2018;
return array($weekno,$period,$year);
}
This works for the most part, but the only problem is that it does not account for the 'leap week' that happens every 5-6 years. (Where there is 53 weeks in the year) see 2nd paragraph of this page about that. Every time this happens, the calculations will be off by another week.
How can I achieve a similar thing, but account for the 53 weeks in a year years and keep it accurate?
Might be too late or not exactly a direct reply, but had similar issues a few years back for a UK business, so decided to work on a library that could prove helpful for you.
https://github.com/RoussKS/financial-year
It can get period/weeks for a financial year.
It handles 2 types of financial year. The standard 12 month (period) one, named calendar as a library convention, and business for 13 period financial years (for 52/53 weeks). Business is the one you would want to use.
It does not check when a year should be a 53 week year as this depends on the business, so you would need to provide that as a param (defaults to false aka 52).
Example use to fetch the period & week
require_once __DIR__ . '/vendor/autoload.php';
// DateTimeAdapter
// If instantiating with string, it must be of ISO-8601 format 'YYYY-MM-DD'.
// This is your business's financial year start date.
$startDate = new \DateTime('2018-04-01');
$fy = new \RoussKS\FinancialYear\DateTimeAdapter('business', $startDate, false); // false for 52 weeks, true for 53 weeks
$dateToExamine = '2018-04-29';
// Get period of the date in question
echo $fy->getPeriodIdByDate($dateToExamine); // 1
// Get week of the date in question
echo $fy->getBusinessWeekIdIdByDate($dateToExamine); // 5
Not sure though what you mean by year, is it like naming the year as per 2018/2019?
Related
This question already has answers here:
How to find first day of the next month and remaining days till this date with PHP
(17 answers)
Closed 2 years ago.
I am working with WooCommerce Subscriptions and am trying to set it so that users can select their own start date using another plugin. Within the date picker plugin, if a user chooses a date within the middle of the month, the payments are supposed to be prorated and then the subscription starts on the first day of the next month. However, the plugin works to only start at the day the subscription starts. I am trying to adjust this so it chooses the start date of the first day of the next month. I have used this as reference - How to find first day of the next month and remaining days till this date with PHP
function woosubscriptions_custom_cart_next_payment_date( $start_pay_date , $recurring_cart){
foreach($recurring_cart->get_cart() as $cart_item_key => $cart_item){
if(isset($cart_item['start-subcription']) && !empty($cart_item['start-subcription'])){
$days_free_trial = WC_Subscriptions_Product::get_trial_length( $cart_item['data'] );
$start_subcription = $cart_item['start-subcription'];
if($days_free_trial > 0){
$start_subcription .= ' ' . $days_free_trial . ' days';
}
$offset=5*60*60; //converting 5 hours to seconds.
$start_pay_date = gmdate( 'Y-m-d H:i:s', strtotime($start_subcription) + $offset);
$first_day_next_month = ($start_pay_date, strtotime('first day of next month'));
break;
}
}
return $first_day_next_month;
}
add_filter( 'wcs_recurring_cart_next_payment_date', 'woosubscriptions_custom_cart_next_payment_date', 10 , 2);
It is not correctly getting the first day of next month.
Your code to calculate the first day of next month is not quite right. The answer you have linked looks different to the code you have used.
strtotime('first day of next month') calculates the first day of the current month and will bear no relation to the start date.
You could use a DateTime object and figure it out from there:
$start_pay_date = new DateTime($start_subcription);
$first_day_next_month = $start_pay_date->modify("first day of next month");
$first_day_next_month would then represent the first day of the month after your $start_pay_date, you can use any of the DateTime methods to then format it how you want (e.g. $first_day_next_month->format("Y-m-d"))
I'm trying to use DateTime to check if a credit card expiry date has expired but I'm a bit lost.
I only want to compare the mm/yy date.
Here is my code so far
$expmonth = $_POST['expMonth']; //e.g 08
$expyear = $_POST['expYear']; //e.g 15
$rawExpiry = $expmonth . $expyear;
$expiryDateTime = \DateTime::createFromFormat('my', $rawExpiry);
$expiryDate = $expiryDateTime->format('m y');
$currentDateTime = new \DateTime();
$currentDate = $currentDateTime->format('m y');
if ($expiryDate < $currentDate) {
echo 'Expired';
} else {
echo 'Valid';
}
I feel i'm almost there but the if statement is producing incorrect results. Any help would be appreciated.
It's simpler than you think. The format of the datess you are working with is not important as PHP does the comparison internally.
$expires = \DateTime::createFromFormat('my', $_POST['expMonth'].$_POST['expYear']);
$now = new \DateTime();
if ($expires < $now) {
// expired
}
You can use the DateTime class to generate a DateTime object matching the format of your given date string using the DateTime::createFromFormat() constructor.
The format ('my') would match any date string with the string pattern 'mmyy', e.g. '0620'. Or for dates with 4 digit years use the format 'mY' which will match dates with the following string pattern 'mmyyyy', e.g. '062020'. It's also sensible to specify the timezone using the DateTimeZone class.
$expiryMonth = 06;
$expiryYear = 20;
$timezone = new DateTimeZone('Europe/London');
$expiryTime = \DateTime::createFromFormat('my', $expiryMonth.$expiryYear, $timezone);
See the DateTime::createFromFormat page for more formats.
However - for credit/debit card expiry dates you will also need to take into account the full expiry DATE and TIME - not just the month and year.
DateTime::createFromFormat will by default use todays day of the month (e.g. 17) if it is not specified. This means that a credit card could appear expired when it still has several days to go. If a card expires 06/20 (i.e. June 2020) then it actually stops working at 00:00:00 on 1st July 2020. The modify method fixes this. E.g.
$expiryTime = \DateTime::createFromFormat('my', $expiryMonth.$expiryYear, $timezone)->modify('+1 month first day of midnight');
The string '+1 month first day of midnight' does three things.
'+1 month' - add one month.
'first day of' - switch to the first day of the month
'midnight' - change the time to 00:00:00
The modify method is really useful for many date manipulations!
So to answer the op, this is what you need — with a slight adjustment to format to cater for single digit months:
$expiryMonth = 6;
$expiryYear = 20;
$timezone = new DateTimeZone('Europe/London');
$expiryTime = \DateTime::createFromFormat(
'm-y',
$expiryMonth.'-'.$expiryYear,
$timezone
)->modify('+1 month first day of midnight');
$currentTime = new \DateTime('now', $timezone);
if ($expiryTime < $currentTime) {
// Card has expired.
}
An addition to the above answers.
Be aware that by default the days will also be in the calculation.
For example today is 2019-10-31 and if you run this:
\DateTime::createFromFormat('Ym', '202111');
It will output 2021-12-01, because day 31 does not exist in November and it will add 1 extra day to your DateTime object with a side effect that you will be in the month December instead of the expected November.
My suggestion is always use the day in your code.
For op's question:
$y=15;
$m=05;
if(strtotime( substr(date('Y'), 0, 2)."{$y}-{$m}" ) < strtotime( date("Y-m") ))
{
echo 'card is expired';
}
For others with full year:
$y=2015;
$m=5;
if(strtotime("{$y}-{$m}") < strtotime( date("Y-m") ))
{
echo 'card is expired';
}
Would it not be simpler to just compare the string "201709" to the current year-month? Creating datetime objects will cost php some effort, I suppose.
if($_POST['expYear']. str_pad($_POST['expMonth'],2,'0', STR_PAD_LEFT ) < date('Ym')) {
echo 'expired';
}
edited as Adam states
The best answer is provided by John Conde above. It it does the minimum amount of processing: creates two correct DateTime objects, compares them and that's all it needs.
It could work also as you started but you must format the dates in a way that puts the year first.
Think a bit about it: as dates, 08/15 (August 2015) is after 12/14 (December 2014) but as strings, '08 15' is before '12 14'.
When the year is in front, even as strings the years are compared first and then, only when the years are equal the months are compared:
$expiryDate = $expiryDateTime->format('y m');
$currentDate = $currentDateTime->format('y m');
if ($expiryDate < $currentDate) {
echo 'Expired';
} else {
echo 'Valid';
}
Keep it simple, as the answer above me says except you need to string pad to the left:
isCardExpired($month, $year)
{
$expires = $year.str_pad($month, 2, '0', STR_PAD_LEFT);
$now = date('Ym');
return $expires < $now;
}
No need to add extra PHP load using DateTime
If you are using Carbon, which is a very popular Datetime extension library. Then this should be:
$expMonth = $_POST['month'];
$expYear = $_POST['year'];
$format_m_y = str_pad($expMonth,2,'0', STR_PAD_LEFT).'-'.substr($expYear, 2);
$date = \Carbon\Carbon::createFromFormat('m-y', $format_m_y)
->endOfMonth()
->startOfDay();
if ($date->isPast()) {
// this card is expired
}
Also take into consideration the exact date and time expiration:
Credit cards expire at the end of the month printed as its expiration date, not at the beginning. Many cards actually technically expire one day after the end of that month. In any case, unless they list a specific day of expiration along with month and year, they should work all the way through the end of their expiration month. Cardholders should not wait until the last moment to secure a replacement card. Source
We are creating a pretty generic ordering system, which bases payouts to users on the current week of a pay cycle.
Our pay cycles are bi-weekly, so we consider that WEEK 1 will be an ODD week, and WEEK 2 will be an EVEN week.
Week 1 (odd) [start period A]
Week 2 (even) [end period A]
Week 3 (odd) [start period B]
Week 4 (even) [end period B] [Payout period A on Friday of this week]
Week 5 (odd) [start period C]
Week 6 (even) [end period C] [Payout period B on Friday of this week]
And so forth.
We can use the following to determine whether or not it is an odd or even week:
self::$now = new DateTime();
if (self::$now->format('W') % 2 === 0) {
// Week is even -
} else {
// Week is odd
}
If it is an odd week, we want to take the Sunday of that week to use that as the 'Start Date' of the current pay cycle. On the other hand, if it is an even week, we want to take the Saturday of that week and use that as the 'End Date' of the current pay cycle.
Previously, our method of calculating these start and end dates was rather crude. We merely selected an arbitrary date to use as the first date of the two week pay cycle, and would use some messy DateTime() code to calculate diffs and so forth. We do not want to do it this way, but instead rely on whether or not the week is EVEN or ODD.
Here is the code we were using to calculate the previous start and end dates:
public function getPreviousPeriodStart() {
$daysIntoCurrentPeriod = ((int)self::$now->diff(self::$refStart)->format('%a') % self::PERIOD_LENGTH);
self::$prevPeriodStart = new DateTime('2 weeks ago');
self::$prevPeriodStart->sub(new DateInterval('P'.$daysIntoCurrentPeriod.'D'));
return self::$prevPeriodStart;
}
public function getPreviousPeriodEnd() {
$daysLeftCurrentPeriod = self::PERIOD_LENGTH - ((int)self::$now->diff(self::$refStart)->format('%a') % self::PERIOD_LENGTH) - 1;
self::$prevPeriodStart = new DateTime('2 weeks ago');
self::$prevPeriodStart->add(new DateInterval('P'.$daysLeftCurrentPeriod.'D'));
return (self::$prevPeriodStart);
}
Again, i know this is poor and sloppy, which is why I want to improve it!
Once we have established the start and end dates of the current pay cycle, we would like to be able to determine the following values:
current pay period start
current pay period end
previous pay period start
previous pay period end
previous period pay date (this will be the pay date for the PREVIOUS pay period - the one that ended before the current pay period began) [This will occur the Friday of the following EVEN pay period]
current period pay date (this will be the pay date for the current pay period) [This will occur the Friday of the NEXT EVEN pay period]
I am trying to find the cleanest, sanest way to handle this. If this approach or methodology is not ideal, I would welcome any alternate suggestions - I just want to make sure this is accurate and maintainable!
Like I said in the comment, odd/even approach will fail on years that have odd number of weeks, like year 2015, which has 53 weeks. So last week of the year 2015 will be odd, and then next week (first week in year 2016) will also be odd. Probably this is not intended?
Your first approach was better. Choose one date as a reference, and that reference is an odd start period date. Based on that, you now know if your current period is odd or even, see method isOdd() bellow.
Example:
class DateTimeExtended extends DateTime {
# ref start date is odd
const REF_START = '2013-W43-1';
protected function isOdd(DateTime $dt)
{
$ref = new DateTime(self::REF_START);
return floor($dt->diff($ref)->days / 7) % 2 == 0;
}
public function getCurrentPeriodStart()
{
$dt = new DateTime($this->format('o-\WW-1'));
if (!$this->isOdd($dt)) {
$dt->modify('-1 week');
}
return $dt;
}
public function getCurrentPeriodEnd()
{
$dt = new DateTime($this->format('o-\WW-7'));
if ($this->isOdd($dt)) {
$dt->modify('+1 week');
}
return $dt;
}
public function getPreviousPeriodStart()
{
$dt = $this->getCurrentPeriodStart();
return $dt->modify('-2 week');
}
public function getPreviousPeriodEnd()
{
$dt = $this->getCurrentPeriodEnd();
return $dt->modify('-2 week');
}
}
Use (demo):
$dt = new DateTimeExtended;
print_r( $dt->getCurrentPeriodStart() ); # 2013-10-21
print_r( $dt->getCurrentPeriodEnd() ); # 2013-11-03
print_r( $dt->getPreviousPeriodStart() ); # 2013-10-07
print_r( $dt->getPreviousPeriodEnd() ); # 2013-10-20
Like Dwayne Towell already said in the question comment, ISO8601 weeks start on Monday, not Sunday, so you will need to adjust code to make it work like that. Just replace ISO formats, like this.
The DateTime class in PHP (5.3+) works just great as long as the first day of the week in your country is Sunday. In the Netherlands the first day of the week is Monday and that just makes the class useless for building a calendar with week view and calculations.
I can't seem to find an answer on Stackoverflow or the rest of the Internet on how to have DateTime act as if the first day of the week is Monday.
I found this piece on Stackoverflow, but it doesn't fix all the ways you can get into trouble and it's not an elegant solution.
$dateTime = new DateTime('2012-05-14');
$monday = clone $dateTime->modify(('Sunday' == $dateTime->format('l')) ? 'Monday last week' : 'Monday this week');
Is there a way to change this or extent DateTime? Can't imagine it's not a setting as most of Europe starts their weeks on monday.
Added:
Posting the full calendar and functions code will not make things clearer. But here is one one line for example.
I often have to check what the first day of the week is or calculate from the first day of the week to a different date and time in that week. My code is getting full of these:
$startOfWeek = $date->modify(('Sunday' == $date->format('l')) ? 'Monday last week' : 'Monday this week')->modify('+3 hours')->format(DATETIME);
I also get an unwanted result trying to get the first full week of the month or year. As my $date object doesn't always contain the same date I have to keep checking it this way, making the code difficult to read. Having a lot more programming to do on this calendar I can't forsee where it's going to bug again.
EDIT There are some inconsistencies though. For some strange reason DateTime does get this next one right:
$test = new DateTime('2012-10-29'); // Monday
echo $test->modify('Sunday this week')->format('Y-m-d'); // 2012-11-04
// But...
$test = new DateTime('2012-11-04'); // Sunday
echo $test->modify('Monday this week')->format('Y-m-d'); // 2012-11-05 instead of 2012-10-29
But I think I can make the question clearer:
Can the DateTime() class be used with monday as the first day of the week. If not, can the class be extended to use monday as the first day of the week.
UPDATE:
Ok, I think I'm getting somewhere... I'm not a pro at coding classes..but this seems to work for the weeks. But it still needs rules for first day, second day... and also for the day name Sunday itself. I don't think this is foolproof. I would appreciate any help to fix it.
class EuroDateTime extends DateTime {
// Fields
private $weekModifiers = array (
'this week',
'next week',
'previous week',
'last week'
);
// Override "modify()"
public function modify($string) {
// Search pattern
$pattern = '/'.implode('|', $this->weekModifiers).'/';
// Change the modifier string if needed
if ( $this->format('N') == 7 ) { // It's Sunday
$matches = array();
if ( preg_match( $pattern, $string, $matches )) {
$string = str_replace($matches[0], '-7 days '.$matches[0], $string);
}
}
return parent::modify($string);
}
}
// This works
$test = new EuroDateTime('2012-11-04');
echo $test->modify('Monday this week')->format('Y-m-d');
// And I can still concatenate calls like the DateTime class was intended
echo $test->modify('Monday this week')->modify('+3 days')->format('Y-m-d');
I found this to work, yet there are some inconsistencies in PHP's DateTime class.
If the departing date is a sunday the previous monday is not considered the same week (fixed by this class). But departing from a monday, the next sunday is considered as the same week. If they fix that in the future this class will need some additions.
class EuroDateTime extends DateTime {
// Override "modify()"
public function modify($string) {
// Change the modifier string if needed
if ( $this->format('N') == 7 ) { // It's Sunday and we're calculating a day using relative weeks
$matches = array();
$pattern = '/this week|next week|previous week|last week/i';
if ( preg_match( $pattern, $string, $matches )) {
$string = str_replace($matches[0], '-7 days '.$matches[0], $string);
}
}
return parent::modify($string);
}
}
There's nothing to stop you manually modifying a date to get the "first" day of the week, depending on your definition of "first". For instance:
$firstDayOfWeek = 1; // Monday
$dateTime = new DateTime('2012-05-16'); // Wednesday
// calculate how many days to remove to get back to the "first" day
$difference = ($firstDayOfWeek - $dateTime->format('N'));
if ($difference > 0) { $difference -= 7; }
$dateTime->modify("$difference days");
var_dump($dateTime->format('r')); // "Mon, 14 May 2012 00:00:00 +0000"
Notice how the output changes as you vary $firstDayOfWeek; if it was changed to 4 above (Thursday), it would then consider Thu, 10 May 2012 as the "first" day.
Edit: this is a rather basic example. The correct way to do this is by using the user/system's locale to give you the "first" day, and compute from there. See this question for more information.
You can also get the start and the end of the week with setISODate, the last parameter is the ISO-daynumber in the week, monday is 1 and sunday 7
$firstday = new \DateTime();
$lastday = clone($firstday);
$firstday->setISODate($firstday->format("Y"),$firstday->format("W"),1);
$lastday->setISODate($firstday->format("Y"),$firstday->format("W"),7);
I too am confused about what the problem is, because DateTime does have a notion of the week starting from Monday: The N format gives you the days of the week counting from Monday to Sunday, with Monday being 1 and Sunday being 7. Moreover, the W format, the only way to get a week number, also starts the week on Monday. (I seem to remember that Dutch week numbers aren't exactly like ISO week numbers, but that's a different story). So what feature are you missing exactly?
Edit So the problem is (only?) that the relative datetime formats, such as "Monday this week" give the wrong result when the reference date is a Sunday. From the docs it sounds like DateTime just defers to strtotime, which is supposed to be locale-dependent. So if you have your locale set properly and this is not working, it sounds like a bug (for what that's worth).
The proper answer:
$week_start_day; // 0 - Sunday, 1 - Monday
$date_time_from = new DateTime('now');
$date_time_to = clone $date_time_from;
$this_week_first_day = $date_time_from->setISODate($date_time_from->format("Y"), $date_time_from->format("W"), $week_start_day);
$this_week_last_day = $date_time_to->setISODate($date_time_to->format("Y"), $date_time_to->format("W"), (6 + $week_start_day));
I'm kind of at a loss here. It seems as though somehow my code is missing a whole week at the end of 2009 and I've tried a couple different things.
My base function to get the start and end date for a week is below. Given a Year, Week and Day of the Week it gives you a date.
function datefromweeknr($aYear, $aWeek, $aDay)
{
$Days=array('xx','ma','di','wo','do','vr','za','zo');
//xx = Current Sun, ma = Mon ..... zo = Sun of the next Week
$DayOfWeek=array_search($aDay,$Days); //get day of week (1=Monday)
$DayOfWeekRef = date("w", mktime (0,0,0,1,4,$aYear)); //get day of week of January 4 (always week 1)
if ($DayOfWeekRef==0){
$DayOfWeekRef=7;
}
$ResultDate=mktime(0,0,0,1,4,$aYear)+((($aWeek-1)*7+($DayOfWeek-$DayOfWeekRef))*86400);
return $ResultDate;
}
Seemed to work completely fine until I realized that I was missing the week of December 27th 2009 to January 2nd 2010.
echo '<table border="1">';
for($i = 1; $i < 53; $i++){
if($i < 10){
$w = '0'.$i.'1';
}
else{
$w = $i.'1';
}
echo '<tr><td>Week#'.$i.' </td><td> '.date("Y-m-d",datefromweeknr(2009,$i,"xx")).' </td><td> '.date("Y-m-d",datefromweeknr(2009, $i,"za")).'</td><td> Week = '.date("W: Y-m-d",strtotime("2009W$w")).' </td></tr>';
}
echo '</table>';
It seems the 52nd week of the year ends on 2009-12-26 and the 1st week of the new year starts on 2010-01-03. I'm losing a whole week, No Bueno!
Anyone know what I'm doing wrong or can point me to a fool proof way of supplying a week number and a year to get me the start and end date of that week without losing any days in the process?
Check here:
http://www.onlineconversion.com/day_week_number.htm
If you enter 29 december 2009, so see that US and ISO/Europe give different week numbers (resp. 52 and 53).
Could this be related to your problem? Which standard do you dates conform too?
Edit:
From http://www.epochconverter.com/epoch/weeknumbers.php :
Week number according to the ISO-8601 standard, weeks starting on Monday. The first week of the year is the week that contains that year's first Thursday. The highest week number in a year is either 52 or 53.
Your question remainded me of a bug comment I read today on php.net:
In PHP 5 prior to 5.2.7, requesting a
given occurrence of a given weekday in
a month where that weekday was the
first day of the month would
incorrectly add one week to the
returned timestamp. This has been
corrected in 5.2.7 and later versions.
Which is irrevelent for now but I would suggest you to replace your calculations in datefromweeknr with strtotime calls. I'm pretty sure strtotime will fix your calculation bug.
So you could use something like:
strtotime('last Monday', $timestamp);