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\Date as ChronosDate;
18: use Cake\Chronos\MutableDate;
19: use IntlDateFormatter;
20: use RuntimeException;
21:
22: /**
23: * Trait for date formatting methods shared by both Time & Date.
24: *
25: * This trait expects that the implementing class define static::$_toStringFormat.
26: */
27: trait DateFormatTrait
28: {
29:
30: /**
31: * The default locale to be used for displaying formatted date strings.
32: *
33: * @var string
34: * @deprecated 3.2.9 Use static::setDefaultLocale() and static::getDefaultLocale() instead.
35: */
36: public static $defaultLocale;
37:
38: /**
39: * In-memory cache of date formatters
40: *
41: * @var \IntlDateFormatter[]
42: */
43: protected static $_formatters = [];
44:
45: /**
46: * The format to use when when converting this object to json
47: *
48: * The format should be either the formatting constants from IntlDateFormatter as
49: * described in (https://secure.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 string|array|int
57: * @see \Cake\I18n\Time::i18nFormat()
58: */
59: protected static $_jsonEncodeFormat = "yyyy-MM-dd'T'HH:mm:ssxxx";
60:
61: /**
62: * Caches whether or not this class is a subclass of a Date or MutableDate
63: *
64: * @var bool
65: */
66: protected static $_isDateInstance;
67:
68: /**
69: * Gets the default locale.
70: *
71: * @return string|null The default locale string to be used or null.
72: */
73: public static function getDefaultLocale()
74: {
75: return static::$defaultLocale;
76: }
77:
78: /**
79: * Sets the default locale.
80: *
81: * @param string|null $locale The default locale string to be used or null.
82: * @return void
83: */
84: public static function setDefaultLocale($locale = null)
85: {
86: static::$defaultLocale = $locale;
87: }
88:
89: /**
90: * Returns a nicely formatted date string for this object.
91: *
92: * The format to be used is stored in the static property `Time::niceFormat`.
93: *
94: * @param string|\DateTimeZone|null $timezone Timezone string or DateTimeZone object
95: * in which the date will be displayed. The timezone stored for this object will not
96: * be changed.
97: * @param string|null $locale The locale name in which the date should be displayed (e.g. pt-BR)
98: * @return string Formatted date string
99: */
100: public function nice($timezone = null, $locale = null)
101: {
102: return $this->i18nFormat(static::$niceFormat, $timezone, $locale);
103: }
104:
105: /**
106: * Returns a formatted string for this time object using the preferred format and
107: * language for the specified locale.
108: *
109: * It is possible to specify the desired format for the string to be displayed.
110: * You can either pass `IntlDateFormatter` constants as the first argument of this
111: * function, or pass a full ICU date formatting string as specified in the following
112: * resource: http://www.icu-project.org/apiref/icu4c/classSimpleDateFormat.html#details.
113: *
114: * Additional to `IntlDateFormatter` constants and date formatting string you can use
115: * Time::UNIX_TIMESTAMP_FORMAT to get a unix timestamp
116: *
117: * ### Examples
118: *
119: * ```
120: * $time = new Time('2014-04-20 22:10');
121: * $time->i18nFormat(); // outputs '4/20/14, 10:10 PM' for the en-US locale
122: * $time->i18nFormat(\IntlDateFormatter::FULL); // Use the full date and time format
123: * $time->i18nFormat([\IntlDateFormatter::FULL, \IntlDateFormatter::SHORT]); // Use full date but short time format
124: * $time->i18nFormat('yyyy-MM-dd HH:mm:ss'); // outputs '2014-04-20 22:10'
125: * $time->i18nFormat(Time::UNIX_TIMESTAMP_FORMAT); // outputs '1398031800'
126: * ```
127: *
128: * If you wish to control the default format to be used for this method, you can alter
129: * the value of the static `Time::$defaultLocale` variable and set it to one of the
130: * possible formats accepted by this function.
131: *
132: * You can read about the available IntlDateFormatter constants at
133: * https://secure.php.net/manual/en/class.intldateformatter.php
134: *
135: * If you need to display the date in a different timezone than the one being used for
136: * this Time object without altering its internal state, you can pass a timezone
137: * string or object as the second parameter.
138: *
139: * Finally, should you need to use a different locale for displaying this time object,
140: * pass a locale string as the third parameter to this function.
141: *
142: * ### Examples
143: *
144: * ```
145: * $time = new Time('2014-04-20 22:10');
146: * $time->i18nFormat(null, null, 'de-DE');
147: * $time->i18nFormat(\IntlDateFormatter::FULL, 'Europe/Berlin', 'de-DE');
148: * ```
149: *
150: * You can control the default locale to be used by setting the static variable
151: * `Time::$defaultLocale` to a valid locale string. If empty, the default will be
152: * taken from the `intl.default_locale` ini config.
153: *
154: * @param string|int|null $format Format string.
155: * @param string|\DateTimeZone|null $timezone Timezone string or DateTimeZone object
156: * in which the date will be displayed. The timezone stored for this object will not
157: * be changed.
158: * @param string|null $locale The locale name in which the date should be displayed (e.g. pt-BR)
159: * @return string|int Formatted and translated date string
160: */
161: public function i18nFormat($format = null, $timezone = null, $locale = null)
162: {
163: if ($format === Time::UNIX_TIMESTAMP_FORMAT) {
164: return $this->getTimestamp();
165: }
166:
167: $time = $this;
168:
169: if ($timezone) {
170: // Handle the immutable and mutable object cases.
171: $time = clone $this;
172: $time = $time->timezone($timezone);
173: }
174:
175: $format = $format !== null ? $format : static::$_toStringFormat;
176: $locale = $locale ?: static::$defaultLocale;
177:
178: return $this->_formatObject($time, $format, $locale);
179: }
180:
181: /**
182: * Returns a translated and localized date string.
183: * Implements what IntlDateFormatter::formatObject() is in PHP 5.5+
184: *
185: * @param \DateTime $date Date.
186: * @param string|int|array $format Format.
187: * @param string $locale The locale name in which the date should be displayed.
188: * @return string
189: */
190: protected function _formatObject($date, $format, $locale)
191: {
192: $pattern = $dateFormat = $timeFormat = $calendar = null;
193:
194: if (is_array($format)) {
195: list($dateFormat, $timeFormat) = $format;
196: } elseif (is_numeric($format)) {
197: $dateFormat = $format;
198: } else {
199: $dateFormat = $timeFormat = IntlDateFormatter::FULL;
200: $pattern = $format;
201: }
202:
203: if (preg_match('/@calendar=(japanese|buddhist|chinese|persian|indian|islamic|hebrew|coptic|ethiopic)/', $locale)) {
204: $calendar = IntlDateFormatter::TRADITIONAL;
205: } else {
206: $calendar = IntlDateFormatter::GREGORIAN;
207: }
208:
209: $timezone = $date->getTimezone()->getName();
210: $key = "{$locale}.{$dateFormat}.{$timeFormat}.{$timezone}.{$calendar}.{$pattern}";
211:
212: if (!isset(static::$_formatters[$key])) {
213: if ($timezone === '+00:00' || $timezone === 'Z') {
214: $timezone = 'UTC';
215: } elseif ($timezone[0] === '+' || $timezone[0] === '-') {
216: $timezone = 'GMT' . $timezone;
217: }
218: $formatter = datefmt_create(
219: $locale,
220: $dateFormat,
221: $timeFormat,
222: $timezone,
223: $calendar,
224: $pattern
225: );
226: if (!$formatter) {
227: throw new RuntimeException(
228: 'Your version of icu does not support creating a date formatter for ' .
229: "`$key`. You should try to upgrade libicu and the intl extension."
230: );
231: }
232: static::$_formatters[$key] = $formatter;
233: }
234:
235: return static::$_formatters[$key]->format($date->format('U'));
236: }
237:
238: /**
239: * {@inheritDoc}
240: */
241: public function __toString()
242: {
243: return $this->i18nFormat();
244: }
245:
246: /**
247: * Resets the format used to the default when converting an instance of this type to
248: * a string
249: *
250: * @return void
251: */
252: public static function resetToStringFormat()
253: {
254: static::setToStringFormat([IntlDateFormatter::SHORT, IntlDateFormatter::SHORT]);
255: }
256:
257: /**
258: * Sets the default format used when type converting instances of this type to string
259: *
260: * @param string|array|int $format Format.
261: * @return void
262: */
263: public static function setToStringFormat($format)
264: {
265: static::$_toStringFormat = $format;
266: }
267:
268: /**
269: * Sets the default format used when converting this object to json
270: *
271: * @param string|array|int $format Format.
272: * @return void
273: */
274: public static function setJsonEncodeFormat($format)
275: {
276: static::$_jsonEncodeFormat = $format;
277: }
278:
279: /**
280: * Returns a new Time object after parsing the provided time string based on
281: * the passed or configured date time format. This method is locale dependent,
282: * Any string that is passed to this function will be interpreted as a locale
283: * dependent string.
284: *
285: * When no $format is provided, the `toString` format will be used.
286: *
287: * If it was impossible to parse the provided time, null will be returned.
288: *
289: * Example:
290: *
291: * ```
292: * $time = Time::parseDateTime('10/13/2013 12:54am');
293: * $time = Time::parseDateTime('13 Oct, 2013 13:54', 'dd MMM, y H:mm');
294: * $time = Time::parseDateTime('10/10/2015', [IntlDateFormatter::SHORT, -1]);
295: * ```
296: *
297: * @param string $time The time string to parse.
298: * @param string|array|null $format Any format accepted by IntlDateFormatter.
299: * @return static|null
300: */
301: public static function parseDateTime($time, $format = null)
302: {
303: $dateFormat = $format ?: static::$_toStringFormat;
304: $timeFormat = $pattern = null;
305:
306: if (is_array($dateFormat)) {
307: list($newDateFormat, $timeFormat) = $dateFormat;
308: $dateFormat = $newDateFormat;
309: } else {
310: $pattern = $dateFormat;
311: $dateFormat = null;
312: }
313:
314: if (static::$_isDateInstance === null) {
315: static::$_isDateInstance =
316: is_subclass_of(static::class, ChronosDate::class) ||
317: is_subclass_of(static::class, MutableDate::class);
318: }
319:
320: $defaultTimezone = static::$_isDateInstance ? 'UTC' : date_default_timezone_get();
321: $formatter = datefmt_create(
322: static::$defaultLocale,
323: $dateFormat,
324: $timeFormat,
325: $defaultTimezone,
326: null,
327: $pattern
328: );
329: $time = $formatter->parse($time);
330: if ($time !== false) {
331: $result = new static('@' . $time);
332:
333: return static::$_isDateInstance ? $result : $result->setTimezone($defaultTimezone);
334: }
335:
336: return null;
337: }
338:
339: /**
340: * Returns a new Time object after parsing the provided $date string based on
341: * the passed or configured date time format. This method is locale dependent,
342: * Any string that is passed to this function will be interpreted as a locale
343: * dependent string.
344: *
345: * When no $format is provided, the `wordFormat` format will be used.
346: *
347: * If it was impossible to parse the provided time, null will be returned.
348: *
349: * Example:
350: *
351: * ```
352: * $time = Time::parseDate('10/13/2013');
353: * $time = Time::parseDate('13 Oct, 2013', 'dd MMM, y');
354: * $time = Time::parseDate('13 Oct, 2013', IntlDateFormatter::SHORT);
355: * ```
356: *
357: * @param string $date The date string to parse.
358: * @param string|int|null $format Any format accepted by IntlDateFormatter.
359: * @return static|null
360: */
361: public static function parseDate($date, $format = null)
362: {
363: if (is_int($format)) {
364: $format = [$format, -1];
365: }
366: $format = $format ?: static::$wordFormat;
367:
368: return static::parseDateTime($date, $format);
369: }
370:
371: /**
372: * Returns a new Time object after parsing the provided $time string based on
373: * the passed or configured date time format. This method is locale dependent,
374: * Any string that is passed to this function will be interpreted as a locale
375: * dependent string.
376: *
377: * When no $format is provided, the IntlDateFormatter::SHORT format will be used.
378: *
379: * If it was impossible to parse the provided time, null will be returned.
380: *
381: * Example:
382: *
383: * ```
384: * $time = Time::parseTime('11:23pm');
385: * ```
386: *
387: * @param string $time The time string to parse.
388: * @param string|int|null $format Any format accepted by IntlDateFormatter.
389: * @return static|null
390: */
391: public static function parseTime($time, $format = null)
392: {
393: if (is_int($format)) {
394: $format = [-1, $format];
395: }
396: $format = $format ?: [-1, IntlDateFormatter::SHORT];
397:
398: return static::parseDateTime($time, $format);
399: }
400:
401: /**
402: * Returns a string that should be serialized when converting this object to json
403: *
404: * @return string
405: */
406: public function jsonSerialize()
407: {
408: return $this->i18nFormat(static::$_jsonEncodeFormat);
409: }
410:
411: /**
412: * Get the difference formatter instance or overwrite the current one.
413: *
414: * @param \Cake\I18n\RelativeTimeFormatter|null $formatter The formatter instance when setting.
415: * @return \Cake\I18n\RelativeTimeFormatter The formatter instance.
416: */
417: public static function diffFormatter($formatter = null)
418: {
419: if ($formatter === null) {
420: // Use the static property defined in chronos.
421: if (static::$diffFormatter === null) {
422: static::$diffFormatter = new RelativeTimeFormatter();
423: }
424:
425: return static::$diffFormatter;
426: }
427:
428: return static::$diffFormatter = $formatter;
429: }
430:
431: /**
432: * Returns the data that should be displayed when debugging this object
433: *
434: * @return array
435: */
436: public function __debugInfo()
437: {
438: return [
439: 'time' => $this->toIso8601String(),
440: 'timezone' => $this->getTimezone()->getName(),
441: 'fixedNowTime' => static::hasTestNow() ? static::getTestNow()->toIso8601String() : false
442: ];
443: }
444: }
445: