1: <?php
2: /**
3: * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4: * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
5: *
6: * Licensed under The MIT License
7: * For full copyright and license information, please see the LICENSE.txt
8: * Redistributions of files must retain the above copyright notice.
9: *
10: * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
11: * @link https://cakephp.org CakePHP(tm) Project
12: * @since 3.0.0
13: * @license https://opensource.org/licenses/mit-license.php MIT License
14: */
15: namespace Cake\I18n;
16:
17: use Cake\Chronos\MutableDateTime;
18: use DateTimeInterface;
19: use DateTimeZone;
20: use IntlDateFormatter;
21: use JsonSerializable;
22:
23: /**
24: * Extends the built-in DateTime class to provide handy methods and locale-aware
25: * formatting helpers
26: */
27: class Time extends MutableDateTime implements JsonSerializable
28: {
29: use DateFormatTrait;
30:
31: /**
32: * The format to use when formatting a time using `Cake\I18n\Time::i18nFormat()`
33: * and `__toString`
34: *
35: * The format should be either the formatting constants from IntlDateFormatter as
36: * described in (https://secure.php.net/manual/en/class.intldateformatter.php) or a pattern
37: * as specified in (http://www.icu-project.org/apiref/icu4c/classSimpleDateFormat.html#details)
38: *
39: * It is possible to provide an array of 2 constants. In this case, the first position
40: * will be used for formatting the date part of the object and the second position
41: * will be used to format the time part.
42: *
43: * @var string|array|int
44: * @see \Cake\I18n\Time::i18nFormat()
45: */
46: protected static $_toStringFormat = [IntlDateFormatter::SHORT, IntlDateFormatter::SHORT];
47:
48: /**
49: * The format to use when formatting a time using `Cake\I18n\Time::nice()`
50: *
51: * The format should be either the formatting constants from IntlDateFormatter as
52: * described in (https://secure.php.net/manual/en/class.intldateformatter.php) or a pattern
53: * as specified in (http://www.icu-project.org/apiref/icu4c/classSimpleDateFormat.html#details)
54: *
55: * It is possible to provide an array of 2 constants. In this case, the first position
56: * will be used for formatting the date part of the object and the second position
57: * will be used to format the time part.
58: *
59: * @var string|array|int
60: * @see \Cake\I18n\Time::nice()
61: */
62: public static $niceFormat = [IntlDateFormatter::MEDIUM, IntlDateFormatter::SHORT];
63:
64: /**
65: * The format to use when formatting a time using `Cake\I18n\Time::timeAgoInWords()`
66: * and the difference is more than `Cake\I18n\Time::$wordEnd`
67: *
68: * @var string|array|int
69: * @see \Cake\I18n\Time::timeAgoInWords()
70: */
71: public static $wordFormat = [IntlDateFormatter::SHORT, -1];
72:
73: /**
74: * The format to use when formatting a time using `Time::timeAgoInWords()`
75: * and the difference is less than `Time::$wordEnd`
76: *
77: * @var array
78: * @see \Cake\I18n\Time::timeAgoInWords()
79: */
80: public static $wordAccuracy = [
81: 'year' => 'day',
82: 'month' => 'day',
83: 'week' => 'day',
84: 'day' => 'hour',
85: 'hour' => 'minute',
86: 'minute' => 'minute',
87: 'second' => 'second',
88: ];
89:
90: /**
91: * The end of relative time telling
92: *
93: * @var string
94: * @see \Cake\I18n\Time::timeAgoInWords()
95: */
96: public static $wordEnd = '+1 month';
97:
98: /**
99: * serialise the value as a Unix Timestamp
100: *
101: * @var string
102: */
103: const UNIX_TIMESTAMP_FORMAT = 'unixTimestampFormat';
104:
105: /**
106: * {@inheritDoc}
107: */
108: public function __construct($time = null, $tz = null)
109: {
110: if ($time instanceof DateTimeInterface) {
111: $tz = $time->getTimezone();
112: $time = $time->format('Y-m-d H:i:s');
113: }
114:
115: if (is_numeric($time)) {
116: $time = '@' . $time;
117: }
118: parent::__construct($time, $tz);
119: }
120:
121: /**
122: * Returns a nicely formatted date string for this object.
123: *
124: * The format to be used is stored in the static property `Time::niceFormat`.
125: *
126: * @param string|\DateTimeZone|null $timezone Timezone string or DateTimeZone object
127: * in which the date will be displayed. The timezone stored for this object will not
128: * be changed.
129: * @param string|null $locale The locale name in which the date should be displayed (e.g. pt-BR)
130: * @return string Formatted date string
131: */
132: public function nice($timezone = null, $locale = null)
133: {
134: return $this->i18nFormat(static::$niceFormat, $timezone, $locale);
135: }
136:
137: /**
138: * Returns true if this object represents a date within the current week
139: *
140: * @return bool
141: */
142: public function isThisWeek()
143: {
144: return static::now($this->getTimezone())->format('W o') == $this->format('W o');
145: }
146:
147: /**
148: * Returns true if this object represents a date within the current month
149: *
150: * @return bool
151: */
152: public function isThisMonth()
153: {
154: return static::now($this->getTimezone())->format('m Y') == $this->format('m Y');
155: }
156:
157: /**
158: * Returns true if this object represents a date within the current year
159: *
160: * @return bool
161: */
162: public function isThisYear()
163: {
164: return static::now($this->getTimezone())->format('Y') == $this->format('Y');
165: }
166:
167: /**
168: * Returns the quarter
169: *
170: * @param bool $range Range.
171: * @return int|array 1, 2, 3, or 4 quarter of year, or array if $range true
172: */
173: public function toQuarter($range = false)
174: {
175: $quarter = (int)ceil($this->format('m') / 3);
176: if ($range === false) {
177: return $quarter;
178: }
179:
180: $year = $this->format('Y');
181: switch ($quarter) {
182: case 1:
183: return [$year . '-01-01', $year . '-03-31'];
184: case 2:
185: return [$year . '-04-01', $year . '-06-30'];
186: case 3:
187: return [$year . '-07-01', $year . '-09-30'];
188: case 4:
189: return [$year . '-10-01', $year . '-12-31'];
190: }
191: }
192:
193: /**
194: * Returns a UNIX timestamp.
195: *
196: * @return string UNIX timestamp
197: */
198: public function toUnixString()
199: {
200: return $this->format('U');
201: }
202:
203: /**
204: * Returns either a relative or a formatted absolute date depending
205: * on the difference between the current time and this object.
206: *
207: * ### Options:
208: *
209: * - `from` => another Time object representing the "now" time
210: * - `format` => a fall back format if the relative time is longer than the duration specified by end
211: * - `accuracy` => Specifies how accurate the date should be described (array)
212: * - year => The format if years > 0 (default "day")
213: * - month => The format if months > 0 (default "day")
214: * - week => The format if weeks > 0 (default "day")
215: * - day => The format if weeks > 0 (default "hour")
216: * - hour => The format if hours > 0 (default "minute")
217: * - minute => The format if minutes > 0 (default "minute")
218: * - second => The format if seconds > 0 (default "second")
219: * - `end` => The end of relative time telling
220: * - `relativeString` => The `printf` compatible string when outputting relative time
221: * - `absoluteString` => The `printf` compatible string when outputting absolute time
222: * - `timezone` => The user timezone the timestamp should be formatted in.
223: *
224: * Relative dates look something like this:
225: *
226: * - 3 weeks, 4 days ago
227: * - 15 seconds ago
228: *
229: * Default date formatting is d/M/YY e.g: on 18/2/09. Formatting is done internally using
230: * `i18nFormat`, see the method for the valid formatting strings
231: *
232: * The returned string includes 'ago' or 'on' and assumes you'll properly add a word
233: * like 'Posted ' before the function output.
234: *
235: * NOTE: If the difference is one week or more, the lowest level of accuracy is day
236: *
237: * @param array $options Array of options.
238: * @return string Relative time string.
239: */
240: public function timeAgoInWords(array $options = [])
241: {
242: return static::diffFormatter()->timeAgoInWords($this, $options);
243: }
244:
245: /**
246: * Get list of timezone identifiers
247: *
248: * @param int|string|null $filter A regex to filter identifier
249: * Or one of DateTimeZone class constants
250: * @param string|null $country A two-letter ISO 3166-1 compatible country code.
251: * This option is only used when $filter is set to DateTimeZone::PER_COUNTRY
252: * @param bool|array $options If true (default value) groups the identifiers list by primary region.
253: * Otherwise, an array containing `group`, `abbr`, `before`, and `after`
254: * keys. Setting `group` and `abbr` to true will group results and append
255: * timezone abbreviation in the display value. Set `before` and `after`
256: * to customize the abbreviation wrapper.
257: * @return array List of timezone identifiers
258: * @since 2.2
259: */
260: public static function listTimezones($filter = null, $country = null, $options = [])
261: {
262: if (is_bool($options)) {
263: $options = [
264: 'group' => $options,
265: ];
266: }
267: $defaults = [
268: 'group' => true,
269: 'abbr' => false,
270: 'before' => ' - ',
271: 'after' => null,
272: ];
273: $options += $defaults;
274: $group = $options['group'];
275:
276: $regex = null;
277: if (is_string($filter)) {
278: $regex = $filter;
279: $filter = null;
280: }
281: if ($filter === null) {
282: $filter = DateTimeZone::ALL;
283: }
284: $identifiers = DateTimeZone::listIdentifiers($filter, $country);
285:
286: if ($regex) {
287: foreach ($identifiers as $key => $tz) {
288: if (!preg_match($regex, $tz)) {
289: unset($identifiers[$key]);
290: }
291: }
292: }
293:
294: if ($group) {
295: $groupedIdentifiers = [];
296: $now = time();
297: $before = $options['before'];
298: $after = $options['after'];
299: foreach ($identifiers as $key => $tz) {
300: $abbr = null;
301: if ($options['abbr']) {
302: $dateTimeZone = new DateTimeZone($tz);
303: $trans = $dateTimeZone->getTransitions($now, $now);
304: $abbr = isset($trans[0]['abbr']) ?
305: $before . $trans[0]['abbr'] . $after :
306: null;
307: }
308: $item = explode('/', $tz, 2);
309: if (isset($item[1])) {
310: $groupedIdentifiers[$item[0]][$tz] = $item[1] . $abbr;
311: } else {
312: $groupedIdentifiers[$item[0]] = [$tz => $item[0] . $abbr];
313: }
314: }
315:
316: return $groupedIdentifiers;
317: }
318:
319: return array_combine($identifiers, $identifiers);
320: }
321:
322: /**
323: * Returns true this instance will happen within the specified interval
324: *
325: * This overridden method provides backwards compatible behavior for integers,
326: * or strings with trailing spaces. This behavior is *deprecated* and will be
327: * removed in future versions of CakePHP.
328: *
329: * @param string|int $timeInterval the numeric value with space then time type.
330: * Example of valid types: 6 hours, 2 days, 1 minute.
331: * @return bool
332: */
333: public function wasWithinLast($timeInterval)
334: {
335: $tmp = trim($timeInterval);
336: if (is_numeric($tmp)) {
337: deprecationWarning(
338: 'Passing int/numeric string into Time::wasWithinLast() is deprecated. ' .
339: 'Pass strings including interval eg. "6 days"'
340: );
341: $timeInterval = $tmp . ' days';
342: }
343:
344: return parent::wasWithinLast($timeInterval);
345: }
346:
347: /**
348: * Returns true this instance happened within the specified interval
349: *
350: * This overridden method provides backwards compatible behavior for integers,
351: * or strings with trailing spaces. This behavior is *deprecated* and will be
352: * removed in future versions of CakePHP.
353: *
354: * @param string|int $timeInterval the numeric value with space then time type.
355: * Example of valid types: 6 hours, 2 days, 1 minute.
356: * @return bool
357: */
358: public function isWithinNext($timeInterval)
359: {
360: $tmp = trim($timeInterval);
361: if (is_numeric($tmp)) {
362: deprecationWarning(
363: 'Passing int/numeric string into Time::isWithinNext() is deprecated. ' .
364: 'Pass strings including interval eg. "6 days"'
365: );
366: $timeInterval = $tmp . ' days';
367: }
368:
369: return parent::isWithinNext($timeInterval);
370: }
371: }
372: