Time.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546
  1. <?php
  2. /**
  3. * Cake Time utility class file.
  4. *
  5. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  6. * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  7. *
  8. * Licensed under The MIT License
  9. * For full copyright and license information, please see the LICENSE.txt
  10. * Redistributions of files must retain the above copyright notice.
  11. *
  12. * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  13. * @link http://cakephp.org CakePHP(tm) Project
  14. * @since 0.10.0
  15. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  16. */
  17. namespace Cake\Utility;
  18. use Cake\Core\Configure;
  19. use Carbon\Carbon;
  20. use IntlDateFormatter;
  21. /**
  22. * Time Helper class for easy use of time data.
  23. *
  24. * Manipulation of time data.
  25. *
  26. * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/time.html
  27. */
  28. class Time extends Carbon {
  29. /**
  30. * The format to use when formatting a time using `Cake\Utility\Time::i18nFormat()`
  31. * and `__toString`
  32. *
  33. * The format should be eiter the formatting constants from IntDateFormatter as
  34. * described in (http://www.php.net/manual/en/class.intldateformatter.php) or a pattern
  35. * as specified in (http://www.icu-project.org/apiref/icu4c/classSimpleDateFormat.html#details)
  36. *
  37. * It is possible to provide an array of 2 constants. In this case, the first position
  38. * will be used for formatting the date part of the object and the second position
  39. * will be used to format the time part.
  40. *
  41. * @var mixed
  42. * @see \Cake\Utility\Time::i18nFormat()
  43. */
  44. protected static $toStringFormat = [IntlDateFormatter::SHORT, IntlDateFormatter::SHORT];
  45. /**
  46. * The format to use when formatting a time using `Cake\Utility\Time::nice()`
  47. *
  48. * The format should be eiter the formatting constants from IntDateFormatter as
  49. * described in (http://www.php.net/manual/en/class.intldateformatter.php) or a pattern
  50. * as specified in (http://www.icu-project.org/apiref/icu4c/classSimpleDateFormat.html#details)
  51. *
  52. * It is possible to provide an array of 2 constants. In this case, the first position
  53. * will be used for formatting the date part of the object and the second position
  54. * will be used to format the time part.
  55. *
  56. * @var mixed
  57. * @see \Cake\Utility\Time::nice()
  58. */
  59. public static $niceFormat = [IntlDateFormatter::MEDIUM, IntlDateFormatter::SHORT];
  60. /**
  61. * The default locale to be used for displaying formatted date strings.
  62. *
  63. * @var string
  64. */
  65. public static $defaultLocale;
  66. /**
  67. * The format to use when formatting a time using `Cake\Utility\Time::timeAgoInWords()`
  68. * and the difference is more than `Cake\Utility\Time::$wordEnd`
  69. *
  70. * @var string
  71. * @see \Cake\Utility\Time::timeAgoInWords()
  72. */
  73. public static $wordFormat = 'j/n/y';
  74. /**
  75. * The format to use when formatting a time using `Cake\Utility\Time::timeAgoInWords()`
  76. * and the difference is less than `Cake\Utility\Time::$wordEnd`
  77. *
  78. * @var array
  79. * @see \Cake\Utility\Time::timeAgoInWords()
  80. */
  81. public static $wordAccuracy = array(
  82. 'year' => "day",
  83. 'month' => "day",
  84. 'week' => "day",
  85. 'day' => "hour",
  86. 'hour' => "minute",
  87. 'minute' => "minute",
  88. 'second' => "second",
  89. );
  90. /**
  91. * The end of relative time telling
  92. *
  93. * @var string
  94. * @see \Cake\Utility\Time::timeAgoInWords()
  95. */
  96. public static $wordEnd = '+1 month';
  97. public function __construct($time = null, $tz = null) {
  98. if ($time instanceof \DateTime) {
  99. list($time, $tz) = [$dt->format('Y-m-d H:i:s'), $dt->getTimeZone()];
  100. }
  101. if (is_numeric($time)) {
  102. $time = '@' . $time;
  103. }
  104. parent::__construct($time, $tz);
  105. }
  106. /**
  107. * Returns a nicely formatted date string for this object.
  108. *
  109. * The format to be used is stored in the static property `Time::niceFormat`.
  110. *
  111. * @param string|\DateTimeZone $timezone Timezone string or DateTimeZone object
  112. * in which the date will be displayed. The timezone stored for this object will not
  113. * be changed.
  114. * @param $locale The locale name in which the date should be displayed (e.g. pt-BR)
  115. * @return string Formatted date string
  116. */
  117. public function nice($timezone = null, $locale = null) {
  118. return $this->i18nFormat(static::$niceFormat, $timezone, $locale);
  119. }
  120. /**
  121. * Returns true if this object represents a date within the current week
  122. *
  123. * @return bool
  124. */
  125. public function isThisWeek() {
  126. return static::now($this->getTimezone())->format('W o') == $this->format('W o');
  127. }
  128. /**
  129. * Returns true if this object represents a date within the current month
  130. *
  131. * @return bool
  132. */
  133. public function isThisMonth() {
  134. return static::now($this->getTimezone())->format('m Y') == $this->format('m Y');
  135. }
  136. /**
  137. * Returns true if this object represents a date within the current year
  138. *
  139. * @return bool
  140. */
  141. public function isThisYear() {
  142. return static::now($this->getTimezone())->format('Y') == $this->format('Y');
  143. }
  144. /**
  145. * Returns the quarter
  146. *
  147. * @return mixed 1, 2, 3, or 4 quarter of year or array if $range true
  148. */
  149. public function toQuarter($range = false) {
  150. $quarter = ceil($this->format('m') / 3);
  151. if ($range === false) {
  152. return $quarter;
  153. }
  154. $year = $this->format('Y');
  155. switch ($quarter) {
  156. case 1:
  157. return array($year . '-01-01', $year . '-03-31');
  158. case 2:
  159. return array($year . '-04-01', $year . '-06-30');
  160. case 3:
  161. return array($year . '-07-01', $year . '-09-30');
  162. case 4:
  163. return array($year . '-10-01', $year . '-12-31');
  164. }
  165. }
  166. /**
  167. * Returns a UNIX timestamp.
  168. *
  169. * @return string Unix timestamp
  170. */
  171. public function toUnixString() {
  172. return $this->format('U');
  173. }
  174. /**
  175. * Returns either a relative or a formatted absolute date depending
  176. * on the difference between the current time and this object.
  177. *
  178. * ### Options:
  179. *
  180. * - `format` => a fall back format if the relative time is longer than the duration specified by end
  181. * - `accuracy` => Specifies how accurate the date should be described (array)
  182. * - year => The format if years > 0 (default "day")
  183. * - month => The format if months > 0 (default "day")
  184. * - week => The format if weeks > 0 (default "day")
  185. * - day => The format if weeks > 0 (default "hour")
  186. * - hour => The format if hours > 0 (default "minute")
  187. * - minute => The format if minutes > 0 (default "minute")
  188. * - second => The format if seconds > 0 (default "second")
  189. * - `end` => The end of relative time telling
  190. * - `relativeString` => The printf compatible string when outputting relative time
  191. * - `absoluteString` => The printf compatible string when outputting absolute time
  192. * - `timezone` => The user timezone the timestamp should be formatted in.
  193. *
  194. * Relative dates look something like this:
  195. *
  196. * - 3 weeks, 4 days ago
  197. * - 15 seconds ago
  198. *
  199. * Default date formatting is d/m/yy e.g: on 18/2/09
  200. *
  201. * The returned string includes 'ago' or 'on' and assumes you'll properly add a word
  202. * like 'Posted ' before the function output.
  203. *
  204. * NOTE: If the difference is one week or more, the lowest level of accuracy is day
  205. *
  206. * @param array $options Array of options.
  207. * @return string Relative time string.
  208. * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::timeAgoInWords
  209. */
  210. public function timeAgoInWords(array $options = []) {
  211. $timezone = null;
  212. $format = static::$wordFormat;
  213. $end = static::$wordEnd;
  214. $relativeString = __d('cake', '%s ago');
  215. $absoluteString = __d('cake', 'on %s');
  216. $accuracy = static::$wordAccuracy;
  217. if (isset($options['timezone'])) {
  218. $timezone = $options['timezone'];
  219. }
  220. if (isset($options['accuracy'])) {
  221. if (is_array($options['accuracy'])) {
  222. $accuracy = array_merge($accuracy, $options['accuracy']);
  223. } else {
  224. foreach ($accuracy as $key => $level) {
  225. $accuracy[$key] = $options['accuracy'];
  226. }
  227. }
  228. }
  229. if (isset($options['format'])) {
  230. $format = $options['format'];
  231. }
  232. if (isset($options['end'])) {
  233. $end = $options['end'];
  234. }
  235. if (isset($options['relativeString'])) {
  236. $relativeString = $options['relativeString'];
  237. unset($options['relativeString']);
  238. }
  239. if (isset($options['absoluteString'])) {
  240. $absoluteString = $options['absoluteString'];
  241. unset($options['absoluteString']);
  242. }
  243. unset($options['end'], $options['format']);
  244. $now = static::now()->format('U');
  245. $inSeconds = $this->format('U');
  246. $backwards = ($inSeconds > $now);
  247. $futureTime = $now;
  248. $pastTime = $inSeconds;
  249. if ($backwards) {
  250. $futureTime = $inSeconds;
  251. $pastTime = $now;
  252. }
  253. $diff = $futureTime - $pastTime;
  254. if (!$diff) {
  255. return __d('cake', 'just now', 'just now');
  256. }
  257. if ($diff > abs($now - (new static($end))->format('U'))) {
  258. return sprintf($absoluteString, date($format, $inSeconds));
  259. }
  260. // If more than a week, then take into account the length of months
  261. if ($diff >= 604800) {
  262. list($future['H'], $future['i'], $future['s'], $future['d'], $future['m'], $future['Y']) = explode('/', date('H/i/s/d/m/Y', $futureTime));
  263. list($past['H'], $past['i'], $past['s'], $past['d'], $past['m'], $past['Y']) = explode('/', date('H/i/s/d/m/Y', $pastTime));
  264. $years = $months = $weeks = $days = $hours = $minutes = $seconds = 0;
  265. $years = $future['Y'] - $past['Y'];
  266. $months = $future['m'] + ((12 * $years) - $past['m']);
  267. if ($months >= 12) {
  268. $years = floor($months / 12);
  269. $months = $months - ($years * 12);
  270. }
  271. if ($future['m'] < $past['m'] && $future['Y'] - $past['Y'] === 1) {
  272. $years--;
  273. }
  274. if ($future['d'] >= $past['d']) {
  275. $days = $future['d'] - $past['d'];
  276. } else {
  277. $daysInPastMonth = date('t', $pastTime);
  278. $daysInFutureMonth = date('t', mktime(0, 0, 0, $future['m'] - 1, 1, $future['Y']));
  279. if (!$backwards) {
  280. $days = ($daysInPastMonth - $past['d']) + $future['d'];
  281. } else {
  282. $days = ($daysInFutureMonth - $past['d']) + $future['d'];
  283. }
  284. if ($future['m'] != $past['m']) {
  285. $months--;
  286. }
  287. }
  288. if (!$months && $years >= 1 && $diff < ($years * 31536000)) {
  289. $months = 11;
  290. $years--;
  291. }
  292. if ($months >= 12) {
  293. $years = $years + 1;
  294. $months = $months - 12;
  295. }
  296. if ($days >= 7) {
  297. $weeks = floor($days / 7);
  298. $days = $days - ($weeks * 7);
  299. }
  300. } else {
  301. $years = $months = $weeks = 0;
  302. $days = floor($diff / 86400);
  303. $diff = $diff - ($days * 86400);
  304. $hours = floor($diff / 3600);
  305. $diff = $diff - ($hours * 3600);
  306. $minutes = floor($diff / 60);
  307. $diff = $diff - ($minutes * 60);
  308. $seconds = $diff;
  309. }
  310. $fWord = $accuracy['second'];
  311. if ($years > 0) {
  312. $fWord = $accuracy['year'];
  313. } elseif (abs($months) > 0) {
  314. $fWord = $accuracy['month'];
  315. } elseif (abs($weeks) > 0) {
  316. $fWord = $accuracy['week'];
  317. } elseif (abs($days) > 0) {
  318. $fWord = $accuracy['day'];
  319. } elseif (abs($hours) > 0) {
  320. $fWord = $accuracy['hour'];
  321. } elseif (abs($minutes) > 0) {
  322. $fWord = $accuracy['minute'];
  323. }
  324. $fNum = str_replace(array('year', 'month', 'week', 'day', 'hour', 'minute', 'second'), array(1, 2, 3, 4, 5, 6, 7), $fWord);
  325. $relativeDate = '';
  326. if ($fNum >= 1 && $years > 0) {
  327. $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '%d year', '%d years', $years, $years);
  328. }
  329. if ($fNum >= 2 && $months > 0) {
  330. $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '%d month', '%d months', $months, $months);
  331. }
  332. if ($fNum >= 3 && $weeks > 0) {
  333. $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '%d week', '%d weeks', $weeks, $weeks);
  334. }
  335. if ($fNum >= 4 && $days > 0) {
  336. $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '%d day', '%d days', $days, $days);
  337. }
  338. if ($fNum >= 5 && $hours > 0) {
  339. $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '%d hour', '%d hours', $hours, $hours);
  340. }
  341. if ($fNum >= 6 && $minutes > 0) {
  342. $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '%d minute', '%d minutes', $minutes, $minutes);
  343. }
  344. if ($fNum >= 7 && $seconds > 0) {
  345. $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '%d second', '%d seconds', $seconds, $seconds);
  346. }
  347. // When time has passed
  348. if (!$backwards && $relativeDate) {
  349. return sprintf($relativeString, $relativeDate);
  350. }
  351. if (!$backwards) {
  352. $aboutAgo = array(
  353. 'second' => __d('cake', 'about a second ago'),
  354. 'minute' => __d('cake', 'about a minute ago'),
  355. 'hour' => __d('cake', 'about an hour ago'),
  356. 'day' => __d('cake', 'about a day ago'),
  357. 'week' => __d('cake', 'about a week ago'),
  358. 'year' => __d('cake', 'about a year ago')
  359. );
  360. return $aboutAgo[$fWord];
  361. }
  362. // When time is to come
  363. if (!$relativeDate) {
  364. $aboutIn = array(
  365. 'second' => __d('cake', 'in about a second'),
  366. 'minute' => __d('cake', 'in about a minute'),
  367. 'hour' => __d('cake', 'in about an hour'),
  368. 'day' => __d('cake', 'in about a day'),
  369. 'week' => __d('cake', 'in about a week'),
  370. 'year' => __d('cake', 'in about a year')
  371. );
  372. return $aboutIn[$fWord];
  373. }
  374. return $relativeDate;
  375. }
  376. /**
  377. * Returns true this instance happened within the specified interval
  378. *
  379. * @param string|int $timeInterval the numeric value with space then time type.
  380. * Example of valid types: 6 hours, 2 days, 1 minute.
  381. * @return bool
  382. */
  383. public function wasWithinLast($timeInterval) {
  384. $tmp = str_replace(' ', '', $timeInterval);
  385. if (is_numeric($tmp)) {
  386. $timeInterval = $tmp . ' days';
  387. }
  388. $interval = new static('-' . $timeInterval);
  389. $now = new static();
  390. return $this >= $interval && $this <= $now;
  391. }
  392. /**
  393. * Returns true this instance will happen within the specified interval
  394. *
  395. * @param string|int $timeInterval the numeric value with space then time type.
  396. * Example of valid types: 6 hours, 2 days, 1 minute.
  397. * @return bool
  398. */
  399. public function isWithinNext($timeInterval) {
  400. $tmp = str_replace(' ', '', $timeInterval);
  401. if (is_numeric($tmp)) {
  402. $timeInterval = $tmp . ' days';
  403. }
  404. $interval = new static('+' . $timeInterval);
  405. $now = new static();
  406. return $this <= $interval && $this >= $now;
  407. }
  408. /**
  409. * Returns a formatted date string, given either a UNIX timestamp or a valid strtotime() date string.
  410. * This function also accepts a time string and a format string as first and second parameters.
  411. * In that case this function behaves as a wrapper for TimeHelper::i18nFormat()
  412. *
  413. * ## Examples
  414. *
  415. * Create localized & formatted time:
  416. *
  417. * {{{
  418. * Cake\Utility\Time::format('2012-02-15', '%m-%d-%Y'); // returns 02-15-2012
  419. * Cake\Utility\Time::format('2012-02-15 23:01:01', '%c'); // returns preferred date and time based on configured locale
  420. * Cake\Utility\Time::format('0000-00-00', '%d-%m-%Y', 'N/A'); // return N/A becuase an invalid date was passed
  421. * Cake\Utility\Time::format('2012-02-15 23:01:01', '%c', 'N/A', 'America/New_York'); // converts passed date to timezone
  422. * }}}
  423. *
  424. * @param string $format strftime format string.
  425. * @param string|\DateTimeZone $timezone Timezone string or DateTimeZone object
  426. * in which the date will be displayed. The timezone stored for this object will not
  427. * be changed.
  428. * @param $locale The locale name in which the date should be displayed (e.g. pt-BR)
  429. * @return string Formatted and translated date string
  430. * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::i18nFormat
  431. */
  432. public function i18nFormat($format = null, $timezone = null, $locale = null) {
  433. $time = $this;
  434. if ($timezone) {
  435. $time = clone $this;
  436. $time->timezone($timezone);
  437. }
  438. $format = $format !== null ? $format : static::$toStringFormat;
  439. $locale = $locale ?: static::$defaultLocale;
  440. return IntlDateFormatter::formatObject($time, $format, $locale);
  441. }
  442. /**
  443. * Get list of timezone identifiers
  444. *
  445. * @param int|string $filter A regex to filter identifer
  446. * Or one of DateTimeZone class constants
  447. * @param string $country A two-letter ISO 3166-1 compatible country code.
  448. * This option is only used when $filter is set to DateTimeZone::PER_COUNTRY
  449. * @param bool $group If true (default value) groups the identifiers list by primary region
  450. * @return array List of timezone identifiers
  451. * @since 2.2
  452. * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::listTimezones
  453. */
  454. public static function listTimezones($filter = null, $country = null, $group = true) {
  455. $regex = null;
  456. if (is_string($filter)) {
  457. $regex = $filter;
  458. $filter = null;
  459. }
  460. if ($filter === null) {
  461. $filter = \DateTimeZone::ALL;
  462. }
  463. $identifiers = \DateTimeZone::listIdentifiers($filter, $country);
  464. if ($regex) {
  465. foreach ($identifiers as $key => $tz) {
  466. if (!preg_match($regex, $tz)) {
  467. unset($identifiers[$key]);
  468. }
  469. }
  470. }
  471. if ($group) {
  472. $groupedIdentifiers = array();
  473. foreach ($identifiers as $key => $tz) {
  474. $item = explode('/', $tz, 2);
  475. if (isset($item[1])) {
  476. $groupedIdentifiers[$item[0]][$tz] = $item[1];
  477. } else {
  478. $groupedIdentifiers[$item[0]] = array($tz => $item[0]);
  479. }
  480. }
  481. return $groupedIdentifiers;
  482. }
  483. return array_combine($identifiers, $identifiers);
  484. }
  485. }