blob: b4d16ba97a6789a177e58d846d14a895fbddbad3 [file] [log] [blame]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001<?php
2
3/**
4 * This file is part of the Carbon package.
5 *
6 * (c) Brian Nesbitt <brian@nesbot.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010011
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020012namespace Carbon;
13
14use Carbon\Exceptions\InvalidCastException;
15use Carbon\Exceptions\InvalidTimeZoneException;
16use DateTimeInterface;
17use DateTimeZone;
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010018use Throwable;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020019
20class CarbonTimeZone extends DateTimeZone
21{
22 public function __construct($timezone = null)
23 {
24 parent::__construct(static::getDateTimeZoneNameFromMixed($timezone));
25 }
26
27 protected static function parseNumericTimezone($timezone)
28 {
29 if ($timezone <= -100 || $timezone >= 100) {
30 throw new InvalidTimeZoneException('Absolute timezone offset cannot be greater than 100.');
31 }
32
33 return ($timezone >= 0 ? '+' : '').$timezone.':00';
34 }
35
36 protected static function getDateTimeZoneNameFromMixed($timezone)
37 {
38 if ($timezone === null) {
39 return date_default_timezone_get();
40 }
41
42 if (\is_string($timezone)) {
43 $timezone = preg_replace('/^\s*([+-]\d+)(\d{2})\s*$/', '$1:$2', $timezone);
44 }
45
46 if (is_numeric($timezone)) {
47 return static::parseNumericTimezone($timezone);
48 }
49
50 return $timezone;
51 }
52
53 protected static function getDateTimeZoneFromName(&$name)
54 {
55 return @timezone_open($name = (string) static::getDateTimeZoneNameFromMixed($name));
56 }
57
58 /**
59 * Cast the current instance into the given class.
60 *
61 * @param string $className The $className::instance() method will be called to cast the current object.
62 *
63 * @return DateTimeZone
64 */
65 public function cast(string $className)
66 {
67 if (!method_exists($className, 'instance')) {
68 if (is_a($className, DateTimeZone::class, true)) {
69 return new $className($this->getName());
70 }
71
72 throw new InvalidCastException("$className has not the instance() method needed to cast the date.");
73 }
74
75 return $className::instance($this);
76 }
77
78 /**
79 * Create a CarbonTimeZone from mixed input.
80 *
81 * @param DateTimeZone|string|int|null $object original value to get CarbonTimeZone from it.
82 * @param DateTimeZone|string|int|null $objectDump dump of the object for error messages.
83 *
84 * @throws InvalidTimeZoneException
85 *
86 * @return false|static
87 */
88 public static function instance($object = null, $objectDump = null)
89 {
90 $tz = $object;
91
92 if ($tz instanceof static) {
93 return $tz;
94 }
95
96 if ($tz === null) {
97 return new static();
98 }
99
100 if (!$tz instanceof DateTimeZone) {
101 $tz = static::getDateTimeZoneFromName($object);
102 }
103
104 if ($tz === false) {
105 if (Carbon::isStrictModeEnabled()) {
106 throw new InvalidTimeZoneException('Unknown or bad timezone ('.($objectDump ?: $object).')');
107 }
108
109 return false;
110 }
111
112 return new static($tz->getName());
113 }
114
115 /**
116 * Returns abbreviated name of the current timezone according to DST setting.
117 *
118 * @param bool $dst
119 *
120 * @return string
121 */
122 public function getAbbreviatedName($dst = false)
123 {
124 $name = $this->getName();
125
126 foreach ($this->listAbbreviations() as $abbreviation => $zones) {
127 foreach ($zones as $zone) {
128 if ($zone['timezone_id'] === $name && $zone['dst'] == $dst) {
129 return $abbreviation;
130 }
131 }
132 }
133
134 return 'unknown';
135 }
136
137 /**
138 * @alias getAbbreviatedName
139 *
140 * Returns abbreviated name of the current timezone according to DST setting.
141 *
142 * @param bool $dst
143 *
144 * @return string
145 */
146 public function getAbbr($dst = false)
147 {
148 return $this->getAbbreviatedName($dst);
149 }
150
151 /**
152 * Get the offset as string "sHH:MM" (such as "+00:00" or "-12:30").
153 *
154 * @param DateTimeInterface|null $date
155 *
156 * @return string
157 */
158 public function toOffsetName(DateTimeInterface $date = null)
159 {
160 return static::getOffsetNameFromMinuteOffset(
161 $this->getOffset($date ?: Carbon::now($this)) / 60
162 );
163 }
164
165 /**
166 * Returns a new CarbonTimeZone object using the offset string instead of region string.
167 *
168 * @param DateTimeInterface|null $date
169 *
170 * @return CarbonTimeZone
171 */
172 public function toOffsetTimeZone(DateTimeInterface $date = null)
173 {
174 return new static($this->toOffsetName($date));
175 }
176
177 /**
178 * Returns the first region string (such as "America/Toronto") that matches the current timezone or
179 * false if no match is found.
180 *
181 * @see timezone_name_from_abbr native PHP function.
182 *
183 * @param DateTimeInterface|null $date
184 * @param int $isDst
185 *
186 * @return string|false
187 */
188 public function toRegionName(DateTimeInterface $date = null, $isDst = 1)
189 {
190 $name = $this->getName();
191 $firstChar = substr($name, 0, 1);
192
193 if ($firstChar !== '+' && $firstChar !== '-') {
194 return $name;
195 }
196
197 $date = $date ?: Carbon::now($this);
198
199 // Integer construction no longer supported since PHP 8
200 // @codeCoverageIgnoreStart
201 try {
202 $offset = @$this->getOffset($date) ?: 0;
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100203 } catch (Throwable $e) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200204 $offset = 0;
205 }
206 // @codeCoverageIgnoreEnd
207
208 $name = @timezone_name_from_abbr('', $offset, $isDst);
209
210 if ($name) {
211 return $name;
212 }
213
214 foreach (timezone_identifiers_list() as $timezone) {
215 if (Carbon::instance($date)->tz($timezone)->getOffset() === $offset) {
216 return $timezone;
217 }
218 }
219
220 return false;
221 }
222
223 /**
224 * Returns a new CarbonTimeZone object using the region string instead of offset string.
225 *
226 * @param DateTimeInterface|null $date
227 *
228 * @return CarbonTimeZone|false
229 */
230 public function toRegionTimeZone(DateTimeInterface $date = null)
231 {
232 $tz = $this->toRegionName($date);
233
234 if ($tz === false) {
235 if (Carbon::isStrictModeEnabled()) {
236 throw new InvalidTimeZoneException('Unknown timezone for offset '.$this->getOffset($date ?: Carbon::now($this)).' seconds.');
237 }
238
239 return false;
240 }
241
242 return new static($tz);
243 }
244
245 /**
246 * Cast to string (get timezone name).
247 *
248 * @return string
249 */
250 public function __toString()
251 {
252 return $this->getName();
253 }
254
255 /**
256 * Create a CarbonTimeZone from mixed input.
257 *
258 * @param DateTimeZone|string|int|null $object
259 *
260 * @return false|static
261 */
262 public static function create($object = null)
263 {
264 return static::instance($object);
265 }
266
267 /**
268 * Create a CarbonTimeZone from int/float hour offset.
269 *
270 * @param float $hourOffset number of hour of the timezone shift (can be decimal).
271 *
272 * @return false|static
273 */
274 public static function createFromHourOffset(float $hourOffset)
275 {
276 return static::createFromMinuteOffset($hourOffset * Carbon::MINUTES_PER_HOUR);
277 }
278
279 /**
280 * Create a CarbonTimeZone from int/float minute offset.
281 *
282 * @param float $minuteOffset number of total minutes of the timezone shift.
283 *
284 * @return false|static
285 */
286 public static function createFromMinuteOffset(float $minuteOffset)
287 {
288 return static::instance(static::getOffsetNameFromMinuteOffset($minuteOffset));
289 }
290
291 /**
292 * Convert a total minutes offset into a standardized timezone offset string.
293 *
294 * @param float $minutes number of total minutes of the timezone shift.
295 *
296 * @return string
297 */
298 public static function getOffsetNameFromMinuteOffset(float $minutes): string
299 {
300 $minutes = round($minutes);
301 $unsignedMinutes = abs($minutes);
302
303 return ($minutes < 0 ? '-' : '+').
304 str_pad((string) floor($unsignedMinutes / 60), 2, '0', STR_PAD_LEFT).
305 ':'.
306 str_pad((string) ($unsignedMinutes % 60), 2, '0', STR_PAD_LEFT);
307 }
308}