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