Teknik is a suite of services with attractive and functional interfaces. https://www.teknik.io/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Date.php 23KB

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