ISO-8601 week numbering vs "Outlook" numbering in PHP - php

I recently came across a big problem, as I have a system that's paying the customers weekly.
As we all know, a year has 52 weeks, and there are standards for it. I'm using PHP aka date('W') to get the week number from a date, that calculates that according to the standard ISO-8601.
Here are some references:
http://www.iso.org/iso/date_and_time_format and
http://en.wikipedia.org/wiki/ISO_week_date
But here's the ISSUE: year 2009 has 53 weeks. It seems that through the Gregorian calendar within 400 years there are 71 years that have 53 weeks. That's one thing I didn't know, and probably many didn't as well.
According to Wikipedia:
2009-12-31 is 2009-W53-4 (ISO year 2009 has 53 weeks, extending the Gregorian year 2009, which starts and ends with Thursday, at both ends with three days).
and the date function in PHP totally respects it.
If you look into MS Outlook, and show day of the week in the calendar view, it will appear 52 weeks
considering 28 DEC 2009 to 03 JAN 2010 week 1. Is this another standard? The US standard or something?
If so, then why PHP can't support it? Did anyone make a function that supported this?
Is it correct to have 53 weeks? We have both European and US clients.

Outlook doesn't follow any sort of standard for week numbering. It has two settings that determine week numbering, called "First day of week" and "First week of year".
By setting "First day of week" to Monday and "First week of year" to "First 4-day week" the ISO standard can be simulated.
Each user will have to make this adjustment to follow the ISO standard.
I know of no separate US standard, and, apparently, neither does Outlook.

I don't think you will have an issue here. Fact is you are following an international standard for date and time formatting and counting (ISO8601). If you have any customers that complain, simply refer them to the standard.
Outlook's week numbering is somewhat equivalent to the following:
$dummyWeek = floor((date('z') + (date('N') - 1)) / 7) + 1;
For billing purposes, you are better off using ISO8601 as a standard. In fact, if you look at your taxes that you are going to fill this year, they will describe the last fiscal year as being 53 weeks long.
The problem with the Outlook way of counting is that a week is not guaranteed to be 7 days. For example, OW01-2010 is compromised of only 2 days: Fri Jan 1, Sat Jan 2. That's an awfully short billing period for a week.
ISO8601 weeks are guaranteed to be 7 days long which is why we need a leap-week every 4/5/6 years.
Which one of those options would you prefer:
ISO8601: Having 53 weeks once in a while, but every single one of them is 7 days long.
Outlook: Having 52/53 weeks in a year at random, but having to pay twice a year for an "half-week".

Here is the way I solved it.
Note: I code in PHP, but the algorithm implemented in SQL is the relevant part of my code below. Also note that I handled '2028-12-31' separately because it is a special date that gives 54 as it's week number.
// generate an sql that calculates the outlook week of $date (until 2033)
function sqlWeek ($date) {
$week = "if( ". $date . " = '2028-12-31', 54, (week( " . $date . ", 0) + if( (year( " . $date . ") - 1) in (2011, 2016, 2022, 2033), 0, 1)) MOD (if (year( ". $date .") = 2028, 54, if( (year( ".$date." ) - 1) in (2011, 2016, 2022, 2033), 53, 52))))";
return "concat( if( char_length( ".$week." ) < 2, concat( '0', ".$week." ), ".$week." ), '/', year($date) )";
}

There is more of the world outside of the USA than there is inside - and ISO 8601 is probably used more widely outside the USA than it is inside.
The initial statement 'a year has 52 weeks' is only partially true, as you have now found. In terms of ISO weeks, you can end up with week 53, as noted in Wikipedia. You can have up to three days of Year N+1 in week 52 of Year N, or week 53 of Year N. You can have up to three days of Year N-1 in Week 1 of Year N, too. And you need to determine both ISO 'week-year' as well as 'week' - because when the Gregorian calendar year is N, the ISO week-year can be year N-1 or N+1 (depending on which end of the year you are working with). That's why there are separate format specifiers ('%Y', '%y', '%g', '%G') in strftime(), for example (but note that the POSIX standard thinks that days before the first Monday in January are in week 0 of the current year, not in week 52 or 53 of the previous year).
There doesn't seem to be any reliable 'US standard' for 'week'. You get different results depending on the software you use. If you look at the Oracle definition of week, then the first seven days of the year are week 1; you have 1 or 2 days in week 53 at the end of the year.
See also SO 274861 for an implementation of ISO week numbers code. Even if you don't code in the language, the algorithm should be readily understandable.

Just a word of warning, if you're using PHP you might also be using MySQL. MySQL does NOT follow ISO-8601 week conventions but does it close enough that you may think it does. As other people are noting about Outlook, trying to mimic a wonky non-standard way of calculating week numbers isn't worth it!

Related

Split a Carbon period into full months and reminder in days for the start and end

What I have is:
$period = CarbonPeriod::create("03-09-2022", '1 month', "20-03-2023"); // 3rd of September and 20th of March
From this I can tell the number of months on which the period spans. But what I need is to have a way to calculate a salary based on that period knowing a monthly salary.
So what I'm actually trying to get is:
28 Days(for the first month) followed by 5 full months followed by 20 days (for the last month)
Is there any way I can get this from CarbonPeriod or CarbonInterval?
The following will give you a good approximation:
$salaryPerMonth = 1000;
$totalSalary = Carbon::create('03-09-2022')->floatDiffInRealMonths('20-03-2023') * $salaryPerMonth;
But unless you're working 7/7 days, the approach is not exact, you would need to take into account only business days (excluding, holidays, etc.)
If you want to go to this deeper precision level, take a look at https://github.com/kylekatarnls/business-day and https://github.com/kylekatarnls/business-time

Why does strftime('%G', strtotime('2017-01-01')) produce 2016?

I found a possible bug in PHP 5.6. The function strftime with a parameter of %G generates the 4 digit year. However, it seems to return the wrong year when fed 1483246800 - i.e. Jan 1, 2017. It returns 2016 instead!
Example code snippet:
echo strftime('%G', strtotime('2017-01-01'));
This should print "2017", but I get "2016" instead. I am running PHP 5.6.
This edge case also shows up for other years - e.g. 2016-01-03 outputs 2015 instead of 2016.
It's not a bug.
As someone pointed out in answer to a bug report someone else filed a while back:
%G - The 4-digit year corresponding to the ISO week number (see %V). This has the same format and value as %Y, except that if the ISO week number belongs to the previous or next year, that year is used instead...A reminder about ISO week numbers. They begin on mondays, end on sundays, and are considered a part of the year in which the majority of their days fall.
This means that using %G for a date close to a year's beginning/end could give you the correct year, the previous year, as in your case, or the next year, (for example, echo strftime('%Y', strtotime('2002-12-30)) gives 2003).
If you want to get the correct year, you should use %Y instead. echo strftime('%Y', strtotime('2017-01-01')); gives 2017.
It's also worth checking out the definition of %V:
ISO-8601:1988 week number of the given year, starting with the first week of the year with at least 4 weekdays, with Monday being the start of the week.

PHP DateTime sub produces unexpected results

I have the following example of me subtracting the DateInterval from DateTimeImmutable
$dateA = new DateTimeImmutable('2016-06-30');
$dateB = new DateTimeImmutable('2016-05-31');
$dateInterval = new DateInterval('P3M');
// print 2016-03-30 as expected
echo $dateA->sub($dateInterval)->format('Y-m-d');
// print 2016-03-02 which i would expect 2016-02-29
echo $dateB->sub($dateInterval)->format('Y-m-d');
When I set the period to 'P8M' it works as expected. How it comes, it dosent works for february?
Ok, it's really simple (kind of). Each 'month' interval evaluates to the prior (or X number of prior) month's equivalent day. If there are more days in the current month, than the month being landed on, the excess overflows to the following month.
So if you have a date which is May 31, 2016 and want to subtract 3 month intervals, it will:
Go back 3 months (in the list of months, don't think days yet), resulting in 'February'
Then look for February 31st. This doesn't exist so bleed over to following month 2 days (2016 Febuary has 29 days, so 2 extra days)
Viola! March 2nd.
Go forward, lets say you're in May 31, 2016 and want to add one month
Go forward one month to June.
Look for June 31st, nope, 1 extra day, bleed over to July.
As expected, July 1st is the answer.
The lesson in this: Adding and Subtracting Month intervals sucks, is confusing, and can lead to non-intuitive results unless you've got your month calculation rosetta stone with you.
Explanation from the PHP Docs
Note:
Relative month values are calculated based on the length of months that they pass through. An example would be "+2 month 2011-11-30", which would produce "2012-01-30". This is due to November being 30 days in length, and December being 31 days in length, producing a total of 61 days.

php week(curdate()) sum of other years

OK so i have this query:
SELECT obrero as MAESTRO, sum(costo_semanal) AS TOTAL_COST,
ROUND(SUM(week_cost)/MONTH(CURDATE()),2) AS MONTHLY_COST,
ROUND(SUM(week_cost)/WEEK(CURDATE()),2) AS WEEKLY_COST
from tbl_costos WHERE obrero ='$maestro'
I did this and it worked great in 2015, the problem is that now on 2016 we go back to week 1 and month 1 so im not having the proper division.
What I need to accomplish is to sum the 52 weeks of the past year and sum the current week of this year so i could have a % of the cost per week
"cost/number of weeks" = $cost per week.
for example of today 2016-01-18 being the 4th week of the year
total paid (of 2015 and 4 weeks of 2016) = $4000.00
weeks = 52 + 4 = 56
4000.00/56 = $71.4285714 average cost per week
The same thing applies to Months, it should be doing the division with 13, and cus January is month 1, its doing it over 1.
I could just do:
SUM(week_cost)/(12+ MONTH(CURDATE()));
and
SUM(week_cost)/(52 + WEEK(CURDATE()));
but that would solve the problem for this year only!!
You should use DATEDIFF() in order to calculate the number of days each maestro has worked from a given date, "start_date" to a given end date "end_date", and then convert those days to weeks, months and years if you need to.
Be flexible and anticipate that each maestro can work from any given date, or a report can be asked from any given date. Your's assume that each maestro started to work on 2015-01-01 and that all reports shall be done with this in mind. Reality is different.
Select obrero as MAESTRO, sum(costo_semanal) as TOTAL_COST, ROUND(SUM(costo_semanal/ROUND(DATEDIFF(start_date, end_date)/30,0)),0) AS MONTHLY_COST, ROUND(SUM(costo_semanal/ROUND(DATEDIFF(start_date, end_date)/7,0)),0) AS WEEKLY_COST FROM tbl_costos WHERE obrero=$maestro;
Do not place the variable $maestro on your query, better use PREPARED STATEMENTS.

How to get year-sensitive calendar week ? date("W") gives back 52 for 1st of January

As the headline says, PHP's date("W") function gives back the calendar week (for the current day). Unfortunatly it gives back 52 or 53 for the first day(s) of most years. This is, in a simple thinking way, correct, but very annoying, as January 1st of 2012 is not calendar week 52, it's NOT a calendar week of the current year. Most calendars define this as week 0 or week 52 of the previous year.
This is very tricky when you group each day of the year by their calendar week: 1st of January 2012 and 31st of December 2012 are both put into the same calendar week group.
So my question is: Is there a (native) year-sensitive alternative to PHP's date("W") ?
EDIT: I think I wrote the first version of this question in a very unclear way, so this is my edit: I'm searching for a function that gives back the correct calendar week for the first day(s) of the year. PHP's date("W") gives back 52 for the 1st of January 2012, which is "wrong". It should be 0 or null. According to official sources, the first calendar week of a year starts on the first monday of the year. So, if the first day of a year is not a monday, it's not week 1 ! It's week 0. The wikipedia article says
If 1 January is on a Monday, Tuesday, Wednesday or Thursday, it is in week 01. If 1 January is on a Friday, Saturday or Sunday, it is in week 52 or 53 of the previous year.
This becomes tricky as the last days of the year are also in week 52/53. date("W") does not divide into current year and previous year.
This solution converts the excess of december to week 53 and everything in january prior to week 1 to week 0.
$w=(int)date('W');
$m=(int)date('n');
$w=$w==1?($m==12?53:1):($w>=51?($m==1?0:$w):$w);
echo "week $w in ".date('Y');
2013-12-31 ==> week 53 in 2013
2014-01-01 ==> week 1 in 2014
2015-12-31 ==> week 52 in 2015
2016-01-01 ==> week 0 in 2016
And a small test run, so you can see for yourself ;-)
$id=array(25,26,27,28,29,30,31,1,2,3,4,5,6,7,8);
for($iy=2013;$iy<2067;++$iy){foreach($id as $k=>$v){if($k<7){$im=12;}else{$im=1;}
if($k==7){++$iy;echo '====<br>';}$tme=strtotime("$im/$v/$iy");
echo date('d-m-Y',$tme),' * * ';
//THE ACTUAL CODE =================
$w=(int)date('W',$tme);
$m=(int)date('n',$tme);
$w=$w==1?($m==12?53:1):($w>=51?($m==1?0:$w):$w);
//THE ACTUAL CODE =================
echo '<b>WEEK: ',$w,' --- ','YEAR: ',date('Y',$tme),'</b><br>';}--$iy;
echo '----------------------------------<br>';}
Is there a (native) year-sensitive alternative to PHP's date("W") ?
No, there isn't.
According to official sources, the first calendar week of a year starts on the first monday of the year.
I'm not sure what official sources you're referring to.
PHP's date("W") returns the week number according to ISO 8601. As an international standard, ISO 8601 counts as one of possibly many "official sources". If its definition of week numbers doesn't fit your application, you're free to use anything else you like.
If you use a non-standard definition of "first week of the year", or if you use an official source that's not widely recognized, expect to have to write your own function to replace date("W"). (I'm pretty sure you'll need to write a function.)
The date 2012-01-01 was a Sunday. ISO 8601, Wikipedia, and php agree that the ISO week number for 2012-01-01 is 52.
ISO 8601 doesn't define a week 0.
So, if the first day of a year is not a monday, it's not week 1 !
Neither ISO nor Wikipedia say that. ISO 8601 defines week number 1 as the week that has the year's first Thursday in it. For 2012, the first Thursday was on Jan 5, so week number 1 was Jan 2 to Jan 8. 2012-01-01 was in the final week of the previous year, in terms of ISO weeks.
If you want something different, you can play with arithmetic, division, and so on. (Try dividing date("z") by 7, for example.) Or you can store that data in a database, and have your weeks any way you like.
If you're dealing with accounting periods, I'd almost certainly store that data in a table in a database. It's pretty easy to generate that kind of data with a spreadsheet.
The text of data in a table is much easier to audit than the text of a php function, no matter how simple that function is. And the data is certain to be the same for any program that accesses it, no matter what language it's written in. (So if your database someday has programs written in 5 different languages accessing it, you don't have to write, test, and maintain 5 different functions to get the week number.)
$d = new DateTime('first monday january '.date('Y'));
echo $d->format("W");
Google brought me here, and I wanted to post the following to help others like me...
I am in the US, and use DayPilot, and it works as follows:
Week starts on Sun, not Mon.
Jan 1st is always Week 1.
If Jan 1st is not a Sunday, Week 1 is less than 7 days.
This all makes a lot of since to me!
Here is my PHP function to copy that behavior:
function ProperWeekNum($inDate)
{$outNum = $inDate->format('W');
//Make week start on Sunday
if ($inDate->format('D') == 'Sun') {$outNum++;}
//Fix begining of year
if (($outNum >= 52) && ($inDate->format('M') == 'Jan')) {$outNum = 1;}
//Fix WEEK #1 is 1-day (Sat)
else //...without this 2022 was off by 1 all year
{$jan1st = new DateTime($inDate->format('Y').'-01-01');
if ($jan1st->format('D') == 'Sat') {$outNum++;}
}
//Return without leading zero
return ltrim($outNum, '0');
}
I use the function as follows, so when I click on DayPilot, my custom popup's Week # always matches DayPilot's Week #:
$weeknum = ProperWeekNum($startdate);
if ($weeknum != ProperWeekNum($enddate))
{$weeknum .= '-'.ProperWeekNum($enddate);}
Probably won't help the OP, but hopefully it helps someone.

Categories