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