1: <?php
2: declare(strict_types=1);
3:
4: namespace om\RRule;
5:
6: use DateTime;
7: use DateTimeZone;
8:
9: /**
10: * Converts a local (wall-clock) time to an instant as RFC 5545, section 3.3.5 requires:
11: * an ambiguous time (DST fall-back) is the first occurrence, a nonexistent time (DST gap)
12: * uses the UTC offset before the gap. PHP itself is not consistent in these cases.
13: */
14: final class LocalTime {
15: /**
16: * @param int $local the wall-clock time as seconds since 1970-01-01 00:00 (as if it were UTC)
17: */
18: public static function timestamp(DateTimeZone $timezone, int $local): int {
19: static $probe;
20: $probe ??= new DateTime('@0');
21: $offsetAt = static fn(int $timestamp): int => $timezone->getOffset($probe->setTimestamp($timestamp));
22:
23: // there is at most one transition within two days
24: $before = $offsetAt($local - 2 * 86400);
25: $after = $offsetAt($local + 2 * 86400);
26: if ($before === $after && $offsetAt($local - $before) === $before) {
27: return $local - $before;
28: }
29:
30: $valid = [];
31: foreach ([$before, $after] as $offset) {
32: $timestamp = $local - $offset;
33: if ($offsetAt($timestamp) === $offset) {
34: $valid[] = $timestamp;
35: }
36: }
37: if ($valid !== []) {
38: return min($valid); // the first of repeated times
39: }
40: // a nonexistent time is interpreted with the offset before the gap
41: return $local - $before;
42: }
43: }
44: