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.2.0
13: * @license https://opensource.org/licenses/mit-license.php MIT License
14: */
15: namespace Cake\I18n;
16:
17: use Cake\Chronos\Chronos;
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: * This object provides an immutable variant of Cake\I18n\Time
28: */
29: class FrozenTime extends Chronos implements JsonSerializable
30: {
31: use DateFormatTrait;
32:
33: /**
34: * The format to use when formatting a time using `Cake\I18n\FrozenTime::i18nFormat()`
35: * and `__toString`
36: *
37: * The format should be either the formatting constants from IntlDateFormatter as
38: * described in (https://secure.php.net/manual/en/class.intldateformatter.php) or a pattern
39: * as specified in (http://www.icu-project.org/apiref/icu4c/classSimpleDateFormat.html#details)
40: *
41: * It is possible to provide an array of 2 constants. In this case, the first position
42: * will be used for formatting the date part of the object and the second position
43: * will be used to format the time part.
44: *
45: * @var string|array|int
46: * @see \Cake\I18n\FrozenTime::i18nFormat()
47: */
48: protected static $_toStringFormat = [IntlDateFormatter::SHORT, IntlDateFormatter::SHORT];
49:
50: /**
51: * The format to use when formatting a time using `Cake\I18n\FrozenTime::nice()`
52: *
53: * The format should be either the formatting constants from IntlDateFormatter as
54: * described in (https://secure.php.net/manual/en/class.intldateformatter.php) or a pattern
55: * as specified in (http://www.icu-project.org/apiref/icu4c/classSimpleDateFormat.html#details)
56: *
57: * It is possible to provide an array of 2 constants. In this case, the first position
58: * will be used for formatting the date part of the object and the second position
59: * will be used to format the time part.
60: *
61: * @var string|array|int
62: * @see \Cake\I18n\FrozenTime::nice()
63: */
64: public static $niceFormat = [IntlDateFormatter::MEDIUM, IntlDateFormatter::SHORT];
65:
66: /**
67: * The format to use when formatting a time using `Cake\I18n\FrozenTime::timeAgoInWords()`
68: * and the difference is more than `Cake\I18n\FrozenTime::$wordEnd`
69: *
70: * @var string|array|int
71: * @see \Cake\I18n\FrozenTime::timeAgoInWords()
72: */
73: public static $wordFormat = [IntlDateFormatter::SHORT, -1];
74:
75: /**
76: * The format to use when formatting a time using `Time::timeAgoInWords()`
77: * and the difference is less than `Time::$wordEnd`
78: *
79: * @var array
80: * @see \Cake\I18n\FrozenTime::timeAgoInWords()
81: */
82: public static $wordAccuracy = [
83: 'year' => 'day',
84: 'month' => 'day',
85: 'week' => 'day',
86: 'day' => 'hour',
87: 'hour' => 'minute',
88: 'minute' => 'minute',
89: 'second' => 'second',
90: ];
91:
92: /**
93: * The end of relative time telling
94: *
95: * @var string
96: * @see \Cake\I18n\FrozenTime::timeAgoInWords()
97: */
98: public static $wordEnd = '+1 month';
99:
100: /**
101: * serialise the value as a Unix Timestamp
102: *
103: * @var string
104: */
105: const UNIX_TIMESTAMP_FORMAT = 'unixTimestampFormat';
106:
107: /**
108: * {@inheritDoc}
109: */
110: public function __construct($time = null, $tz = null)
111: {
112: if ($time instanceof DateTimeInterface) {
113: $tz = $time->getTimezone();
114: $time = $time->format('Y-m-d H:i:s');
115: }
116:
117: if (is_numeric($time)) {
118: $time = '@' . $time;
119: }
120:
121: parent::__construct($time, $tz);
122: }
123:
124: /**
125: * Returns either a relative or a formatted absolute date depending
126: * on the difference between the current time and this object.
127: *
128: * ### Options:
129: *
130: * - `from` => another Time object representing the "now" time
131: * - `format` => a fall back format if the relative time is longer than the duration specified by end
132: * - `accuracy` => Specifies how accurate the date should be described (array)
133: * - year => The format if years > 0 (default "day")
134: * - month => The format if months > 0 (default "day")
135: * - week => The format if weeks > 0 (default "day")
136: * - day => The format if weeks > 0 (default "hour")
137: * - hour => The format if hours > 0 (default "minute")
138: * - minute => The format if minutes > 0 (default "minute")
139: * - second => The format if seconds > 0 (default "second")
140: * - `end` => The end of relative time telling
141: * - `relativeString` => The printf compatible string when outputting relative time
142: * - `absoluteString` => The printf compatible string when outputting absolute time
143: * - `timezone` => The user timezone the timestamp should be formatted in.
144: *
145: * Relative dates look something like this:
146: *
147: * - 3 weeks, 4 days ago
148: * - 15 seconds ago
149: *
150: * Default date formatting is d/M/YY e.g: on 18/2/09. Formatting is done internally using
151: * `i18nFormat`, see the method for the valid formatting strings
152: *
153: * The returned string includes 'ago' or 'on' and assumes you'll properly add a word
154: * like 'Posted ' before the function output.
155: *
156: * NOTE: If the difference is one week or more, the lowest level of accuracy is day
157: *
158: * @param array $options Array of options.
159: * @return string Relative time string.
160: */
161: public function timeAgoInWords(array $options = [])
162: {
163: return static::diffFormatter()->timeAgoInWords($this, $options);
164: }
165:
166: /**
167: * Get list of timezone identifiers
168: *
169: * @param int|string|null $filter A regex to filter identifier
170: * Or one of DateTimeZone class constants
171: * @param string|null $country A two-letter ISO 3166-1 compatible country code.
172: * This option is only used when $filter is set to DateTimeZone::PER_COUNTRY
173: * @param bool|array $options If true (default value) groups the identifiers list by primary region.
174: * Otherwise, an array containing `group`, `abbr`, `before`, and `after`
175: * keys. Setting `group` and `abbr` to true will group results and append
176: * timezone abbreviation in the display value. Set `before` and `after`
177: * to customize the abbreviation wrapper.
178: * @return array List of timezone identifiers
179: * @since 2.2
180: */
181: public static function listTimezones($filter = null, $country = null, $options = [])
182: {
183: if (is_bool($options)) {
184: $options = [
185: 'group' => $options,
186: ];
187: }
188: $defaults = [
189: 'group' => true,
190: 'abbr' => false,
191: 'before' => ' - ',
192: 'after' => null,
193: ];
194: $options += $defaults;
195: $group = $options['group'];
196:
197: $regex = null;
198: if (is_string($filter)) {
199: $regex = $filter;
200: $filter = null;
201: }
202: if ($filter === null) {
203: $filter = DateTimeZone::ALL;
204: }
205: $identifiers = DateTimeZone::listIdentifiers($filter, $country);
206:
207: if ($regex) {
208: foreach ($identifiers as $key => $tz) {
209: if (!preg_match($regex, $tz)) {
210: unset($identifiers[$key]);
211: }
212: }
213: }
214:
215: if ($group) {
216: $groupedIdentifiers = [];
217: $now = time();
218: $before = $options['before'];
219: $after = $options['after'];
220: foreach ($identifiers as $key => $tz) {
221: $abbr = null;
222: if ($options['abbr']) {
223: $dateTimeZone = new DateTimeZone($tz);
224: $trans = $dateTimeZone->getTransitions($now, $now);
225: $abbr = isset($trans[0]['abbr']) ?
226: $before . $trans[0]['abbr'] . $after :
227: null;
228: }
229: $item = explode('/', $tz, 2);
230: if (isset($item[1])) {
231: $groupedIdentifiers[$item[0]][$tz] = $item[1] . $abbr;
232: } else {
233: $groupedIdentifiers[$item[0]] = [$tz => $item[0] . $abbr];
234: }
235: }
236:
237: return $groupedIdentifiers;
238: }
239:
240: return array_combine($identifiers, $identifiers);
241: }
242:
243: /**
244: * Returns true this instance happened within the specified interval
245: *
246: * This overridden method provides backwards compatible behavior for integers,
247: * or strings with trailing spaces. This behavior is *deprecated* and will be
248: * removed in future versions of CakePHP.
249: *
250: * @param string|int $timeInterval the numeric value with space then time type.
251: * Example of valid types: 6 hours, 2 days, 1 minute.
252: * @return bool
253: */
254: public function wasWithinLast($timeInterval)
255: {
256: $tmp = trim($timeInterval);
257: if (is_numeric($tmp)) {
258: deprecationWarning(
259: 'Passing int/numeric string into FrozenTime::wasWithinLast() is deprecated. ' .
260: 'Pass strings including interval eg. "6 days"'
261: );
262: $timeInterval = $tmp . ' days';
263: }
264:
265: return parent::wasWithinLast($timeInterval);
266: }
267:
268: /**
269: * Returns true this instance will happen within the specified interval
270: *
271: * This overridden method provides backwards compatible behavior for integers,
272: * or strings with trailing spaces. This behavior is *deprecated* and will be
273: * removed in future versions of CakePHP.
274: *
275: * @param string|int $timeInterval the numeric value with space then time type.
276: * Example of valid types: 6 hours, 2 days, 1 minute.
277: * @return bool
278: */
279: public function isWithinNext($timeInterval)
280: {
281: $tmp = trim($timeInterval);
282: if (is_numeric($tmp)) {
283: deprecationWarning(
284: 'Passing int/numeric string into FrozenTime::isWithinNext() is deprecated. ' .
285: 'Pass strings including interval eg. "6 days"'
286: );
287: $timeInterval = $tmp . ' days';
288: }
289:
290: return parent::isWithinNext($timeInterval);
291: }
292: }
293: