PageRenderTime 47ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/core/Date.php

https://github.com/CodeYellowBV/piwik
PHP | 706 lines | 349 code | 50 blank | 307 comment | 49 complexity | 3e1bebfb61364b1301aaea44d363c5b7 MD5 | raw file
Possible License(s): LGPL-3.0, JSON, MIT, GPL-3.0, LGPL-2.1, GPL-2.0, AGPL-1.0, BSD-2-Clause, BSD-3-Clause
  1. <?php
  2. /**
  3. * Piwik - free/libre analytics platform
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. *
  8. */
  9. namespace Piwik;
  10. use Exception;
  11. /**
  12. * Utility class that wraps date/time related PHP functions. Using this class can
  13. * be easier than using `date`, `time`, `date_default_timezone_set`, etc.
  14. *
  15. * ### Performance concerns
  16. *
  17. * The helper methods in this class are instance methods and thus `Date` instances
  18. * need to be constructed before they can be used. The memory allocation can result
  19. * in noticeable performance degradation if you construct thousands of Date instances,
  20. * say, in a loop.
  21. *
  22. * ### Examples
  23. *
  24. * **Basic usage**
  25. *
  26. * $date = Date::factory('2007-07-24 14:04:24', 'EST');
  27. * $date->addHour(5);
  28. * echo $date->getLocalized("%longDay% the %day% of %longMonth% at %time%");
  29. *
  30. * @api
  31. */
  32. class Date
  33. {
  34. /** Number of seconds in a day. */
  35. const NUM_SECONDS_IN_DAY = 86400;
  36. /** The default date time string format. */
  37. const DATE_TIME_FORMAT = 'Y-m-d H:i:s';
  38. /**
  39. * The stored timestamp is always UTC based.
  40. * The returned timestamp via getTimestamp() will have the conversion applied
  41. * @var int|null
  42. */
  43. protected $timestamp = null;
  44. /**
  45. * Timezone the current date object is set to.
  46. * Timezone will only affect the returned timestamp via getTimestamp()
  47. * @var string
  48. */
  49. protected $timezone = 'UTC';
  50. /**
  51. * Constructor.
  52. *
  53. * @param int $timestamp The number in seconds since the unix epoch.
  54. * @param string $timezone The timezone of the datetime.
  55. * @throws Exception If $timestamp is not an int.
  56. */
  57. protected function __construct($timestamp, $timezone = 'UTC')
  58. {
  59. if (!is_int($timestamp)) {
  60. throw new Exception("Date is expecting a unix timestamp, got: '$timestamp'.");
  61. }
  62. $this->timezone = $timezone;
  63. $this->timestamp = $timestamp;
  64. }
  65. /**
  66. * Creates a new Date instance using a string datetime value. The timezone of the Date
  67. * result will be in UTC.
  68. *
  69. * @param string|int $dateString `'today'`, `'yesterday'`, `'now'`, `'yesterdaySameTime'`, a string with
  70. * `'YYYY-MM-DD HH:MM:SS'` format or a unix timestamp.
  71. * @param string $timezone The timezone of the result. If specified, `$dateString` will be converted
  72. * from UTC to this timezone before being used in the Date return value.
  73. * @throws Exception If `$dateString` is in an invalid format or if the time is before
  74. * Tue, 06 Aug 1991.
  75. * @return Date
  76. */
  77. public static function factory($dateString, $timezone = null)
  78. {
  79. $invalidDateException = new Exception(Piwik::translate('General_ExceptionInvalidDateFormat', array("YYYY-MM-DD, or 'today' or 'yesterday'", "strtotime", "http://php.net/strtotime")) . ": $dateString");
  80. if ($dateString instanceof self) {
  81. $dateString = $dateString->toString();
  82. }
  83. if ($dateString == 'now') {
  84. $date = self::now();
  85. } elseif ($dateString == 'today') {
  86. $date = self::today();
  87. } elseif ($dateString == 'yesterday') {
  88. $date = self::yesterday();
  89. } elseif ($dateString == 'yesterdaySameTime') {
  90. $date = self::yesterdaySameTime();
  91. } elseif (!is_int($dateString)
  92. && (
  93. // strtotime returns the timestamp for April 1st for a date like 2011-04-01,today
  94. // but we don't want this, as this is a date range and supposed to throw the exception
  95. strpos($dateString, ',') !== false
  96. ||
  97. ($dateString = strtotime($dateString)) === false
  98. )
  99. ) {
  100. throw $invalidDateException;
  101. } else {
  102. $date = new Date($dateString);
  103. }
  104. $timestamp = $date->getTimestamp();
  105. // can't be doing web analytics before the 1st website
  106. // Tue, 06 Aug 1991 00:00:00 GMT
  107. if ($timestamp < 681436800) {
  108. throw $invalidDateException;
  109. }
  110. if (empty($timezone)) {
  111. return $date;
  112. }
  113. $timestamp = self::adjustForTimezone($timestamp, $timezone);
  114. return Date::factory($timestamp);
  115. }
  116. /**
  117. * Returns the current timestamp as a string with the following format: `'YYYY-MM-DD HH:MM:SS'`.
  118. *
  119. * @return string
  120. */
  121. public function getDatetime()
  122. {
  123. return $this->toString(self::DATE_TIME_FORMAT);
  124. }
  125. /**
  126. * Returns the start of the day of the current timestamp in UTC. For example,
  127. * if the current timestamp is `'2007-07-24 14:04:24'` in UTC, the result will
  128. * be `'2007-07-24'`.
  129. *
  130. * @return string
  131. */
  132. public function getDateStartUTC()
  133. {
  134. $dateStartUTC = gmdate('Y-m-d', $this->timestamp);
  135. $date = Date::factory($dateStartUTC)->setTimezone($this->timezone);
  136. return $date->toString(self::DATE_TIME_FORMAT);
  137. }
  138. /**
  139. * Returns the end of the day of the current timestamp in UTC. For example,
  140. * if the current timestamp is `'2007-07-24 14:03:24'` in UTC, the result will
  141. * be `'2007-07-24 23:59:59'`.
  142. *
  143. * @return string
  144. */
  145. public function getDateEndUTC()
  146. {
  147. $dateEndUTC = gmdate('Y-m-d 23:59:59', $this->timestamp);
  148. $date = Date::factory($dateEndUTC)->setTimezone($this->timezone);
  149. return $date->toString(self::DATE_TIME_FORMAT);
  150. }
  151. /**
  152. * Returns a new date object with the same timestamp as `$this` but with a new
  153. * timezone.
  154. *
  155. * See {@link getTimestamp()} to see how the timezone is used.
  156. *
  157. * @param string $timezone eg, `'UTC'`, `'Europe/London'`, etc.
  158. * @return Date
  159. */
  160. public function setTimezone($timezone)
  161. {
  162. return new Date($this->timestamp, $timezone);
  163. }
  164. /**
  165. * Helper function that returns the offset in the timezone string 'UTC+14'
  166. * Returns false if the timezone is not UTC+X or UTC-X
  167. *
  168. * @param string $timezone
  169. * @return int|bool utc offset or false
  170. */
  171. protected static function extractUtcOffset($timezone)
  172. {
  173. if ($timezone == 'UTC') {
  174. return 0;
  175. }
  176. $start = substr($timezone, 0, 4);
  177. if ($start != 'UTC-'
  178. && $start != 'UTC+'
  179. ) {
  180. return false;
  181. }
  182. $offset = (float)substr($timezone, 4);
  183. if ($start == 'UTC-') {
  184. $offset = -$offset;
  185. }
  186. return $offset;
  187. }
  188. /**
  189. * Converts a timestamp in a from UTC to a timezone.
  190. *
  191. * @param int $timestamp The UNIX timestamp to adjust.
  192. * @param string $timezone The timezone to adjust to.
  193. * @return int The adjusted time as seconds from EPOCH.
  194. */
  195. public static function adjustForTimezone($timestamp, $timezone)
  196. {
  197. // manually adjust for UTC timezones
  198. $utcOffset = self::extractUtcOffset($timezone);
  199. if ($utcOffset !== false) {
  200. return self::addHourTo($timestamp, $utcOffset);
  201. }
  202. date_default_timezone_set($timezone);
  203. $datetime = date(self::DATE_TIME_FORMAT, $timestamp);
  204. date_default_timezone_set('UTC');
  205. return strtotime($datetime);
  206. }
  207. /**
  208. * Returns the Unix timestamp of the date in UTC.
  209. *
  210. * @return int
  211. */
  212. public function getTimestampUTC()
  213. {
  214. return $this->timestamp;
  215. }
  216. /**
  217. * Returns the unix timestamp of the date in UTC, converted from the current
  218. * timestamp timezone.
  219. *
  220. * @return int
  221. */
  222. public function getTimestamp()
  223. {
  224. if (empty($this->timezone)) {
  225. $this->timezone = 'UTC';
  226. }
  227. $utcOffset = self::extractUtcOffset($this->timezone);
  228. if ($utcOffset !== false) {
  229. return (int)($this->timestamp - $utcOffset * 3600);
  230. }
  231. // The following code seems clunky - I thought the DateTime php class would allow to return timestamps
  232. // after applying the timezone offset. Instead, the underlying timestamp is not changed.
  233. // I decided to get the date without the timezone information, and create the timestamp from the truncated string.
  234. // Unit tests pass (@see Date.test.php) but I'm pretty sure this is not the right way to do it
  235. date_default_timezone_set($this->timezone);
  236. $dtzone = timezone_open('UTC');
  237. $time = date('r', $this->timestamp);
  238. $dtime = date_create($time);
  239. date_timezone_set($dtime, $dtzone);
  240. $dateWithTimezone = date_format($dtime, 'r');
  241. $dateWithoutTimezone = substr($dateWithTimezone, 0, -6);
  242. $timestamp = strtotime($dateWithoutTimezone);
  243. date_default_timezone_set('UTC');
  244. return (int)$timestamp;
  245. }
  246. /**
  247. * Returns `true` if the current date is older than the given `$date`.
  248. *
  249. * @param Date $date
  250. * @return bool
  251. */
  252. public function isLater(Date $date)
  253. {
  254. return $this->getTimestamp() > $date->getTimestamp();
  255. }
  256. /**
  257. * Returns `true` if the current date is earlier than the given `$date`.
  258. *
  259. * @param Date $date
  260. * @return bool
  261. */
  262. public function isEarlier(Date $date)
  263. {
  264. return $this->getTimestamp() < $date->getTimestamp();
  265. }
  266. /**
  267. * Returns `true` if the current year is a leap year, false otherwise.
  268. *
  269. * @return bool
  270. */
  271. public function isLeapYear()
  272. {
  273. $currentYear = date('Y', $this->getTimestamp());
  274. return ($currentYear % 400) == 0 || (($currentYear % 4) == 0 && ($currentYear % 100) != 0);
  275. }
  276. /**
  277. * Converts this date to the requested string format. See {@link http://php.net/date}
  278. * for the list of format strings.
  279. *
  280. * @param string $format
  281. * @return string
  282. */
  283. public function toString($format = 'Y-m-d')
  284. {
  285. return date($format, $this->getTimestamp());
  286. }
  287. /**
  288. * See {@link toString()}.
  289. *
  290. * @return string The current date in `'YYYY-MM-DD'` format.
  291. */
  292. public function __toString()
  293. {
  294. return $this->toString();
  295. }
  296. /**
  297. * Performs three-way comparison of the week of the current date against the given `$date`'s week.
  298. *
  299. * @param \Piwik\Date $date
  300. * @return int Returns `0` if the current week is equal to `$date`'s, `-1` if the current week is
  301. * earlier or `1` if the current week is later.
  302. */
  303. public function compareWeek(Date $date)
  304. {
  305. $currentWeek = date('W', $this->getTimestamp());
  306. $toCompareWeek = date('W', $date->getTimestamp());
  307. if ($currentWeek == $toCompareWeek) {
  308. return 0;
  309. }
  310. if ($currentWeek < $toCompareWeek) {
  311. return -1;
  312. }
  313. return 1;
  314. }
  315. /**
  316. * Performs three-way comparison of the month of the current date against the given `$date`'s month.
  317. *
  318. * @param \Piwik\Date $date Month to compare
  319. * @return int Returns `0` if the current month is equal to `$date`'s, `-1` if the current month is
  320. * earlier or `1` if the current month is later.
  321. */
  322. public function compareMonth(Date $date)
  323. {
  324. $currentMonth = date('n', $this->getTimestamp());
  325. $toCompareMonth = date('n', $date->getTimestamp());
  326. if ($currentMonth == $toCompareMonth) {
  327. return 0;
  328. }
  329. if ($currentMonth < $toCompareMonth) {
  330. return -1;
  331. }
  332. return 1;
  333. }
  334. /**
  335. * Performs three-way comparison of the month of the current date against the given `$date`'s year.
  336. *
  337. * @param \Piwik\Date $date Year to compare
  338. * @return int Returns `0` if the current year is equal to `$date`'s, `-1` if the current year is
  339. * earlier or `1` if the current year is later.
  340. */
  341. public function compareYear(Date $date)
  342. {
  343. $currentYear = date('Y', $this->getTimestamp());
  344. $toCompareYear = date('Y', $date->getTimestamp());
  345. if ($currentYear == $toCompareYear) {
  346. return 0;
  347. }
  348. if ($currentYear < $toCompareYear) {
  349. return -1;
  350. }
  351. return 1;
  352. }
  353. /**
  354. * Returns `true` if current date is today.
  355. *
  356. * @return bool
  357. */
  358. public function isToday()
  359. {
  360. return $this->toString('Y-m-d') === Date::factory('today', $this->timezone)->toString('Y-m-d');
  361. }
  362. /**
  363. * Returns a date object set to now in UTC (same as {@link today()}, except that the time is also set).
  364. *
  365. * @return \Piwik\Date
  366. */
  367. public static function now()
  368. {
  369. return new Date(time());
  370. }
  371. /**
  372. * Returns a date object set to today at midnight in UTC.
  373. *
  374. * @return \Piwik\Date
  375. */
  376. public static function today()
  377. {
  378. return new Date(strtotime(date("Y-m-d 00:00:00")));
  379. }
  380. /**
  381. * Returns a date object set to yesterday at midnight in UTC.
  382. *
  383. * @return \Piwik\Date
  384. */
  385. public static function yesterday()
  386. {
  387. return new Date(strtotime("yesterday"));
  388. }
  389. /**
  390. * Returns a date object set to yesterday with the current time of day in UTC.
  391. *
  392. * @return \Piwik\Date
  393. */
  394. public static function yesterdaySameTime()
  395. {
  396. return new Date(strtotime("yesterday " . date('H:i:s')));
  397. }
  398. /**
  399. * Returns a new Date instance with `$this` date's day and the specified new
  400. * time of day.
  401. *
  402. * @param string $time String in the `'HH:MM:SS'` format.
  403. * @return \Piwik\Date The new date with the time of day changed.
  404. */
  405. public function setTime($time)
  406. {
  407. return new Date(strtotime(date("Y-m-d", $this->timestamp) . " $time"), $this->timezone);
  408. }
  409. /**
  410. * Returns a new Date instance with `$this` date's time of day and the day specified
  411. * by `$day`.
  412. *
  413. * @param int $day The day eg. `31`.
  414. * @return \Piwik\Date
  415. */
  416. public function setDay($day)
  417. {
  418. $ts = $this->timestamp;
  419. $result = mktime(
  420. date('H', $ts),
  421. date('i', $ts),
  422. date('s', $ts),
  423. date('n', $ts),
  424. $day,
  425. date('Y', $ts)
  426. );
  427. return new Date($result, $this->timezone);
  428. }
  429. /**
  430. * Returns a new Date instance with `$this` date's time of day, month and day, but with
  431. * a new year (specified by `$year`).
  432. *
  433. * @param int $year The year, eg. `2010`.
  434. * @return \Piwik\Date
  435. */
  436. public function setYear($year)
  437. {
  438. $ts = $this->timestamp;
  439. $result = mktime(
  440. date('H', $ts),
  441. date('i', $ts),
  442. date('s', $ts),
  443. date('n', $ts),
  444. date('j', $ts),
  445. $year
  446. );
  447. return new Date($result, $this->timezone);
  448. }
  449. /**
  450. * Subtracts `$n` number of days from `$this` date and returns a new Date object.
  451. *
  452. * @param int $n An integer > 0.
  453. * @return \Piwik\Date
  454. */
  455. public function subDay($n)
  456. {
  457. if ($n === 0) {
  458. return clone $this;
  459. }
  460. $ts = strtotime("-$n day", $this->timestamp);
  461. return new Date($ts, $this->timezone);
  462. }
  463. /**
  464. * Subtracts `$n` weeks from `$this` date and returns a new Date object.
  465. *
  466. * @param int $n An integer > 0.
  467. * @return \Piwik\Date
  468. */
  469. public function subWeek($n)
  470. {
  471. return $this->subDay(7 * $n);
  472. }
  473. /**
  474. * Subtracts `$n` months from `$this` date and returns the result as a new Date object.
  475. *
  476. * @param int $n An integer > 0.
  477. * @return \Piwik\Date new date
  478. */
  479. public function subMonth($n)
  480. {
  481. if ($n === 0) {
  482. return clone $this;
  483. }
  484. $ts = $this->timestamp;
  485. $result = mktime(
  486. date('H', $ts),
  487. date('i', $ts),
  488. date('s', $ts),
  489. date('n', $ts) - $n,
  490. 1, // we set the day to 1
  491. date('Y', $ts)
  492. );
  493. return new Date($result, $this->timezone);
  494. }
  495. /**
  496. * Subtracts `$n` years from `$this` date and returns the result as a new Date object.
  497. *
  498. * @param int $n An integer > 0.
  499. * @return \Piwik\Date
  500. */
  501. public function subYear($n)
  502. {
  503. if ($n === 0) {
  504. return clone $this;
  505. }
  506. $ts = $this->timestamp;
  507. $result = mktime(
  508. date('H', $ts),
  509. date('i', $ts),
  510. date('s', $ts),
  511. 1, // we set the month to 1
  512. 1, // we set the day to 1
  513. date('Y', $ts) - $n
  514. );
  515. return new Date($result, $this->timezone);
  516. }
  517. /**
  518. * Returns a localized date string using the given template.
  519. * The template should contain tags that will be replaced with localized date strings.
  520. *
  521. * Allowed tags include:
  522. *
  523. * - **%day%**: replaced with the day of the month without leading zeros, eg, **1** or **20**.
  524. * - **%shortMonth%**: the short month in the current language, eg, **Jan**, **Feb**.
  525. * - **%longMonth%**: the whole month name in the current language, eg, **January**, **February**.
  526. * - **%shortDay%**: the short day name in the current language, eg, **Mon**, **Tue**.
  527. * - **%longDay%**: the long day name in the current language, eg, **Monday**, **Tuesday**.
  528. * - **%longYear%**: the four digit year, eg, **2007**, **2013**.
  529. * - **%shortYear%**: the two digit year, eg, **07**, **13**.
  530. * - **%time%**: the time of day, eg, **07:35:00**, or **15:45:00**.
  531. *
  532. * @param string $template eg. `"%shortMonth% %longYear%"`
  533. * @return string eg. `"Aug 2009"`
  534. */
  535. public function getLocalized($template)
  536. {
  537. $day = $this->toString('j');
  538. $dayOfWeek = $this->toString('N');
  539. $monthOfYear = $this->toString('n');
  540. $patternToValue = array(
  541. "%day%" => $day,
  542. "%shortMonth%" => Piwik::translate('General_ShortMonth_' . $monthOfYear),
  543. "%longMonth%" => Piwik::translate('General_LongMonth_' . $monthOfYear),
  544. "%shortDay%" => Piwik::translate('General_ShortDay_' . $dayOfWeek),
  545. "%longDay%" => Piwik::translate('General_LongDay_' . $dayOfWeek),
  546. "%longYear%" => $this->toString('Y'),
  547. "%shortYear%" => $this->toString('y'),
  548. "%time%" => $this->toString('H:i:s')
  549. );
  550. $out = str_replace(array_keys($patternToValue), array_values($patternToValue), $template);
  551. return $out;
  552. }
  553. /**
  554. * Adds `$n` days to `$this` date and returns the result in a new Date.
  555. * instance.
  556. *
  557. * @param int $n Number of days to add, must be > 0.
  558. * @return \Piwik\Date
  559. */
  560. public function addDay($n)
  561. {
  562. $ts = strtotime("+$n day", $this->timestamp);
  563. return new Date($ts, $this->timezone);
  564. }
  565. /**
  566. * Adds `$n` hours to `$this` date and returns the result in a new Date.
  567. *
  568. * @param int $n Number of hours to add. Can be less than 0.
  569. * @return \Piwik\Date
  570. */
  571. public function addHour($n)
  572. {
  573. $ts = self::addHourTo($this->timestamp, $n);
  574. return new Date($ts, $this->timezone);
  575. }
  576. /**
  577. * Adds N number of hours to a UNIX timestamp and returns the result. Using
  578. * this static function instead of {@link addHour()} will be faster since a
  579. * Date instance does not have to be created.
  580. *
  581. * @param int $timestamp The timestamp to add to.
  582. * @param number $n Number of hours to add, must be > 0.
  583. * @return int The result as a UNIX timestamp.
  584. */
  585. public static function addHourTo($timestamp, $n)
  586. {
  587. $isNegative = ($n < 0);
  588. $minutes = 0;
  589. if ($n != round($n)) {
  590. if ($n >= 1 || $n <= -1) {
  591. $extraMinutes = floor(abs($n));
  592. if ($isNegative) {
  593. $extraMinutes = -$extraMinutes;
  594. }
  595. $minutes = abs($n - $extraMinutes) * 60;
  596. if ($isNegative) {
  597. $minutes *= -1;
  598. }
  599. } else {
  600. $minutes = $n * 60;
  601. }
  602. $n = floor(abs($n));
  603. if ($isNegative) {
  604. $n *= -1;
  605. }
  606. }
  607. return (int)($timestamp + round($minutes * 60) + $n * 3600);
  608. }
  609. /**
  610. * Subtracts `$n` hours from `$this` date and returns the result in a new Date.
  611. *
  612. * @param int $n Number of hours to subtract. Can be less than 0.
  613. * @return \Piwik\Date
  614. */
  615. public function subHour($n)
  616. {
  617. return $this->addHour(-$n);
  618. }
  619. /**
  620. * Adds a period to `$this` date and returns the result in a new Date instance.
  621. *
  622. * @param int $n The number of periods to add. Can be negative.
  623. * @param string $period The type of period to add (YEAR, MONTH, WEEK, DAY, ...)
  624. * @return \Piwik\Date
  625. */
  626. public function addPeriod($n, $period)
  627. {
  628. if ($n < 0) {
  629. $ts = strtotime("$n $period", $this->timestamp);
  630. } else {
  631. $ts = strtotime("+$n $period", $this->timestamp);
  632. }
  633. return new Date($ts, $this->timezone);
  634. }
  635. /**
  636. * Subtracts a period from `$this` date and returns the result in a new Date instance.
  637. *
  638. * @param int $n The number of periods to add. Can be negative.
  639. * @param string $period The type of period to add (YEAR, MONTH, WEEK, DAY, ...)
  640. * @return \Piwik\Date
  641. */
  642. public function subPeriod($n, $period)
  643. {
  644. return $this->addPeriod(-$n, $period);
  645. }
  646. /**
  647. * Returns the number of days represented by a number of seconds.
  648. *
  649. * @param int $secs
  650. * @return float
  651. */
  652. public static function secondsToDays($secs)
  653. {
  654. return $secs / self::NUM_SECONDS_IN_DAY;
  655. }
  656. }