php / php-src

The PHP Interpreter
https://www.php.net
Other
38.2k stars 7.75k forks source link

Evalute U-timestamp in createFromFormat as GMT timestamp #16277

Open kylekatarnls opened 1 month ago

kylekatarnls commented 1 month ago

Description

The following code:

<?php
echo DateTime::createFromFormat('U e', '1730597400 America/Toronto')->format('U');

Resulted in this output:

1730615400

It's 5 hours more than the input timestamp.

But I expected this output instead:

1730597400

The difference matches the timezone offset.

But documentation on createFromFormat about U says:

Seconds since the Unix Epoch (January 1 1970 00:00:00 GMT)

GMT is specified, so I expect the timestamp to be constant whatever is the timezone.

I.E. I expect DateTime::createFromFormat('U e', '1730597400 America/Toronto') to be equivalent to:

$date = DateTime::createFromFormat('U', '1730597400');
$date->setTimezone(new DateTimeZone('America/Toronto'));

PHP Version

PHP 8.3.12

heiglandreas commented 1 month ago

https://3v4l.org/KugCQ

It looks indeed that, when using the createFromFormat, the timestamp is set and then the timezone is replaced without adapting the internal datetime.

damianwadley commented 1 month ago

Keeping in mind that I agree there is a bug here, some quick nit-picking:

I.E. I expect DateTime::createFromFormat('U e', '1730597400 America/Toronto') to be equivalent to:

$date = DateTime::createFromFormat('U', '1730597400');
$date->setTimezone(new DateTimeZone('America/Toronto'));

Actually no, that's not quite how it works. Specifying a timezone indicates how to interpret the string, which could be very different from the default timezone, while setTimezone will modify (the representation of) the date to match the given timezone.

// 12:00:00 according to the default timezone
$date = DateTime::createFromFormat('Y-m-d H:i:s', '2024-10-07 12:00:00');
var_dump($date); // 12:00:00 of the default timezone

// 12:00:00 according to America/Toronto
$date = DateTime::createFromFormat('Y-m-d H:i:s e', '2024-10-07 12:00:00 America/Toronto');
var_dump($date); // 12:00:00 of America/Toronto

// 12:00:00 according to the default timezone
$date = DateTime::createFromFormat('Y-m-d H:i:s', '2024-10-07 12:00:00');
$date->setTimezone(new DateTimeZone('America/Toronto'));
var_dump($date); // ??:00:00 of America/Toronto

As noted, the first and third example should use the same underlying timestamp, while the second should use a different timestamp (if the default timezone is in a different offset than America/Toronto, of course).

In the case of a Unix timestamp, which doesn't have a concept of a timezone, all three examples should be the same. Given UTC as the default timezone:

// 1730597400 = 01:30:00 according to UTC
$date = DateTime::createFromFormat('U', '1730597400');
var_dump($date); // 01:30:00 of UTC

// 1730597400 = 21:30:00 according to America/Toronto
$date = DateTime::createFromFormat('U e', '1730597400 America/Toronto');
var_dump($date); // 21:30:00 of America/Toronto

// 1730597400 = 01:30:00 according to UTC
$date = DateTime::createFromFormat('U', '1730597400');
$date->setTimezone(new DateTimeZone('America/Toronto'));
var_dump($date); // 21:30:00 of America/Toronto

And https://3v4l.org/e2eFe agrees that there is a bug: the second example is shifting the time when it shouldn't be. But the explanation of why it's a bug is not because "it isn't behaving the same as ->setTimezone".

kylekatarnls commented 1 month ago

@damianwadley I don't say it should be equivalent for any string, but it must be equivalent for U because the doc explicitly states that U is a GMT timestamp, so putting Toronto after a timestamp must not give you a "Toronto timestamp" (timezeoned timestamps should probably not being considered as a thing).

And https://3v4l.org/e2eFe agrees that there is a bug: the second example is shifting the time when it shouldn't be.

I'm not sure I get this part. If you are saying that DateTime::createFromFormat('U', '1730597400')->setTimezone(new DateTimeZone('America/Toronto'))->format('U'); should output something else than 1730597400, then I strongly disagree, changing timezone must not change the timestamp, it does not depend on the timezone since it's the number of seconds since January 1 1970 00:00:00 GMT which is the same instant wherever you are in the world.

damianwadley commented 1 month ago

the doc explicitly states that U is a GMT timestamp

Unix timestamps do not have timezones. "A GMT [Unix] timestamp" isn't a thing. What the docs are saying is that it's a timestamp measured from "12am on January 1st, 1970" according to GMT. It could say "9pm on December 31st, 1969" according to US East, or "3am on January 1st, 1970" according to Uganda, and it would mean the same thing. (But it doesn't because nobody ever thinks that way about the epoch.)

I'm not sure I get this part.

The Unix timestamp for all three of them should be the same. Timezones are irrelevant when it comes a timestamp, so nothing you do with a DateTime's timezone should have any effect. However, the var_dump of a DateTime does not show timestamps - it shows string representations. Because it's easier for humans to understand those representations than raw 10-digit numbers. So timezones are relevant to that output.

kylekatarnls commented 1 month ago

That's what I said. And that's why the expected behavior in my original post is the correct one.

I don't get the point about var_dumping the object. The only output I'm talking in this issue is the result of ->format('U')

BTW your example:

$date = DateTime::createFromFormat('U e', '1730597400 America/Toronto');
var_dump($date); // 21:30:00 of America/Toronto

Is what I expect too but is not what is happening right now, as you can see https://3v4l.org/C1Tna it gives 01:30 of America/Toronto and so this is part of the bug to be fixed.

The point about how setTimezone works vs. how passing timezone in the constructor string is likely off-topic. I don't need to be convinced that they are working differently, I'm perfectly aligned with that, I don't say both should work the same way under the hood, I gave those 2 codes, as they both should return create DateTime with America/Toronto timezone and both should return 1730597400 out of ->getTimestamp(), the end result for both snippets should then be equivalent.

But I didn't state anything about how it should produce this result, just that this is what it should produce.

I hope that we all agree on this result, as this is precisely because timezone is irrelevant when working with timestamp that it is the expected result.

hormus commented 1 month ago

If timezone is omitted or null and datetime contains no timezone, the current timezone will be used. Note:

The timezone parameter and the current timezone are ignored when the datetime parameter either contains a UNIX timestamp (e.g. 946684800) or specifies a timezone (e.g. 2010-01-28T15:00:00+02:00).

kylekatarnls commented 1 month ago

Yes, again, that's also the behavior that I expect, but as show in the exemple, this is not what PHP is currently doing/returning.