PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / trunk
Matomo Analytics – Powerful, Privacy-First Insights for WordPress vtrunk
5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / core / Date.php
matomo / app / core Last commit date
API 1 month ago Access 3 months ago Application 1 month ago Archive 1 month ago ArchiveProcessor 1 month ago Archiver 2 years ago AssetManager 1 month ago Auth 6 months ago Category 6 months ago Changes 1 month ago CliMulti 1 year ago Columns 1 month ago Concurrency 1 month ago Config 1 month ago Container 1 month ago CronArchive 3 months ago DataAccess 1 month ago DataFiles 2 years ago DataTable 2 weeks ago Db 2 weeks ago DeviceDetector 1 year ago Email 2 years ago Exception 4 months ago Http 4 months ago Intl 3 months ago Log 2 years ago Mail 1 year ago Measurable 6 months ago Menu 1 month ago Metrics 3 months ago Notification 6 months ago Period 1 month ago Plugin 2 weeks ago Policy 1 month ago ProfessionalServices 1 year ago Report 1 year ago ReportRenderer 3 months ago Request 3 months ago Scheduler 1 month ago Segment 1 month ago Session 2 weeks ago Settings 1 month ago Tracker 2 weeks ago Translation 1 month ago Twig 1 year ago UpdateCheck 3 months ago Updater 1 month ago Updates 2 days ago Validators 1 year ago View 1 month ago ViewDataTable 2 weeks ago Visualization 1 year ago Widget 1 month ago .htaccess 2 years ago Access.php 1 month ago Archive.php 1 month ago ArchiveProcessor.php 1 month ago AssetManager.php 1 month ago Auth.php 6 months ago AuthResult.php 6 months ago BaseFactory.php 2 years ago Cache.php 2 years ago CacheId.php 4 months ago CliMulti.php 1 month ago Common.php 2 weeks ago Config.php 1 month ago Console.php 3 months ago Context.php 2 years ago Cookie.php 1 year ago CronArchive.php 1 month ago DI.php 3 months ago DataArray.php 1 month ago DataTable.php 1 month ago Date.php 1 month ago Db.php 1 month ago DbHelper.php 1 month ago Development.php 1 year ago ErrorHandler.php 6 months ago EventDispatcher.php 1 month ago ExceptionHandler.php 4 months ago FileIntegrity.php 1 month ago Filechecks.php 1 year ago Filesystem.php 1 month ago FrontController.php 4 months ago Http.php 1 month ago IP.php 1 year ago Log.php 3 months ago LogDeleter.php 1 year ago Mail.php 1 year ago Metrics.php 1 month ago NoAccessException.php 2 years ago Nonce.php 6 months ago Notification.php 1 month ago NumberFormatter.php 5 months ago Option.php 5 months ago Period.php 1 month ago Piwik.php 1 month ago Plugin.php 1 month ago Process.php 1 month ago Profiler.php 6 months ago ProxyHeaders.php 4 months ago ProxyHttp.php 5 months ago QuickForm2.php 3 months ago RankingQuery.php 1 month ago ReportRenderer.php 1 month ago Request.php 1 month ago Segment.php 1 month ago Sequence.php 6 months ago Session.php 2 weeks ago SettingsPiwik.php 1 month ago SettingsServer.php 1 year ago Singleton.php 2 years ago Site.php 1 month ago SiteContentDetector.php 1 month ago SupportedBrowser.php 2 years ago TCPDF.php 1 year ago Theme.php 1 year ago Timer.php 1 month ago Tracker.php 1 month ago Twig.php 1 month ago Unzip.php 1 year ago UpdateCheck.php 1 month ago Updater.php 1 month ago UpdaterErrorException.php 2 years ago Updates.php 3 months ago Url.php 3 months ago UrlHelper.php 1 month ago Version.php 2 days ago View.php 1 month ago bootstrap.php 1 year ago dispatch.php 2 years ago testMinimumPhpVersion.php 6 months ago
Date.php
1024 lines
1 <?php
2
3 /**
4 * Matomo - free/libre analytics platform
5 *
6 * @link https://matomo.org
7 * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
8 */
9 namespace Piwik;
10
11 use Exception;
12 use Piwik\Container\StaticContainer;
13 use Piwik\Intl\Data\Provider\DateTimeFormatProvider;
14 /**
15 * Utility class that wraps date/time related PHP functions. Using this class can
16 * be easier than using `date`, `time`, `date_default_timezone_set`, etc.
17 *
18 * ### Performance concerns
19 *
20 * The helper methods in this class are instance methods and thus `Date` instances
21 * need to be constructed before they can be used. The memory allocation can result
22 * in noticeable performance degradation if you construct thousands of Date instances,
23 * say, in a loop.
24 *
25 * ### Examples
26 *
27 * **Basic usage**
28 *
29 * $date = Date::factory('2007-07-24 14:04:24', 'EST');
30 * $date->addHour(5);
31 * echo $date->getLocalized("EEE, d. MMM y 'at' HH:mm:ss");
32 *
33 * @api
34 */
35 class Date
36 {
37 /** Number of seconds in a day. */
38 public const NUM_SECONDS_IN_DAY = 86400;
39 /** The default date time string format. */
40 public const DATE_TIME_FORMAT = 'Y-m-d H:i:s';
41 /** Timestamp when first website came online - Tue, 06 Aug 1991 00:00:00 GMT. */
42 public const FIRST_WEBSITE_TIMESTAMP = 681436800;
43 public const DATETIME_FORMAT_LONG = DateTimeFormatProvider::DATE_FORMAT_LONG;
44 public const DATETIME_FORMAT_SHORT = DateTimeFormatProvider::DATETIME_FORMAT_SHORT;
45 public const DATE_FORMAT_LONG = DateTimeFormatProvider::DATE_FORMAT_LONG;
46 public const DATE_FORMAT_DAY_MONTH = DateTimeFormatProvider::DATE_FORMAT_DAY_MONTH;
47 public const DATE_FORMAT_SHORT = DateTimeFormatProvider::DATE_FORMAT_SHORT;
48 public const DATE_FORMAT_MONTH_SHORT = DateTimeFormatProvider::DATE_FORMAT_MONTH_SHORT;
49 public const DATE_FORMAT_MONTH_LONG = DateTimeFormatProvider::DATE_FORMAT_MONTH_LONG;
50 public const DATE_FORMAT_YEAR = DateTimeFormatProvider::DATE_FORMAT_YEAR;
51 public const TIME_FORMAT = DateTimeFormatProvider::TIME_FORMAT;
52 /** for tests
53 * @var null|int
54 */
55 public static $now = null;
56 /**
57 * Max days for months (non-leap-year). See {@link addPeriod()} implementation.
58 *
59 * @var array<int, int>
60 */
61 private static $maxDaysInMonth = array(1 => 31, 2 => 28, 3 => 31, 4 => 30, 5 => 31, 6 => 30, 7 => 31, 8 => 31, 9 => 30, 10 => 31, 11 => 30, 12 => 31);
62 /**
63 * The stored timestamp is always UTC based.
64 * The returned timestamp via getTimestamp() will have the conversion applied
65 * @var int|null
66 */
67 protected $timestamp = null;
68 /**
69 * Timezone the current date object is set to.
70 * Timezone will only affect the returned timestamp via getTimestamp()
71 * @var string
72 */
73 protected $timezone = 'UTC';
74 /**
75 * @param int $timestamp The number in seconds since the unix epoch.
76 * @param string $timezone The timezone of the datetime.
77 * @throws Exception If $timestamp is not an int.
78 */
79 protected function __construct($timestamp, $timezone = 'UTC')
80 {
81 if (!is_int($timestamp)) {
82 throw new Exception("Date is expecting a unix timestamp, got: '{$timestamp}'.");
83 }
84 $this->timezone = $timezone;
85 $this->timestamp = $timestamp;
86 }
87 /**
88 * Creates a new Date instance using a string datetime value. The timezone of the Date
89 * result will be in UTC.
90 *
91 * @param string|int|Date $dateString `'today'`, `'yesterday'`, `'now'`, `'yesterdaySameTime'`, a string with
92 * `'YYYY-MM-DD HH:MM:SS'` format or a unix timestamp.
93 * @param string $timezone The timezone of the result. If specified, `$dateString` will be converted
94 * from UTC to this timezone before being used in the Date return value.
95 * @return Date
96 * @throws Exception If `$dateString` is in an invalid format or if the time is before
97 * Tue, 06 Aug 1991.
98 */
99 public static function factory($dateString, $timezone = null)
100 {
101 if ($dateString instanceof self) {
102 return new \Piwik\Date($dateString->timestamp, $dateString->timezone);
103 }
104 if ($dateString === 'now') {
105 $date = self::now();
106 } elseif ($dateString === 'today') {
107 $date = self::today();
108 } elseif ($dateString === 'tomorrow') {
109 $date = self::tomorrow();
110 } elseif ($dateString === 'yesterday') {
111 $date = self::yesterday();
112 } elseif ($dateString === 'yesterdaySameTime') {
113 $date = self::yesterdaySameTime();
114 } elseif (is_string($dateString) && preg_match('/last[ -]?week/i', urldecode($dateString))) {
115 $date = self::lastWeek();
116 } elseif (is_string($dateString) && preg_match('/last[ -]?month/i', urldecode($dateString))) {
117 $date = self::lastMonth();
118 } elseif (is_string($dateString) && preg_match('/last[ -]?year/i', urldecode($dateString))) {
119 $date = self::lastYear();
120 } elseif (!is_int($dateString) && (!is_string($dateString) || strpos($dateString, ',') !== \false || ($dateString = strtotime($dateString)) === \false)) {
121 throw self::getInvalidDateFormatException($dateString);
122 } else {
123 $date = new \Piwik\Date($dateString);
124 }
125 $timestamp = $date->getTimestamp();
126 if ($timestamp < self::FIRST_WEBSITE_TIMESTAMP) {
127 $dateOfFirstWebsite = new self(self::FIRST_WEBSITE_TIMESTAMP);
128 $message = \Piwik\Piwik::translate('General_ExceptionInvalidDateBeforeFirstWebsite', array($date->toString(), $dateOfFirstWebsite->getLocalized(self::DATE_FORMAT_SHORT), $dateOfFirstWebsite->getTimestamp()));
129 throw new Exception($message . ": {$dateString}");
130 }
131 if (empty($timezone)) {
132 return $date;
133 }
134 $timestamp = self::adjustForTimezone($timestamp, $timezone);
135 return \Piwik\Date::factory($timestamp);
136 }
137 /**
138 * Returns Date w/ UTC timestamp of time $dateString/$timezone.
139 * (Only applies to special strings, like 'now','today','yesterday','yesterdaySameTime'.
140 *
141 * @param string $dateString
142 * @param string $timezone
143 * @return Date
144 * @ignore
145 */
146 public static function factoryInTimezone($dateString, $timezone)
147 {
148 if ($dateString === 'now') {
149 return self::nowInTimezone((string) $timezone);
150 } elseif ($dateString === 'today') {
151 return self::todayInTimezone((string) $timezone);
152 } elseif ($dateString === 'yesterday') {
153 return self::yesterdayInTimezone((string) $timezone);
154 } elseif ($dateString === 'yesterdaySameTime') {
155 return self::yesterdaySameTimeInTimezone((string) $timezone);
156 } elseif (preg_match('/last[ -]?week/i', urldecode($dateString))) {
157 return self::lastWeekInTimezone((string) $timezone);
158 } elseif (preg_match('/last[ -]?month/i', urldecode($dateString))) {
159 return self::lastMonthInTimezone((string) $timezone);
160 } elseif (preg_match('/last[ -]?year/i', urldecode($dateString))) {
161 return self::lastYearInTimezone((string) $timezone);
162 } else {
163 throw new \Exception("Date::factoryInTimezone() should not be used with {$dateString}.");
164 }
165 }
166 private static function nowInTimezone(string $timezone) : \Piwik\Date
167 {
168 $now = self::getNowTimestamp();
169 $now = self::adjustForTimezone($now, $timezone);
170 return new \Piwik\Date($now);
171 }
172 private static function todayInTimezone(string $timezone) : \Piwik\Date
173 {
174 return self::nowInTimezone($timezone)->getStartOfDay();
175 }
176 private static function yesterdayInTimezone(string $timezone) : \Piwik\Date
177 {
178 return self::todayInTimezone($timezone)->subDay(1);
179 }
180 private static function yesterdaySameTimeInTimezone(string $timezone) : \Piwik\Date
181 {
182 return self::nowInTimezone($timezone)->subDay(1);
183 }
184 private static function lastWeekInTimezone(string $timezone) : \Piwik\Date
185 {
186 return new \Piwik\Date(strtotime('-1week', self::todayInTimezone($timezone)->getTimestamp()));
187 }
188 private static function lastMonthInTimezone(string $timezone) : \Piwik\Date
189 {
190 return new \Piwik\Date(strtotime('-1month', self::todayInTimezone($timezone)->getTimestamp()));
191 }
192 private static function lastYearInTimezone(string $timezone) : \Piwik\Date
193 {
194 return new \Piwik\Date(strtotime('-1year', self::todayInTimezone($timezone)->getTimestamp()));
195 }
196 /**
197 * Returns the current timestamp as a string with the following format: `'YYYY-MM-DD HH:MM:SS'`.
198 *
199 * @return string
200 */
201 public function getDatetime()
202 {
203 return $this->toString(self::DATE_TIME_FORMAT);
204 }
205 /**
206 * Returns the current hour in UTC timezone.
207 * @return string
208 * @throws Exception
209 */
210 public function getHourUTC()
211 {
212 $dateTime = $this->getDatetime();
213 $hourInTz = \Piwik\Date::factory($dateTime, 'UTC')->toString('G');
214 return $hourInTz;
215 }
216 /**
217 * @return string
218 * @deprecated
219 */
220 public function getDateStartUTC()
221 {
222 return $this->getStartOfDay()->toString(self::DATE_TIME_FORMAT);
223 }
224 /**
225 * Returns the start of the day of the current timestamp in UTC. For example,
226 * if the current timestamp is `'2007-07-24 14:04:24'` in UTC, the result will
227 * be `'2007-07-24'` as a Date.
228 *
229 * @return Date
230 */
231 public function getStartOfDay()
232 {
233 $dateStartUTC = gmdate('Y-m-d', $this->timestamp);
234 return \Piwik\Date::factory($dateStartUTC)->setTimezone($this->timezone);
235 }
236 /**
237 * @return string
238 * @deprecated
239 */
240 public function getDateEndUTC()
241 {
242 return $this->getEndOfDay()->toString(self::DATE_TIME_FORMAT);
243 }
244 /**
245 * Returns the end of the day of the current timestamp in UTC. For example,
246 * if the current timestamp is `'2007-07-24 14:03:24'` in UTC, the result will
247 * be `'2007-07-24 23:59:59'`.
248 *
249 * @return Date
250 */
251 public function getEndOfDay()
252 {
253 $dateEndUTC = gmdate('Y-m-d 23:59:59', $this->timestamp);
254 return \Piwik\Date::factory($dateEndUTC)->setTimezone($this->timezone);
255 }
256 /**
257 * Returns a new date object with the same timestamp as `$this` but with a new
258 * timezone.
259 *
260 * See {@link getTimestamp()} to see how the timezone is used.
261 *
262 * @param string $timezone eg, `'UTC'`, `'Europe/London'`, etc.
263 * @return Date
264 */
265 public function setTimezone($timezone)
266 {
267 return new \Piwik\Date($this->timestamp, $timezone);
268 }
269 /**
270 * Returns the offset to UTC time for the given timezone
271 *
272 * @param string $timezone
273 * @return int offset in seconds
274 */
275 public static function getUtcOffset($timezone)
276 {
277 $timestampUTC = self::today()->getTimestampUTC();
278 $timestampZone = self::adjustForTimezone($timestampUTC, $timezone);
279 return $timestampZone - $timestampUTC;
280 }
281 /**
282 * Helper function that returns the offset in the timezone string 'UTC+14'
283 * Returns false if the timezone is not UTC+X or UTC-X
284 *
285 * @param string $timezone
286 * @return int|float|bool utc offset or false
287 */
288 protected static function extractUtcOffset($timezone)
289 {
290 if ($timezone === 'UTC') {
291 return 0;
292 }
293 $start = substr($timezone, 0, 4);
294 if ($start !== 'UTC-' && $start !== 'UTC+') {
295 return \false;
296 }
297 $offset = (float) substr($timezone, 4);
298 if ($start === 'UTC-') {
299 $offset = -$offset;
300 }
301 return $offset;
302 }
303 /**
304 * Converts a timestamp from UTC to a timezone.
305 *
306 * @param int $timestamp The UNIX timestamp to adjust.
307 * @param string $timezone The timezone to adjust to.
308 * @return int The adjusted time as seconds from EPOCH.
309 */
310 public static function adjustForTimezone($timestamp, $timezone)
311 {
312 if (empty($timezone)) {
313 return $timestamp;
314 }
315 // manually adjust for UTC timezones
316 $utcOffset = self::extractUtcOffset($timezone);
317 if ($utcOffset !== \false) {
318 return self::addHourTo($timestamp, $utcOffset);
319 }
320 date_default_timezone_set($timezone);
321 $datetime = date(self::DATE_TIME_FORMAT, $timestamp);
322 date_default_timezone_set('UTC');
323 return strtotime($datetime);
324 }
325 /**
326 * Returns the date in the "Y-m-d H:i:s" PHP format
327 *
328 * @param int $timestamp
329 * @return string
330 */
331 public static function getDatetimeFromTimestamp($timestamp)
332 {
333 return date("Y-m-d H:i:s", $timestamp);
334 }
335 /**
336 * Returns the Unix timestamp of the date in UTC.
337 *
338 * @return int
339 */
340 public function getTimestampUTC()
341 {
342 return $this->timestamp;
343 }
344 /**
345 * Returns the unix timestamp of the date in UTC, converted from the current
346 * timestamp timezone.
347 *
348 * @return int
349 */
350 public function getTimestamp()
351 {
352 if (empty($this->timezone)) {
353 $this->timezone = 'UTC';
354 }
355 $utcOffset = self::extractUtcOffset($this->timezone);
356 if ($utcOffset !== \false) {
357 return (int) ($this->timestamp - $utcOffset * 3600);
358 }
359 // The following code seems clunky - I thought the DateTime php class would allow to return timestamps
360 // after applying the timezone offset. Instead, the underlying timestamp is not changed.
361 // I decided to get the date without the timezone information, and create the timestamp from the truncated string.
362 // Unit tests pass (@see Date.test.php) but I'm pretty sure this is not the right way to do it
363 date_default_timezone_set($this->timezone);
364 $dtzone = timezone_open('UTC');
365 $time = date('r', $this->timestamp);
366 $dtime = date_create($time);
367 date_timezone_set($dtime, $dtzone);
368 $dateWithTimezone = date_format($dtime, 'r');
369 $dateWithoutTimezone = substr($dateWithTimezone, 0, -6);
370 $timestamp = strtotime($dateWithoutTimezone);
371 date_default_timezone_set('UTC');
372 return (int) $timestamp;
373 }
374 /**
375 * Returns `true` if the current date is older than the given `$date`.
376 *
377 * @return bool
378 */
379 public function isLater(\Piwik\Date $date)
380 {
381 return $this->getTimestamp() > $date->getTimestamp();
382 }
383 /**
384 * Returns `true` if the current date is earlier than the given `$date`.
385 *
386 * @return bool
387 */
388 public function isEarlier(\Piwik\Date $date)
389 {
390 return $this->getTimestamp() < $date->getTimestamp();
391 }
392 /**
393 * Returns `true` if the current year is a leap year, false otherwise.
394 *
395 * @return bool
396 */
397 public function isLeapYear()
398 {
399 $isLeap = (bool) date('L', $this->getTimestamp());
400 return $isLeap;
401 }
402 /**
403 * Converts this date to the requested string format. See {@link https://php.net/date}
404 * for the list of format strings.
405 *
406 * @param string $format
407 * @return string
408 */
409 public function toString($format = 'Y-m-d')
410 {
411 return date($format, $this->getTimestamp());
412 }
413 /**
414 * See {@link toString()}.
415 *
416 * @return string The current date in `'YYYY-MM-DD'` format.
417 */
418 public function __toString()
419 {
420 return $this->toString();
421 }
422 /**
423 * Performs three-way comparison of the week of the current date against the given `$date`'s week.
424 *
425 * @return int Returns `0` if the current week is equal to `$date`'s, `-1` if the current week is
426 * earlier or `1` if the current week is later.
427 */
428 public function compareWeek(\Piwik\Date $date)
429 {
430 $currentWeek = date('W', $this->getTimestamp());
431 $toCompareWeek = date('W', $date->getTimestamp());
432 if ($currentWeek == $toCompareWeek) {
433 return 0;
434 }
435 if ($currentWeek < $toCompareWeek) {
436 return -1;
437 }
438 return 1;
439 }
440 /**
441 * Performs three-way comparison of the month of the current date against the given `$date`'s month.
442 *
443 * @param \Piwik\Date $date Month to compare
444 * @return int Returns `0` if the current month is equal to `$date`'s, `-1` if the current month is
445 * earlier or `1` if the current month is later.
446 */
447 public function compareMonth(\Piwik\Date $date)
448 {
449 $currentMonth = date('n', $this->getTimestamp());
450 $toCompareMonth = date('n', $date->getTimestamp());
451 if ($currentMonth == $toCompareMonth) {
452 return 0;
453 }
454 if ($currentMonth < $toCompareMonth) {
455 return -1;
456 }
457 return 1;
458 }
459 /**
460 * Performs three-way comparison of the month of the current date against the given `$date`'s year.
461 *
462 * @param \Piwik\Date $date Year to compare
463 * @return int Returns `0` if the current year is equal to `$date`'s, `-1` if the current year is
464 * earlier or `1` if the current year is later.
465 */
466 public function compareYear(\Piwik\Date $date)
467 {
468 $currentYear = date('Y', $this->getTimestamp());
469 $toCompareYear = date('Y', $date->getTimestamp());
470 if ($currentYear == $toCompareYear) {
471 return 0;
472 }
473 if ($currentYear < $toCompareYear) {
474 return -1;
475 }
476 return 1;
477 }
478 /**
479 * Returns `true` if current date is today.
480 *
481 * @return bool
482 */
483 public function isToday()
484 {
485 return $this->toString('Y-m-d') === \Piwik\Date::factory('today', $this->timezone)->toString('Y-m-d');
486 }
487 /**
488 * Returns a date object set to now in UTC (same as {@link today()}, except that the time is also set).
489 *
490 * @return \Piwik\Date
491 */
492 public static function now()
493 {
494 return new \Piwik\Date(self::getNowTimestamp());
495 }
496 /**
497 * Returns a date object set to today at midnight in UTC.
498 *
499 * @return \Piwik\Date
500 */
501 public static function today()
502 {
503 return new \Piwik\Date(strtotime(date("Y-m-d 00:00:00", self::getNowTimestamp())));
504 }
505 /**
506 * Returns a date object set to tomorrow at midnight in UTC.
507 *
508 * @return \Piwik\Date
509 */
510 public static function tomorrow()
511 {
512 return new \Piwik\Date(strtotime('tomorrow', self::getNowTimestamp()));
513 }
514 /**
515 * Returns a date object set to yesterday at midnight in UTC.
516 *
517 * @return \Piwik\Date
518 */
519 public static function yesterday()
520 {
521 return new \Piwik\Date(strtotime("yesterday", self::getNowTimestamp()));
522 }
523 /**
524 * Returns a date object set to yesterday with the current time of day in UTC.
525 *
526 * @return \Piwik\Date
527 */
528 public static function yesterdaySameTime()
529 {
530 return new \Piwik\Date(strtotime("yesterday " . date('H:i:s', self::getNowTimestamp()), self::getNowTimestamp()));
531 }
532 /**
533 * Returns a date object set to the day a week ago at midnight in UTC.
534 *
535 * @return \Piwik\Date
536 */
537 public static function lastWeek()
538 {
539 return new \Piwik\Date(strtotime("-1week 00:00:00", self::getNowTimestamp()));
540 }
541 /**
542 * Returns a date object set to the day a month ago at midnight in UTC.
543 *
544 * @return \Piwik\Date
545 */
546 public static function lastMonth()
547 {
548 return new \Piwik\Date(strtotime("-1month 00:00:00", self::getNowTimestamp()));
549 }
550 /**
551 * Returns a date object set to the day a year ago at midnight in UTC.
552 *
553 * @return \Piwik\Date
554 */
555 public static function lastYear()
556 {
557 return new \Piwik\Date(strtotime("-1year 00:00:00", self::getNowTimestamp()));
558 }
559 /**
560 * Returns a new Date instance with `$this` date's day and the specified new
561 * time of day.
562 *
563 * @param string $time String in the `'HH:MM:SS'` format.
564 * @return \Piwik\Date The new date with the time of day changed.
565 */
566 public function setTime($time)
567 {
568 return new \Piwik\Date(strtotime(date("Y-m-d", $this->timestamp) . " {$time}"), $this->timezone);
569 }
570 /**
571 * Returns a new Date instance with `$this` date's time of day and the day specified
572 * by `$day`.
573 *
574 * @param int $day The day eg. `31`.
575 * @return \Piwik\Date
576 */
577 public function setDay($day)
578 {
579 $ts = $this->timestamp;
580 $result = mktime((int) date('H', $ts), (int) date('i', $ts), (int) date('s', $ts), (int) date('n', $ts), $day, (int) date('Y', $ts));
581 return new \Piwik\Date($result, $this->timezone);
582 }
583 /**
584 * Returns a new Date instance with `$this` date's time of day, month and day, but with
585 * a new year (specified by `$year`).
586 *
587 * @param int $year The year, eg. `2010`.
588 * @return \Piwik\Date
589 */
590 public function setYear($year)
591 {
592 $ts = $this->timestamp;
593 $result = mktime((int) date('H', $ts), (int) date('i', $ts), (int) date('s', $ts), (int) date('n', $ts), (int) date('j', $ts), $year);
594 return new \Piwik\Date($result, $this->timezone);
595 }
596 /**
597 * Subtracts `$n` number of days from `$this` date and returns a new Date object.
598 *
599 * @param int $n An integer > 0.
600 * @return \Piwik\Date
601 */
602 public function subDay($n)
603 {
604 if ($n === 0) {
605 return clone $this;
606 }
607 $ts = strtotime("-{$n} day", $this->timestamp);
608 return new \Piwik\Date($ts, $this->timezone);
609 }
610 /**
611 * Subtracts `$n` weeks from `$this` date and returns a new Date object.
612 *
613 * @param int $n An integer > 0.
614 * @return \Piwik\Date
615 */
616 public function subWeek($n)
617 {
618 return $this->subDay(7 * $n);
619 }
620 /**
621 * Subtracts `$n` months from `$this` date and returns the result as a new Date object.
622 *
623 * @param int $n An integer > 0.
624 * @return \Piwik\Date new date
625 */
626 public function subMonth($n)
627 {
628 if ($n === 0) {
629 return clone $this;
630 }
631 $ts = $this->timestamp;
632 $result = mktime(
633 (int) date('H', $ts),
634 (int) date('i', $ts),
635 (int) date('s', $ts),
636 (int) date('n', $ts) - $n,
637 1,
638 // we set the day to 1
639 (int) date('Y', $ts)
640 );
641 return new \Piwik\Date($result, $this->timezone);
642 }
643 /**
644 * Subtracts `$n` years from `$this` date and returns the result as a new Date object.
645 *
646 * @param int $n An integer > 0.
647 * @return \Piwik\Date
648 */
649 public function subYear($n)
650 {
651 if ($n === 0) {
652 return clone $this;
653 }
654 $ts = $this->timestamp;
655 $result = mktime(
656 (int) date('H', $ts),
657 (int) date('i', $ts),
658 (int) date('s', $ts),
659 1,
660 // we set the month to 1
661 1,
662 // we set the day to 1
663 (int) date('Y', $ts) - $n
664 );
665 return new \Piwik\Date($result, $this->timezone);
666 }
667 /**
668 * Returns a localized date string using the given template.
669 * The template should contain tags that will be replaced with localized date strings.
670 *
671 * @param string|int $template eg. `"MMM y"` or any format constant defined in {@link DateTimeFormatProvider}
672 * @param bool $ucfirst whether the first letter should be upper-cased
673 * @return string eg. `"Aug 2009"`
674 */
675 public function getLocalized($template, $ucfirst = \true)
676 {
677 $dateTimeFormatProvider = StaticContainer::get(DateTimeFormatProvider::class);
678 $template = $dateTimeFormatProvider->getFormatPattern($template);
679 $tokens = self::parseFormat($template);
680 $out = '';
681 foreach ($tokens as $token) {
682 if (is_array($token)) {
683 $out .= $this->formatToken(array_shift($token));
684 } else {
685 $out .= $token;
686 }
687 }
688 if ($ucfirst) {
689 $out = mb_strtoupper(mb_substr($out, 0, 1)) . mb_substr($out, 1);
690 }
691 return $out;
692 }
693 /**
694 * @param string $token
695 * @return float|int|string
696 */
697 protected function formatToken($token)
698 {
699 $dayOfWeek = $this->toString('N');
700 $monthOfYear = $this->toString('n');
701 $translator = StaticContainer::get('Piwik\\Translation\\Translator');
702 switch ($token) {
703 // year
704 case "yyyy":
705 case "y":
706 return $this->toString('Y');
707 case "yy":
708 return $this->toString('y');
709 // month
710 case "MMMM":
711 return $translator->translate('Intl_Month_Long_' . $monthOfYear);
712 case "MMM":
713 return $translator->translate('Intl_Month_Short_' . $monthOfYear);
714 case "MM":
715 return $this->toString('n');
716 case "M":
717 return $this->toString('m');
718 case "LLLL":
719 return $translator->translate('Intl_Month_Long_StandAlone_' . $monthOfYear);
720 case "LLL":
721 return $translator->translate('Intl_Month_Short_StandAlone_' . $monthOfYear);
722 case "LL":
723 return $this->toString('n');
724 case "L":
725 return $this->toString('m');
726 // day
727 case "dd":
728 return $this->toString('d');
729 case "d":
730 return $this->toString('j');
731 case "EEEE":
732 return $translator->translate('Intl_Day_Long_' . $dayOfWeek);
733 case "EEE":
734 case "EE":
735 case "E":
736 return $translator->translate('Intl_Day_Short_' . $dayOfWeek);
737 case "cccc":
738 return $translator->translate('Intl_Day_Long_StandAlone_' . $dayOfWeek);
739 case "ccc":
740 case "cc":
741 case "c":
742 return $translator->translate('Intl_Day_Short_StandAlone_' . $dayOfWeek);
743 case "D":
744 return 1 + (int) $this->toString('z');
745 // 1 - 366
746 case "F":
747 return (int) (((int) $this->toString('j') + 6) / 7);
748 // week in month
749 case "w":
750 $weekDay = date('N', mktime(0, 0, 0, (int) $this->toString('m'), 1, (int) $this->toString('y')));
751 return floor(($weekDay + (int) $this->toString('m') - 2) / 7) + 1;
752 // week in year
753 case "W":
754 return $this->toString('N');
755 // hour
756 case "HH":
757 return $this->toString('H');
758 case "H":
759 return $this->toString('G');
760 case "hh":
761 return $this->toString('h');
762 case "h":
763 return $this->toString('g');
764 case "KK":
765 // 00 .. 11
766 return str_pad(strval((int) $this->toString('g') - 1), 2, '0');
767 case "K":
768 // 0 .. 11
769 return (int) $this->toString('g') - 1;
770 case "kk":
771 // 01 .. 24
772 return str_pad(strval((int) $this->toString('G') + 1), 2, '0');
773 case "k":
774 // 1 .. 24
775 return (int) $this->toString('G') + 1;
776 // minute
777 case "mm":
778 case "m":
779 return $this->toString('i');
780 // second
781 case "ss":
782 case "s":
783 return $this->toString('s');
784 // would normally also include AM, PM, Noon and Midnight
785 case "b":
786 // would normally be a textual presentation like "in the afternoon"
787 case "B":
788 // am / pm
789 case "a":
790 return $this->toString('a') == 'am' ? $translator->translate('Intl_Time_AM') : $translator->translate('Intl_Time_PM');
791 // currently not implemented:
792 case "G":
793 case "GG":
794 case "GGG":
795 case "GGGG":
796 case "GGGGG":
797 return '';
798 // era
799 case "z":
800 case "Z":
801 case "v":
802 return '';
803 }
804 return '';
805 }
806 /** @var string[] */
807 protected static $tokens = ['G', 'y', 'M', 'L', 'd', 'h', 'H', 'k', 'K', 'm', 's', 'E', 'c', 'e', 'D', 'F', 'w', 'W', 'a', 'b', 'B', 'z', 'Z', 'v'];
808 /**
809 * Parses the datetime format pattern and returns a tokenized result array
810 *
811 * Examples:
812 * Input Output
813 * 'dd.mm.yyyy' array(array('dd'), '.', array('mm'), '.', array('yyyy'))
814 * 'y?M?d?EEEE ah:mm:ss' array(array('y'), '?', array('M'), '?', array('d'), '?', array('EEEE'), ' ', array('a'), array('h'), ':', array('mm'), ':', array('ss'))
815 *
816 * @param string $pattern the pattern to be parsed
817 * @return array<string|string[]> tokenized parsing result
818 */
819 protected static function parseFormat($pattern)
820 {
821 static $formats = [];
822 // cache
823 if (isset($formats[$pattern])) {
824 return $formats[$pattern];
825 }
826 $tokens = [];
827 $n = strlen($pattern);
828 $isLiteral = \false;
829 $literal = '';
830 for ($i = 0; $i < $n; ++$i) {
831 $c = $pattern[$i];
832 if ($c === "'") {
833 if ($i < $n - 1 && $pattern[$i + 1] === "'") {
834 $tokens[] = "'";
835 $i++;
836 } elseif ($isLiteral) {
837 $tokens[] = $literal;
838 $literal = '';
839 $isLiteral = \false;
840 } else {
841 $isLiteral = \true;
842 $literal = '';
843 }
844 } elseif ($isLiteral) {
845 $literal .= $c;
846 } else {
847 for ($j = $i + 1; $j < $n; ++$j) {
848 if ($pattern[$j] !== $c) {
849 break;
850 }
851 }
852 $p = str_repeat($c, $j - $i);
853 if (in_array($c, self::$tokens)) {
854 $tokens[] = [$p];
855 } else {
856 $tokens[] = $p;
857 }
858 $i = $j - 1;
859 }
860 }
861 if ($literal !== '') {
862 $tokens[] = $literal;
863 }
864 return $formats[$pattern] = $tokens;
865 }
866 /**
867 * Adds `$n` days to `$this` date and returns the result in a new Date.
868 * instance.
869 *
870 * @param int $n Number of days to add, must be > 0.
871 * @return \Piwik\Date
872 */
873 public function addDay($n)
874 {
875 $ts = strtotime("+{$n} day", $this->timestamp);
876 return new \Piwik\Date($ts, $this->timezone);
877 }
878 /**
879 * Adds `$n` Month to `$this` date and returns the result in a new Date.
880 * instance.
881 *
882 * @param int $n Number of days to add, must be > 0.
883 * @return \Piwik\Date
884 */
885 public function addMonth($n)
886 {
887 $ts = strtotime("+{$n} month", $this->timestamp);
888 return new \Piwik\Date($ts, $this->timezone);
889 }
890 /**
891 * Adds `$n` hours to `$this` date and returns the result in a new Date.
892 *
893 * @param int|float $n Number of hours to add. Can be less than 0, can be decimal (will get converted to minutes)
894 * @return \Piwik\Date
895 */
896 public function addHour($n)
897 {
898 $ts = self::addHourTo($this->timestamp, $n);
899 return new \Piwik\Date($ts, $this->timezone);
900 }
901 /**
902 * Adds N number of hours to a UNIX timestamp and returns the result. Using
903 * this static function instead of {@link addHour()} will be faster since a
904 * Date instance does not have to be created.
905 *
906 * @param int|float $timestamp The timestamp to add to.
907 * @param number $n Number of hours to add, must be > 0.
908 * @return int The result as a UNIX timestamp.
909 */
910 public static function addHourTo($timestamp, $n)
911 {
912 $isNegative = $n < 0;
913 $minutes = 0;
914 if ($n != round($n)) {
915 if ($n >= 1 || $n <= -1) {
916 $extraMinutes = floor(abs($n));
917 if ($isNegative) {
918 $extraMinutes = -$extraMinutes;
919 }
920 $minutes = abs($n - $extraMinutes) * 60;
921 if ($isNegative) {
922 $minutes *= -1;
923 }
924 } else {
925 $minutes = $n * 60;
926 }
927 $n = floor(abs($n));
928 if ($isNegative) {
929 $n *= -1;
930 }
931 }
932 return (int) ($timestamp + round($minutes * 60) + $n * 3600);
933 }
934 /**
935 * Subtracts `$n` hours from `$this` date and returns the result in a new Date.
936 *
937 * @param int $n Number of hours to subtract. Can be less than 0.
938 * @return \Piwik\Date
939 */
940 public function subHour($n)
941 {
942 return $this->addHour(-$n);
943 }
944 /**
945 * Subtracts `$n` seconds from `$this` date and returns the result in a new Date.
946 *
947 * @param int $n Number of seconds to subtract. Can be less than 0.
948 * @return \Piwik\Date
949 */
950 public function subSeconds($n)
951 {
952 return new \Piwik\Date($this->timestamp - $n, $this->timezone);
953 }
954 /**
955 * Adds a period to `$this` date and returns the result in a new Date instance.
956 *
957 * @param int $n The number of periods to add. Can be negative.
958 * @param string $period The type of period to add (YEAR, MONTH, WEEK, DAY, ...)
959 * @return \Piwik\Date
960 */
961 public function addPeriod($n, $period)
962 {
963 if (strtolower($period) == 'month') {
964 // TODO: comments
965 $dateInfo = getdate($this->timestamp);
966 $ts = mktime($dateInfo['hours'], $dateInfo['minutes'], $dateInfo['seconds'], $dateInfo['mon'] + (int) $n, 1, $dateInfo['year']);
967 $daysToAdd = min($dateInfo['mday'], self::getMaxDaysInMonth($ts)) - 1;
968 $ts += self::NUM_SECONDS_IN_DAY * $daysToAdd;
969 } else {
970 $time = $n < 0 ? "{$n} {$period}" : "+{$n} {$period}";
971 $ts = strtotime($time, $this->timestamp);
972 }
973 return new \Piwik\Date($ts, $this->timezone);
974 }
975 private static function getMaxDaysInMonth(int $timestamp) : int
976 {
977 $month = (int) date('m', $timestamp);
978 if (date('L', $timestamp) == 1 && $month == 2) {
979 return 29;
980 } else {
981 return self::$maxDaysInMonth[$month];
982 }
983 }
984 /**
985 * Subtracts a period from `$this` date and returns the result in a new Date instance.
986 *
987 * @param int $n The number of periods to add. Can be negative.
988 * @param string $period The type of period to add (YEAR, MONTH, WEEK, DAY, ...)
989 * @return \Piwik\Date
990 */
991 public function subPeriod($n, $period)
992 {
993 return $this->addPeriod(-$n, $period);
994 }
995 /**
996 * Returns the number of days represented by a number of seconds.
997 *
998 * @param int $secs
999 * @return float
1000 */
1001 public static function secondsToDays($secs)
1002 {
1003 return $secs / self::NUM_SECONDS_IN_DAY;
1004 }
1005 /**
1006 * @param mixed $dateString
1007 * @return Exception
1008 */
1009 private static function getInvalidDateFormatException($dateString)
1010 {
1011 $message = \Piwik\Piwik::translate('General_ExceptionInvalidDateFormat', array("YYYY-MM-DD, or 'today' or 'yesterday'", "strtotime", "https://php.net/strtotime"));
1012 return new Exception($message . ": " . var_export($dateString, \true));
1013 }
1014 /**
1015 * For tests.
1016 * @return int|null
1017 * @ignore
1018 */
1019 public static function getNowTimestamp()
1020 {
1021 return isset(self::$now) ? self::$now : time();
1022 }
1023 }
1024