I want to list for the user, all timezones with their native UTC/GMT offset, regardless of DST
How can I do it?
I 've come up with this function to do the job:
function standard_tz_offset($timezone) {
$now = new DateTime('now', $timezone);
$year = $now->format('Y');
$startOfYear = new DateTime('1/1/'.$year, $timezone);
$startOfNext = new DateTime('1/1/'.($year + 1), $timezone);
$transitions = $timezone->getTransitions($startOfYear->getTimestamp(),
$startOfNext->getTimestamp());
foreach($transitions as $transition) {
if(!$transition['isdst']) {
return $transition['offset'];
}
}
return false;
}
How it works
The function accepts a timezone and creates two DateTime objects: January 1st 00:00 of the current year and January 1st 00:00 of the next year, both specified in that timezone.
It then calculates the DST transitions during this year, and returns the offset for the first transition it finds where DST is not active.
PHP 5.3 is required because of the call to DateTimeZone::getTransitions with three parameters. If you want this to work in earlier versions you will have to accept a performance hit, because a whole lot of transitions will be generated by PHP (in this case, you don't need to bother with creating the $startOfYear and $startOfNext dates).
I have also tested this with timezones that do not observe DST (e.g. Asia/Calcutta) and it works for those as well.
To test it:
$timezone = new DateTimeZone("Europe/Athens");
echo standard_tz_offset($timezone);
Related
I have these two functions:
function time_is_older_than($timestamp, $time_string)
{
if (strtotime($timestamp) < strtotime('-' . $time_string))
return true;
return false;
}
function time_is_younger_than($timestamp, $time_string)
{
if (strtotime($timestamp) > strtotime('-' . $time_string))
return true;
return false;
}
They enable me to do neat things like:
if (time_is_older_than($last_time_some_action_happened, '5 minutes'))
do_it_again();
They normally work, except for during one hour every six months, when my timezone switches over to "summer time" or "winter time". This means that the clocks are increased or put back one hour at midnight (according to this timezone).
The PHP manual states this for strtotime:
The Unix timestamp that this function returns does not contain information about time zones. In order to do calculations with date/time information, you should use the more capable DateTimeImmutable.
However, if I provide the exact same date/time string, with "+08:00" added in the end versus "+00:00", for example, I get different numbers of seconds returned. So strtotime() does understand timezones when it parses the provided time, even if the returned integer obviously doesn't contain this information. (Nor is it expected or required to by me.)
I've spent countless hours trying to debug this, testing countless things, and just sitting here thinking, but I can't figure out what exactly would make the code I have fail, specifically for one hour. And especially what about it I need to change. Setting the second parameter for strtotime() seems likely, but I just couldn't make it work correctly.
My hottest "lead" for quite some time was that the strtotime('-' . $time_string) part is ending up using a different timezone than the timestamp strings provided, but I do provide timezone data to it most of the time! An example of $last_time_some_action_happened might be something like 2020-10-28 02:22:41.123456+01.
I set the timezone with date_default_timezone_set().
I suspect that I only need to make some very minor change, but I've been experimenting so much and so long now, even taking rests in between, that my brain can no longer see this clearly. I bet the solution is something awfully simple.
Please don't tell me to use DateTimeImmutable. This would fundamentally change my entire structure and require me to do things very differently. Perhaps I should, and even will, at some point, but for now, I just wish to fix this rare but still very annoying bug in my existing code. (If it's possible at all, which I very much believe is the case.)
I'm able to reproduce the issue you are having:
date_default_timezone_set('Pacific/Auckland');
// Daylight saving time 2020 in New Zealand began at 2:00am on Sunday, 27 September
$current = strtotime('2020-09-27 02:04:00');
$d1 = strtotime('2020-09-27 02:05:00', $current);
$d2 = strtotime('-5 minutes', $current);
var_dump($d1 > $d2); // false
var_dump(date('Y-m-d H:i:s', $d1)); // 2020-09-27 03:05:00
var_dump(date('Y-m-d H:i:s', $d2)); // 2020-09-27 03:59:00
This person looks to be having the same issue as you and may appear to be a bug.
DateTime::modify and DST switch
The solution is to convert the dates to UTC then compare:
// Convert to UTC and compare
$d1 = new \DateTime('2020-09-27 02:05:00', new \DateTimeZone('Pacific/Auckland'));
$d2 = new \DateTime('2020-09-27 02:04:00', new \DateTimeZone('Pacific/Auckland'));
$d2->setTimezone(new \DateTimeZone('UTC'));
$d2->modify('-5 minutes');
$d2->setTimezone(new \DateTimeZone('Pacific/Auckland'));
var_dump($d1 > $d2); // true
var_dump($d1->format(\DateTimeInterface::RFC3339_EXTENDED)); // 2020-09-27T03:05:00.000+13:00
var_dump($d2->format(\DateTimeInterface::RFC3339_EXTENDED)); // 2020-09-27T01:59:00.000+12:00
I've updated your functions:
function time_is_older_than($datetime, $time_string)
{
$d1 = new \DateTime($datetime);
$d1->setTimezone(new \DateTimeZone('UTC'));
$d2 = new \DateTime();
$d2->setTimezone(new \DateTimeZone('UTC'));
$d2->modify('-' . $time_string);
return $d1 < $d2;
}
function time_is_younger_than($datetime, $time_string)
{
$d1 = new \DateTime($datetime);
$d1->setTimezone(new \DateTimeZone('UTC'));
$d2 = new \DateTime();
$d2->setTimezone(new \DateTimeZone('UTC'));
$d2->modify('-' . $time_string);
return $d1 > $d2;
}
Could you consider a solution:
In the timestamp string(like Thu, 21 Dec 2000 16:01:07 +0200), add a timezone tag which specify timezone without difference of daylight saving time.
My understanding is that datetimes in php are represented as the number of milliseconds after a certain date (some time in 1960 I think?). How to I construct a datetime that represents the earliest allowable date in php? An example possible syntax would be:
$date = new DateTime(0);
but this doesn't work. Is there some other way to do this?
Thanks for any input.
You're pretty close
$date = (new DateTime())->setTimestamp(0);
Will give January 1st, 1970
I think the smallest and largest dates that the DateTime object will accept are as follows (on a 64 bit machine as of PHP 7.4, demonstrated using PHPUnit). This can be useful in providing default mins and maxes on a date validator for both DateTime as well as Carbon. This answer is also posted in the user contributed notes of the PHP manual page for DateTime::__construct().
If you want to get very precise about it, modify the code below to account for time and timezone.
// smallest date
$input = '-9999-01-01';
$dt = new \DateTime($input);
self::assertEquals($input, $dt->format('Y-m-d'));
$input = '-10000-12-31';
$dt = new \DateTime($input);
self::assertEquals('2000-12-31', $dt->format('Y-m-d'));
// largest date
$input = '9999-12-31';
$dt = new \DateTime($input);
self::assertEquals($input, $dt->format('Y-m-d'));
$input = '10000-01-01';
$dt = new \DateTime($input);
self::assertEquals('2000-01-01', $dt->format('Y-m-d'));
echo date('d-m-Y', 0); // outputs: 01-01-1970
epoch 0 gives the unix timestamp 01-01-1970 or 00:00:00 UTC on January 1st 1970.
I want to add an x number of week days (e.g. 48 weekday hours) to the current timestamp. I am trying to do this using the following
echo (strtotime('2 weekdays');
However, this doesn't seem to take me an exact 48 hours ahead in time. For example, inputting the current server time of Tuesday 18/03/2014 10:47 returns Thursday 20/03/2014 00:00. using the following function:
echo (strtotime('2 weekdays')-mktime())/86400;
It can tell that it's returning only 1.3 weekdays from now.
Why is it doing this? Are there any existing functions which allow an exact amount of weekday hours?
Given you want to preserve the weekdays functionality and not loose the hours, minutes and seconds, you could do this:
$now = new DateTime();
$hms = new DateInterval(
'PT'.$now->format('H').'H'.
$now->format('i').'M'.
$now->format('s').'S'.
);
$date = new DateTime('2 weekdays');
$date->add($hms);//add hours here again
The reason why weekday doesn't add the hours is because, if you add 1 weekday at any point in time on a monday, the next weekday has to be tuesday.
The hour simply does not matter. Say your date is 2014-01-02 12:12:12, and you want the next weekday, that day starts at 2014-01-03 00:00:00, so that's what you get.
My last solution works though, and here's how: I use the $now instance of DateTime, and its format method to construct a DateInterval format string, to be passed to the constructor. An interval format is quite easy: it starts with P, for period, then a digit and a char to indicate what that digit represents: 1Y for 1 Year, and 2D for 2 Days.
However, we're only interested in hours, minutes and seconds. Actual time, which is indicated using a T in the interval format string, hence we start the string with PT (Period Time).
Using the format specifiers H, i and s, we construct an interval format that in the case of 12:12:12 looks like this:
$hms = new DateInterval(
'PT12H12M12S'
);
Then, it's a simple matter of calling the DateTime::add method to add the hours, minutes and seconds to our date + weekdays:
$weekdays = new DateTime('6 weekdays');
$weekdays->add($hms);
echo $weekdays->format('Y-m-d H:i:s'), PHP_EOL;
And you're there.
Alternatively, you could just use the basic same trick to compute the actual day-difference between your initial date, and that date + x weekdays, and then add that diff to your initial date. It's the same basic principle, but instead of having to create a format like PTXHXMXS, a simple PXD will do.
Working example here
I'd urge you to use the DateInterface classes, as it is more flexible, allows for type-hinting to be used and makes dealing with dates just a whole lot easier for all of us. Besides, it's not too different from your current code:
$today = new DateTime;
$tomorrow = new DateTime('tomorrow');
$dayAfter = new DateTime('2 days');
In fact, it's a lot easier if you want to do frequent date manipulations on a single date:
$date = new DateTime();//or DateTime::createFromFormat('Y-m-d H:i:s', $dateString);
$diff = new DateInterval('P2D');//2 days
$date->add($diff);
echo $date->format('Y-m-d H:i:s'), PHP_EOL, 'is the date + 2 days', PHP_EOL;
$date->sub($diff);
echo $date->format('Y-m-d H:i:s'), PHP_EOL, 'was the original date, now restored';
Easy, once you've spent some time browsing through the docs
I think I have found a solution. It's primitive but after some quick testing it seems to work.
The function calculates the time passed since midnight of the current day, and adds it onto the date returned by strtotime. Since this could fall into a weekend day, I've checked and added an extra day or two accordingly.
function weekDays($days) {
$tstamp = (strtotime($days.' weekdays') + (time() - strtotime("today")));
if(date('D',$tstamp) == 'Sat') {
$tstamp = $tstamp + 86400*2;
}
elseif(date('D',$tstamp) == 'Sun') {
$tstamp = $tstamp + 86400;
}
return $tstamp;
}
Function strtotime('2 weekdays') seems to add 2 weekdays to the current date without the time.
If you want to add 48 hours why not adding 2*24*60*60 to mktime()?
echo(date('Y-m-d', mktime()+2*24*60*60));
The currently accepted solution works, but it will fail when you want to add weekdays to a timestamp that is not now. Here's a simpler snippet that will work for any given point in time:
$start = new DateTime('2021-09-29 15:12:10');
$start->add(date_interval_create_from_date_string('+ 3 weekdays'));
echo $start->format('Y-m-d H:i:s'); // 2021-10-04 15:12:10
Note that this will also work for a negative amount of weekdays:
$start = new DateTime('2021-09-29 15:12:10');
$start->add(date_interval_create_from_date_string('- 3 weekdays'));
echo $start->format('Y-m-d H:i:s'); // 2021-09-24 15:12:10
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
In PHP, you can tell if a given date is during the Daylight Savings Time period by using something like this:
$isDST = date("I", $myDate); // 1 or 0
The problem is that this only tells you whether that one point in time is in DST. Is there a reliable way to check whether DST is in effect at any time in that timezone?
Edit to clarify:
Brisbane, Australia does not observe daylight savings at any time of the year. All year around, it is GMT+10.
Sydney, Australia does, from October to March when it changes from GMT+10 to GMT+11.
I'm wondering if there would be some existing method, or a way to implement a method which works as such:
timezoneDoesDST('Australia/Brisbane'); // false
timezoneDoesDST('Australia/Sydney'); // true
I've found a method which works using PHP's DateTimezone class (PHP 5.2+)
function timezoneDoesDST($tzId) {
$tz = new DateTimeZone($tzId);
$trans = $tz->getTransitions();
return ((count($trans) && $trans[count($trans) - 1]['ts'] > time()));
}
or, if you're running PHP 5.3+
function timezoneDoesDST($tzId) {
$tz = new DateTimeZone($tzId);
return count($tz->getTransitions(time())) > 0;
}
The getTransitions() function gives you information about each time the offset changes for a timezone. This includes historical data (Brisbane had daylight savings in 1916.. who knew?), so this function checks if there's an offset change in the future or not.
Actually nickf method didn't works for me so I reworked it a little ...
/**
* Finds wherever a TZ is experimenting dst or not
* #author hertzel Armengol <emudojo # gmail.com>
* #params string TimeZone -> US/Pacific for example
*
*/
function timezoneExhibitsDST($tzId) {
$tz = new DateTimeZone($tzId);
$date = new DateTime("now",$tz);
$trans = $tz->getTransitions();
foreach ($trans as $k => $t)
if ($t["ts"] > $date->format('U')) {
return $trans[$k-1]['isdst'];
}
}
// Usage
var_dump(timezoneExhibitsDST("US/Pacific")); --> prints false
var_dump(timezoneExhibitsDST("Europe/London")); --> prints false
var_dump(timezoneExhibitsDST("America/Chicago")); --> prints false
same function call will return true in 1 month (March) hope it helps
DateTimeZone::getTransitions might help.
You could probably wing it:
$hasDst = date("I", strtotime('June 1')) !== date("I", strtotime('Jan 1'));
Otherwise you'd need to parse the text-based zoneinfo data files.
I don't think so, but since almost every country that observes DST changes its time for an entire season or two, you could try to test 4 points during any given year.
For example, test date("I", $date) for 2009/01/01, 2009/04/01, 2009/07/01 and 2009/10/01. If that timezone falls into DST, then at least one of those dates will return 1.
date has to be on the user/server timezone for it to work, and you can't use a range with date as you do with getTransitions