Completed
Push — master ( aca739...fadeb5 )
by Carsten
07:36
created

Formatter::format()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 22
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 5.0592

Importance

Changes 0
Metric Value
dl 0
loc 22
ccs 13
cts 15
cp 0.8667
rs 8.6737
c 0
b 0
f 0
cc 5
eloc 17
nc 6
nop 2
crap 5.0592
1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\i18n;
9
10
use Closure;
11
use DateInterval;
12
use DateTime;
13
use DateTimeInterface;
14
use DateTimeZone;
15
use IntlDateFormatter;
16
use NumberFormatter;
17
use Yii;
18
use yii\base\Component;
19
use yii\base\InvalidConfigException;
20
use yii\base\InvalidParamException;
21
use yii\helpers\FormatConverter;
22
use yii\helpers\Html;
23
use yii\helpers\HtmlPurifier;
24
25
/**
26
 * Formatter provides a set of commonly used data formatting methods.
27
 *
28
 * The formatting methods provided by Formatter are all named in the form of `asXyz()`.
29
 * The behavior of some of them may be configured via the properties of Formatter. For example,
30
 * by configuring [[dateFormat]], one may control how [[asDate()]] formats the value into a date string.
31
 *
32
 * Formatter is configured as an application component in [[\yii\base\Application]] by default.
33
 * You can access that instance via `Yii::$app->formatter`.
34
 *
35
 * The Formatter class is designed to format values according to a [[locale]]. For this feature to work
36
 * the [PHP intl extension](http://php.net/manual/en/book.intl.php) has to be installed.
37
 * Most of the methods however work also if the PHP intl extension is not installed by providing
38
 * a fallback implementation. Without intl month and day names are in English only.
39
 * Note that even if the intl extension is installed, formatting date and time values for years >=2038 or <=1901
40
 * on 32bit systems will fall back to the PHP implementation because intl uses a 32bit UNIX timestamp internally.
41
 * On a 64bit system the intl formatter is used in all cases if installed.
42
 *
43
 * @author Qiang Xue <[email protected]>
44
 * @author Enrica Ruedin <[email protected]>
45
 * @author Carsten Brandt <[email protected]>
46
 * @since 2.0
47
 */
48
class Formatter extends Component
49
{
50
    /**
51
     * @var string the text to be displayed when formatting a `null` value.
52
     * Defaults to `'<span class="not-set">(not set)</span>'`, where `(not set)`
53
     * will be translated according to [[locale]].
54
     */
55
    public $nullDisplay;
56
    /**
57
     * @var array the text to be displayed when formatting a boolean value. The first element corresponds
58
     * to the text displayed for `false`, the second element for `true`.
59
     * Defaults to `['No', 'Yes']`, where `Yes` and `No`
60
     * will be translated according to [[locale]].
61
     */
62
    public $booleanFormat;
63
    /**
64
     * @var string the locale ID that is used to localize the date and number formatting.
65
     * For number and date formatting this is only effective when the
66
     * [PHP intl extension](http://php.net/manual/en/book.intl.php) is installed.
67
     * If not set, [[\yii\base\Application::language]] will be used.
68
     */
69
    public $locale;
70
    /**
71
     * @var string the time zone to use for formatting time and date values.
72
     *
73
     * This can be any value that may be passed to [date_default_timezone_set()](http://www.php.net/manual/en/function.date-default-timezone-set.php)
74
     * e.g. `UTC`, `Europe/Berlin` or `America/Chicago`.
75
     * Refer to the [php manual](http://www.php.net/manual/en/timezones.php) for available time zones.
76
     * If this property is not set, [[\yii\base\Application::timeZone]] will be used.
77
     *
78
     * Note that the default time zone for input data is assumed to be UTC by default if no time zone is included in the input date value.
79
     * If you store your data in a different time zone in the database, you have to adjust [[defaultTimeZone]] accordingly.
80
     */
81
    public $timeZone;
82
    /**
83
     * @var string the time zone that is assumed for input values if they do not include a time zone explicitly.
84
     *
85
     * The value must be a valid time zone identifier, e.g. `UTC`, `Europe/Berlin` or `America/Chicago`.
86
     * Please refer to the [php manual](http://www.php.net/manual/en/timezones.php) for available time zones.
87
     *
88
     * It defaults to `UTC` so you only have to adjust this value if you store datetime values in another time zone in your database.
89
     *
90
     * Note that a UNIX timestamp is always in UTC by its definition. That means that specifying a default time zone different from
91
     * UTC has no effect on date values given as UNIX timestamp.
92
     *
93
     * @since 2.0.1
94
     */
95
    public $defaultTimeZone = 'UTC';
96
    /**
97
     * @var string the default format string to be used to format a [[asDate()|date]].
98
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
99
     *
100
     * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime#TOC-Date-Time-Format-Syntax).
101
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
102
     * PHP [date()](http://php.net/manual/en/function.date.php)-function.
103
     *
104
     * For example:
105
     *
106
     * ```php
107
     * 'MM/dd/yyyy' // date in ICU format
108
     * 'php:m/d/Y' // the same date in PHP format
109
     * ```
110
     */
111
    public $dateFormat = 'medium';
112
    /**
113
     * @var string the default format string to be used to format a [[asTime()|time]].
114
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
115
     *
116
     * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime#TOC-Date-Time-Format-Syntax).
117
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
118
     * PHP [date()](http://php.net/manual/en/function.date.php)-function.
119
     *
120
     * For example:
121
     *
122
     * ```php
123
     * 'HH:mm:ss' // time in ICU format
124
     * 'php:H:i:s' // the same time in PHP format
125
     * ```
126
     */
127
    public $timeFormat = 'medium';
128
    /**
129
     * @var string the default format string to be used to format a [[asDatetime()|date and time]].
130
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
131
     *
132
     * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime#TOC-Date-Time-Format-Syntax).
133
     *
134
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
135
     * PHP [date()](http://php.net/manual/en/function.date.php)-function.
136
     *
137
     * For example:
138
     *
139
     * ```php
140
     * 'MM/dd/yyyy HH:mm:ss' // date and time in ICU format
141
     * 'php:m/d/Y H:i:s' // the same date and time in PHP format
142
     * ```
143
     */
144
    public $datetimeFormat = 'medium';
145
    /**
146
     * @var \IntlCalendar|int|null the calendar to be used for date formatting. The value of this property will be directly
147
     * passed to the [constructor of the `IntlDateFormatter` class](http://php.net/manual/en/intldateformatter.create.php).
148
     *
149
     * Defaults to `null`, which means the Gregorian calendar will be used. You may also explicitly pass the constant
150
     * `\IntlDateFormatter::GREGORIAN` for Gregorian calendar.
151
     *
152
     * To use an alternative calendar like for example the [Jalali calendar](https://en.wikipedia.org/wiki/Jalali_calendar),
153
     * set this property to `\IntlDateFormatter::TRADITIONAL`.
154
     * The calendar must then be specified in the [[locale]], for example for the persian calendar the configuration for the formatter would be:
155
     *
156
     * ```php
157
     * 'formatter' => [
158
     *     'locale' => 'fa_IR@calendar=persian',
159
     *     'calendar' => \IntlDateFormatter::TRADITIONAL,
160
     * ],
161
     * ```
162
     *
163
     * Available calendar names can be found in the [ICU manual](http://userguide.icu-project.org/datetime/calendar).
164
     *
165
     * Since PHP 5.5 you may also use an instance of the [[\IntlCalendar]] class.
166
     * Check the [PHP manual](http://php.net/manual/en/intldateformatter.create.php) for more details.
167
     *
168
     * If the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, setting this property will have no effect.
169
     *
170
     * @see http://php.net/manual/en/intldateformatter.create.php
171
     * @see http://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants.calendartypes
172
     * @see http://php.net/manual/en/class.intlcalendar.php
173
     * @since 2.0.7
174
     */
175
    public $calendar;
176
    /**
177
     * @var string the character displayed as the decimal point when formatting a number.
178
     * If not set, the decimal separator corresponding to [[locale]] will be used.
179
     * If [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, the default value is '.'.
180
     */
181
    public $decimalSeparator;
182
    /**
183
     * @var string the character displayed as the thousands separator (also called grouping separator) character when formatting a number.
184
     * If not set, the thousand separator corresponding to [[locale]] will be used.
185
     * If [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, the default value is ','.
186
     */
187
    public $thousandSeparator;
188
    /**
189
     * @var array a list of name value pairs that are passed to the
190
     * intl [NumberFormatter::setAttribute()](http://php.net/manual/en/numberformatter.setattribute.php) method of all
191
     * the number formatter objects created by [[createNumberFormatter()]].
192
     * This property takes only effect if the [PHP intl extension](http://php.net/manual/en/book.intl.php) is installed.
193
     *
194
     * Please refer to the [PHP manual](http://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants.unumberformatattribute)
195
     * for the possible options.
196
     *
197
     * For example to adjust the maximum and minimum value of fraction digits you can configure this property like the following:
198
     *
199
     * ```php
200
     * [
201
     *     NumberFormatter::MIN_FRACTION_DIGITS => 0,
202
     *     NumberFormatter::MAX_FRACTION_DIGITS => 2,
203
     * ]
204
     * ```
205
     */
206
    public $numberFormatterOptions = [];
207
    /**
208
     * @var array a list of name value pairs that are passed to the
209
     * intl [NumberFormatter::setTextAttribute()](http://php.net/manual/en/numberformatter.settextattribute.php) method of all
210
     * the number formatter objects created by [[createNumberFormatter()]].
211
     * This property takes only effect if the [PHP intl extension](http://php.net/manual/en/book.intl.php) is installed.
212
     *
213
     * Please refer to the [PHP manual](http://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants.unumberformattextattribute)
214
     * for the possible options.
215
     *
216
     * For example to change the minus sign for negative numbers you can configure this property like the following:
217
     *
218
     * ```php
219
     * [
220
     *     NumberFormatter::NEGATIVE_PREFIX => 'MINUS',
221
     * ]
222
     * ```
223
     */
224
    public $numberFormatterTextOptions = [];
225
    /**
226
     * @var array a list of name value pairs that are passed to the
227
     * intl [NumberFormatter::setSymbol()](http://php.net/manual/en/numberformatter.setsymbol.php) method of all
228
     * the number formatter objects created by [[createNumberFormatter()]].
229
     * This property takes only effect if the [PHP intl extension](http://php.net/manual/en/book.intl.php) is installed.
230
     *
231
     * Please refer to the [PHP manual](http://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants.unumberformatsymbol)
232
     * for the possible options.
233
     *
234
     * For example to choose a custom currency symbol, e.g. [U+20BD](http://unicode-table.com/en/20BD/) instead of `руб.` for Russian Ruble:
235
     *
236
     * ```php
237
     * [
238
     *     NumberFormatter::CURRENCY_SYMBOL => '₽',
239
     * ]
240
     * ```
241
     *
242
     * @since 2.0.4
243
     */
244
    public $numberFormatterSymbols = [];
245
    /**
246
     * @var string the 3-letter ISO 4217 currency code indicating the default currency to use for [[asCurrency]].
247
     * If not set, the currency code corresponding to [[locale]] will be used.
248
     * Note that in this case the [[locale]] has to be specified with a country code, e.g. `en-US` otherwise it
249
     * is not possible to determine the default currency.
250
     */
251
    public $currencyCode;
252
    /**
253
     * @var int the base at which a kilobyte is calculated (1000 or 1024 bytes per kilobyte), used by [[asSize]] and [[asShortSize]].
254
     * Defaults to 1024.
255
     */
256
    public $sizeFormatBase = 1024;
257
258
    /**
259
     * @var bool whether the [PHP intl extension](http://php.net/manual/en/book.intl.php) is loaded.
260
     */
261
    private $_intlLoaded = false;
262
263
264
    /**
265
     * @inheritdoc
266
     */
267 247
    public function init()
268
    {
269 247
        if ($this->timeZone === null) {
270 247
            $this->timeZone = Yii::$app->timeZone;
271
        }
272 247
        if ($this->locale === null) {
273 30
            $this->locale = Yii::$app->language;
274
        }
275 247
        if ($this->booleanFormat === null) {
276 247
            $this->booleanFormat = [Yii::t('yii', 'No', [], $this->locale), Yii::t('yii', 'Yes', [], $this->locale)];
277
        }
278 247
        if ($this->nullDisplay === null) {
279 247
            $this->nullDisplay = '<span class="not-set">' . Yii::t('yii', '(not set)', [], $this->locale) . '</span>';
280
        }
281 247
        $this->_intlLoaded = extension_loaded('intl');
282 247
        if (!$this->_intlLoaded) {
283 115
            if ($this->decimalSeparator === null) {
284 115
                $this->decimalSeparator = '.';
285
            }
286 115
            if ($this->thousandSeparator === null) {
287 115
                $this->thousandSeparator = ',';
288
            }
289
        }
290 247
    }
291
292
    /**
293
     * Formats the value based on the given format type.
294
     * This method will call one of the "as" methods available in this class to do the formatting.
295
     * For type "xyz", the method "asXyz" will be used. For example, if the format is "html",
296
     * then [[asHtml()]] will be used. Format names are case insensitive.
297
     * @param mixed $value the value to be formatted.
298
     * @param string|array|Closure $format the format of the value, e.g., "html", "text" or an anonymous function
299
     * returning the formatted value.
300
     *
301
     * To specify additional parameters of the formatting method, you may use an array.
302
     * The first element of the array specifies the format name, while the rest of the elements will be used as the
303
     * parameters to the formatting method. For example, a format of `['date', 'Y-m-d']` will cause the invocation
304
     * of `asDate($value, 'Y-m-d')`.
305
     *
306
     * The anonymous function signature should be: `function($value, $formatter)`,
307
     * where `$value` is the value that should be formatted and `$formatter` is an instance of the Formatter class,
308
     * which can be used to call other formatting functions.
309
     * The possibility to use an anonymous function is available since version 2.0.13.
310
     * @return string the formatting result.
311
     * @throws InvalidParamException if the format type is not supported by this class.
312
     */
313 11
    public function format($value, $format)
314
    {
315 11
        if ($format instanceof Closure) {
316
            return call_user_func($format, $value, $this);
317 11
        } elseif (is_array($format)) {
318 7
            if (!isset($format[0])) {
319
                throw new InvalidParamException('The $format array must contain at least one element.');
320
            }
321 7
            $f = $format[0];
322 7
            $format[0] = $value;
323 7
            $params = $format;
324 7
            $format = $f;
325
        } else {
326 6
            $params = [$value];
327
        }
328 11
        $method = 'as' . $format;
329 11
        if ($this->hasMethod($method)) {
330 11
            return call_user_func_array([$this, $method], $params);
331
        } else {
332 2
            throw new InvalidParamException("Unknown format type: $format");
333
        }
334
    }
335
336
337
    // simple formats
338
339
340
    /**
341
     * Formats the value as is without any formatting.
342
     * This method simply returns back the parameter without any format.
343
     * The only exception is a `null` value which will be formatted using [[nullDisplay]].
344
     * @param mixed $value the value to be formatted.
345
     * @return string the formatted result.
346
     */
347 1
    public function asRaw($value)
348
    {
349 1
        if ($value === null) {
350 1
            return $this->nullDisplay;
351
        }
352 1
        return $value;
353
    }
354
355
    /**
356
     * Formats the value as an HTML-encoded plain text.
357
     * @param string $value the value to be formatted.
358
     * @return string the formatted result.
359
     */
360 5
    public function asText($value)
361
    {
362 5
        if ($value === null) {
363 2
            return $this->nullDisplay;
364
        }
365 5
        return Html::encode($value);
366
    }
367
368
    /**
369
     * Formats the value as an HTML-encoded plain text with newlines converted into breaks.
370
     * @param string $value the value to be formatted.
371
     * @return string the formatted result.
372
     */
373 1
    public function asNtext($value)
374
    {
375 1
        if ($value === null) {
376 1
            return $this->nullDisplay;
377
        }
378 1
        return nl2br(Html::encode($value));
379
    }
380
381
    /**
382
     * Formats the value as HTML-encoded text paragraphs.
383
     * Each text paragraph is enclosed within a `<p>` tag.
384
     * One or multiple consecutive empty lines divide two paragraphs.
385
     * @param string $value the value to be formatted.
386
     * @return string the formatted result.
387
     */
388 1
    public function asParagraphs($value)
389
    {
390 1
        if ($value === null) {
391 1
            return $this->nullDisplay;
392
        }
393 1
        return str_replace('<p></p>', '', '<p>' . preg_replace('/\R{2,}/u', "</p>\n<p>", Html::encode($value)) . '</p>');
394
    }
395
396
    /**
397
     * Formats the value as HTML text.
398
     * The value will be purified using [[HtmlPurifier]] to avoid XSS attacks.
399
     * Use [[asRaw()]] if you do not want any purification of the value.
400
     * @param string $value the value to be formatted.
401
     * @param array|null $config the configuration for the HTMLPurifier class.
402
     * @return string the formatted result.
403
     */
404
    public function asHtml($value, $config = null)
405
    {
406
        if ($value === null) {
407
            return $this->nullDisplay;
408
        }
409
        return HtmlPurifier::process($value, $config);
410
    }
411
412
    /**
413
     * Formats the value as a mailto link.
414
     * @param string $value the value to be formatted.
415
     * @param array $options the tag options in terms of name-value pairs. See [[Html::mailto()]].
416
     * @return string the formatted result.
417
     */
418 1
    public function asEmail($value, $options = [])
419
    {
420 1
        if ($value === null) {
421 1
            return $this->nullDisplay;
422
        }
423 1
        return Html::mailto(Html::encode($value), $value, $options);
424
    }
425
426
    /**
427
     * Formats the value as an image tag.
428
     * @param mixed $value the value to be formatted.
429
     * @param array $options the tag options in terms of name-value pairs. See [[Html::img()]].
430
     * @return string the formatted result.
431
     */
432 1
    public function asImage($value, $options = [])
433
    {
434 1
        if ($value === null) {
435 1
            return $this->nullDisplay;
436
        }
437 1
        return Html::img($value, $options);
438
    }
439
440
    /**
441
     * Formats the value as a hyperlink.
442
     * @param mixed $value the value to be formatted.
443
     * @param array $options the tag options in terms of name-value pairs. See [[Html::a()]].
444
     * @return string the formatted result.
445
     */
446 1
    public function asUrl($value, $options = [])
447
    {
448 1
        if ($value === null) {
449 1
            return $this->nullDisplay;
450
        }
451 1
        $url = $value;
452 1
        if (strpos($url, '://') === false) {
453 1
            $url = 'http://' . $url;
454
        }
455
456 1
        return Html::a(Html::encode($value), $url, $options);
457
    }
458
459
    /**
460
     * Formats the value as a boolean.
461
     * @param mixed $value the value to be formatted.
462
     * @return string the formatted result.
463
     * @see booleanFormat
464
     */
465 1
    public function asBoolean($value)
466
    {
467 1
        if ($value === null) {
468 1
            return $this->nullDisplay;
469
        }
470
471 1
        return $value ? $this->booleanFormat[1] : $this->booleanFormat[0];
472
    }
473
474
475
    // date and time formats
476
477
478
    /**
479
     * Formats the value as a date.
480
     * @param int|string|DateTime $value the value to be formatted. The following
481
     * types of value are supported:
482
     *
483
     * - an integer representing a UNIX timestamp. A UNIX timestamp is always in UTC by its definition.
484
     * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
485
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
486
     * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object. You may set the time zone
487
     *   for the DateTime object to specify the source time zone.
488
     *
489
     * The formatter will convert date values according to [[timeZone]] before formatting it.
490
     * If no timezone conversion should be performed, you need to set [[defaultTimeZone]] and [[timeZone]] to the same value.
491
     * Also no conversion will be performed on values that have no time information, e.g. `"2017-06-05"`.
492
     *
493
     * @param string $format the format used to convert the value into a date string.
494
     * If null, [[dateFormat]] will be used.
495
     *
496
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
497
     * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime).
498
     *
499
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
500
     * PHP [date()](http://php.net/manual/en/function.date.php)-function.
501
     *
502
     * @return string the formatted result.
503
     * @throws InvalidParamException if the input value can not be evaluated as a date value.
504
     * @throws InvalidConfigException if the date format is invalid.
505
     * @see dateFormat
506
     */
507 168
    public function asDate($value, $format = null)
508
    {
509 168
        if ($format === null) {
510 146
            $format = $this->dateFormat;
511
        }
512 168
        return $this->formatDateTimeValue($value, $format, 'date');
513
    }
514
515
    /**
516
     * Formats the value as a time.
517
     * @param int|string|DateTime $value the value to be formatted. The following
518
     * types of value are supported:
519
     *
520
     * - an integer representing a UNIX timestamp. A UNIX timestamp is always in UTC by its definition.
521
     * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
522
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
523
     * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object. You may set the time zone
524
     *   for the DateTime object to specify the source time zone.
525
     *
526
     * The formatter will convert date values according to [[timeZone]] before formatting it.
527
     * If no timezone conversion should be performed, you need to set [[defaultTimeZone]] and [[timeZone]] to the same value.
528
     *
529
     * @param string $format the format used to convert the value into a date string.
530
     * If null, [[timeFormat]] will be used.
531
     *
532
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
533
     * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime).
534
     *
535
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
536
     * PHP [date()](http://php.net/manual/en/function.date.php)-function.
537
     *
538
     * @return string the formatted result.
539
     * @throws InvalidParamException if the input value can not be evaluated as a date value.
540
     * @throws InvalidConfigException if the date format is invalid.
541
     * @see timeFormat
542
     */
543 148
    public function asTime($value, $format = null)
544
    {
545 148
        if ($format === null) {
546 144
            $format = $this->timeFormat;
547
        }
548 148
        return $this->formatDateTimeValue($value, $format, 'time');
549
    }
550
551
    /**
552
     * Formats the value as a datetime.
553
     * @param int|string|DateTime $value the value to be formatted. The following
554
     * types of value are supported:
555
     *
556
     * - an integer representing a UNIX timestamp. A UNIX timestamp is always in UTC by its definition.
557
     * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
558
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
559
     * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object. You may set the time zone
560
     *   for the DateTime object to specify the source time zone.
561
     *
562
     * The formatter will convert date values according to [[timeZone]] before formatting it.
563
     * If no timezone conversion should be performed, you need to set [[defaultTimeZone]] and [[timeZone]] to the same value.
564
     *
565
     * @param string $format the format used to convert the value into a date string.
566
     * If null, [[dateFormat]] will be used.
567
     *
568
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
569
     * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime).
570
     *
571
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
572
     * PHP [date()](http://php.net/manual/en/function.date.php)-function.
573
     *
574
     * @return string the formatted result.
575
     * @throws InvalidParamException if the input value can not be evaluated as a date value.
576
     * @throws InvalidConfigException if the date format is invalid.
577
     * @see datetimeFormat
578
     */
579 152
    public function asDatetime($value, $format = null)
580
    {
581 152
        if ($format === null) {
582 144
            $format = $this->datetimeFormat;
583
        }
584 152
        return $this->formatDateTimeValue($value, $format, 'datetime');
585
    }
586
587
    /**
588
     * @var array map of short format names to IntlDateFormatter constant values.
589
     */
590
    private $_dateFormats = [
591
        'short' => 3, // IntlDateFormatter::SHORT,
592
        'medium' => 2, // IntlDateFormatter::MEDIUM,
593
        'long' => 1, // IntlDateFormatter::LONG,
594
        'full' => 0, // IntlDateFormatter::FULL,
595
    ];
596
597
    /**
598
     * @param int|string|DateTime $value the value to be formatted. The following
599
     * types of value are supported:
600
     *
601
     * - an integer representing a UNIX timestamp
602
     * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
603
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
604
     * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
605
     *
606
     * @param string $format the format used to convert the value into a date string.
607
     * @param string $type 'date', 'time', or 'datetime'.
608
     * @throws InvalidConfigException if the date format is invalid.
609
     * @return string the formatted result.
610
     */
611 176
    private function formatDateTimeValue($value, $format, $type)
612
    {
613 176
        $timeZone = $this->timeZone;
614
        // avoid time zone conversion for date-only and time-only values
615 176
        if ($type === 'date' || $type === 'time') {
616 170
            list($timestamp, $hasTimeInfo, $hasDateInfo) = $this->normalizeDatetimeValue($value, true);
617 168
            if ($type === 'date' && !$hasTimeInfo || $type === 'time' && !$hasDateInfo) {
618 12
                $timeZone = $this->defaultTimeZone;
619
            }
620
        } else {
621 152
            $timestamp = $this->normalizeDatetimeValue($value);
622
        }
623 174
        if ($timestamp === null) {
624 6
            return $this->nullDisplay;
625
        }
626
627
        // intl does not work with dates >=2038 or <=1901 on 32bit machines, fall back to PHP
628 174
        $year = $timestamp->format('Y');
629 174
        if ($this->_intlLoaded && !(PHP_INT_SIZE === 4 && ($year <= 1901 || $year >= 2038))) {
630 85
            if (strncmp($format, 'php:', 4) === 0) {
631 6
                $format = FormatConverter::convertDatePhpToIcu(substr($format, 4));
632
            }
633 85
            if (isset($this->_dateFormats[$format])) {
634 3
                if ($type === 'date') {
635 1
                    $formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], IntlDateFormatter::NONE, $timeZone, $this->calendar);
636 2
                } elseif ($type === 'time') {
637 1
                    $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, $this->_dateFormats[$format], $timeZone, $this->calendar);
638
                } else {
639 1
                    $formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], $this->_dateFormats[$format], $timeZone, $this->calendar);
640
                }
641
            } else {
642 85
                $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, IntlDateFormatter::NONE, $timeZone, $this->calendar, $format);
643
            }
644 85
            if ($formatter === null) {
645
                throw new InvalidConfigException(intl_get_error_message());
646
            }
647
            // make IntlDateFormatter work with DateTimeImmutable
648 85
            if ($timestamp instanceof \DateTimeImmutable) {
0 ignored issues
show
Bug introduced by
The class DateTimeImmutable does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
649 14
                $timestamp = new DateTime($timestamp->format(DateTime::ISO8601), $timestamp->getTimezone());
650
            }
651 85
            return $formatter->format($timestamp);
652
        } else {
653 89
            if (strncmp($format, 'php:', 4) === 0) {
654 11
                $format = substr($format, 4);
655
            } else {
656 84
                $format = FormatConverter::convertDateIcuToPhp($format, $type, $this->locale);
657
            }
658 89
            if ($timeZone != null) {
659 89
                if ($timestamp instanceof \DateTimeImmutable) {
0 ignored issues
show
Bug introduced by
The class DateTimeImmutable does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
660 13
                    $timestamp = $timestamp->setTimezone(new DateTimeZone($timeZone));
661
                } else {
662 79
                    $timestamp->setTimezone(new DateTimeZone($timeZone));
663
                }
664
            }
665 89
            return $timestamp->format($format);
666
        }
667
    }
668
669
    /**
670
     * Normalizes the given datetime value as a DateTime object that can be taken by various date/time formatting methods.
671
     *
672
     * @param int|string|DateTime $value the datetime value to be normalized. The following
673
     * types of value are supported:
674
     *
675
     * - an integer representing a UNIX timestamp
676
     * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
677
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
678
     * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
679
     *
680
     * @param bool $checkDateTimeInfo whether to also check if the date/time value has some time and date information attached.
681
     * Defaults to `false`. If `true`, the method will then return an array with the first element being the normalized
682
     * timestamp, the second a boolean indicating whether the timestamp has time information and third a boolean indicating
683
     * whether the timestamp has date information.
684
     * This parameter is available since version 2.0.1.
685
     * @return DateTime|array the normalized datetime value.
686
     * Since version 2.0.1 this may also return an array if `$checkDateTimeInfo` is true.
687
     * The first element of the array is the normalized timestamp and the second is a boolean indicating whether
688
     * the timestamp has time information or it is just a date value.
689
     * Since version 2.0.12 the array has third boolean element indicating whether the timestamp has date information
690
     * or it is just a time value.
691
     * @throws InvalidParamException if the input value can not be evaluated as a date value.
692
     */
693 181
    protected function normalizeDatetimeValue($value, $checkDateTimeInfo = false)
694
    {
695
        // checking for DateTime and DateTimeInterface is not redundant, DateTimeInterface is only in PHP>5.5
696 181
        if ($value === null || $value instanceof DateTime || $value instanceof DateTimeInterface) {
0 ignored issues
show
Bug introduced by
The class DateTimeInterface does not exist. Is this class maybe located in a folder that is not analyzed, or in a newer version of your dependencies than listed in your composer.lock/composer.json?
Loading history...
697
            // skip any processing
698 50
            return $checkDateTimeInfo ? [$value, true, true] : $value;
699
        }
700 141
        if (empty($value)) {
701 10
            $value = 0;
702
        }
703
        try {
704 141
            if (is_numeric($value)) { // process as unix timestamp, which is always in UTC
705 31
                $timestamp = new DateTime('@' . (int) $value, new DateTimeZone('UTC'));
706 31
                return $checkDateTimeInfo ? [$timestamp, true, true] : $timestamp;
707 113
            } elseif (($timestamp = DateTime::createFromFormat('Y-m-d', $value, new DateTimeZone($this->defaultTimeZone))) !== false) { // try Y-m-d format (support invalid dates like 2012-13-01)
708 10
                return $checkDateTimeInfo ? [$timestamp, false, true] : $timestamp;
709 103
            } elseif (($timestamp = DateTime::createFromFormat('Y-m-d H:i:s', $value, new DateTimeZone($this->defaultTimeZone))) !== false) { // try Y-m-d H:i:s format (support invalid dates like 2012-13-01 12:63:12)
710 19
                return $checkDateTimeInfo ? [$timestamp, true, true] : $timestamp;
711
            }
712
            // finally try to create a DateTime object with the value
713 88
            if ($checkDateTimeInfo) {
714 86
                $timestamp = new DateTime($value, new DateTimeZone($this->defaultTimeZone));
715 84
                $info = date_parse($value);
716
                return [
717 84
                    $timestamp,
718 84
                    !($info['hour'] === false && $info['minute'] === false && $info['second'] === false),
719 84
                    !($info['year'] === false && $info['month'] === false && $info['day'] === false),
720
                ];
721
            } else {
722 86
                return new DateTime($value, new DateTimeZone($this->defaultTimeZone));
723
            }
724 2
        } catch (\Exception $e) {
725 2
            throw new InvalidParamException("'$value' is not a valid date time value: " . $e->getMessage()
726 2
                . "\n" . print_r(DateTime::getLastErrors(), true), $e->getCode(), $e);
727
        }
728
    }
729
730
    /**
731
     * Formats a date, time or datetime in a float number as UNIX timestamp (seconds since 01-01-1970).
732
     * @param int|string|DateTime $value the value to be formatted. The following
733
     * types of value are supported:
734
     *
735
     * - an integer representing a UNIX timestamp
736
     * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
737
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
738
     * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
739
     *
740
     * @return string the formatted result.
741
     */
742 145
    public function asTimestamp($value)
743
    {
744 145
        if ($value === null) {
745 2
            return $this->nullDisplay;
746
        }
747 145
        $timestamp = $this->normalizeDatetimeValue($value);
748 145
        return number_format($timestamp->format('U'), 0, '.', '');
749
    }
750
751
    /**
752
     * Formats the value as the time interval between a date and now in human readable form.
753
     *
754
     * This method can be used in three different ways:
755
     *
756
     * 1. Using a timestamp that is relative to `now`.
757
     * 2. Using a timestamp that is relative to the `$referenceTime`.
758
     * 3. Using a `DateInterval` object.
759
     *
760
     * @param int|string|DateTime|DateInterval $value the value to be formatted. The following
761
     * types of value are supported:
762
     *
763
     * - an integer representing a UNIX timestamp
764
     * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
765
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
766
     * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
767
     * - a PHP DateInterval object (a positive time interval will refer to the past, a negative one to the future)
768
     *
769
     * @param int|string|DateTime $referenceTime if specified the value is used as a reference time instead of `now`
770
     * when `$value` is not a `DateInterval` object.
771
     * @return string the formatted result.
772
     * @throws InvalidParamException if the input value can not be evaluated as a date value.
773
     */
774 92
    public function asRelativeTime($value, $referenceTime = null)
775
    {
776 92
        if ($value === null) {
777 2
            return $this->nullDisplay;
778
        }
779
780 92
        if ($value instanceof DateInterval) {
781 2
            $interval = $value;
782
        } else {
783 92
            $timestamp = $this->normalizeDatetimeValue($value);
784
785 92
            if ($timestamp === false) {
786
                // $value is not a valid date/time value, so we try
787
                // to create a DateInterval with it
788
                try {
789
                    $interval = new DateInterval($value);
790
                } catch (\Exception $e) {
791
                    // invalid date/time and invalid interval
792
                    return $this->nullDisplay;
793
                }
794
            } else {
795 92
                $timeZone = new DateTimeZone($this->timeZone);
796
797 92
                if ($referenceTime === null) {
798
                    $dateNow = new DateTime('now', $timeZone);
799
                } else {
800 92
                    $dateNow = $this->normalizeDatetimeValue($referenceTime);
801 92
                    $dateNow->setTimezone($timeZone);
802
                }
803
804 92
                $dateThen = $timestamp->setTimezone($timeZone);
805
806 92
                $interval = $dateThen->diff($dateNow);
807
            }
808
        }
809
810 92
        if ($interval->invert) {
811 92
            if ($interval->y >= 1) {
812 2
                return Yii::t('yii', 'in {delta, plural, =1{a year} other{# years}}', ['delta' => $interval->y], $this->locale);
813
            }
814 92
            if ($interval->m >= 1) {
815 2
                return Yii::t('yii', 'in {delta, plural, =1{a month} other{# months}}', ['delta' => $interval->m], $this->locale);
816
            }
817 92
            if ($interval->d >= 1) {
818 2
                return Yii::t('yii', 'in {delta, plural, =1{a day} other{# days}}', ['delta' => $interval->d], $this->locale);
819
            }
820 92
            if ($interval->h >= 1) {
821 92
                return Yii::t('yii', 'in {delta, plural, =1{an hour} other{# hours}}', ['delta' => $interval->h], $this->locale);
822
            }
823 2
            if ($interval->i >= 1) {
824 2
                return Yii::t('yii', 'in {delta, plural, =1{a minute} other{# minutes}}', ['delta' => $interval->i], $this->locale);
825
            }
826 2
            if ($interval->s == 0) {
827
                return Yii::t('yii', 'just now', [], $this->locale);
828
            }
829 2
            return Yii::t('yii', 'in {delta, plural, =1{a second} other{# seconds}}', ['delta' => $interval->s], $this->locale);
830
        } else {
831 92
            if ($interval->y >= 1) {
832 2
                return Yii::t('yii', '{delta, plural, =1{a year} other{# years}} ago', ['delta' => $interval->y], $this->locale);
833
            }
834 92
            if ($interval->m >= 1) {
835 2
                return Yii::t('yii', '{delta, plural, =1{a month} other{# months}} ago', ['delta' => $interval->m], $this->locale);
836
            }
837 92
            if ($interval->d >= 1) {
838 2
                return Yii::t('yii', '{delta, plural, =1{a day} other{# days}} ago', ['delta' => $interval->d], $this->locale);
839
            }
840 92
            if ($interval->h >= 1) {
841 92
                return Yii::t('yii', '{delta, plural, =1{an hour} other{# hours}} ago', ['delta' => $interval->h], $this->locale);
842
            }
843 2
            if ($interval->i >= 1) {
844 2
                return Yii::t('yii', '{delta, plural, =1{a minute} other{# minutes}} ago', ['delta' => $interval->i], $this->locale);
845
            }
846 2
            if ($interval->s == 0) {
847 2
                return Yii::t('yii', 'just now', [], $this->locale);
848
            }
849 2
            return Yii::t('yii', '{delta, plural, =1{a second} other{# seconds}} ago', ['delta' => $interval->s], $this->locale);
850
        }
851
    }
852
853
    /**
854
     * Represents the value as duration in human readable format.
855
     *
856
     * @param DateInterval|string|int $value the value to be formatted. Acceptable formats:
857
     *  - [DateInterval object](http://php.net/manual/ru/class.dateinterval.php)
858
     *  - integer - number of seconds. For example: value `131` represents `2 minutes, 11 seconds`
859
     *  - ISO8601 duration format. For example, all of these values represent `1 day, 2 hours, 30 minutes` duration:
860
     *    `2015-01-01T13:00:00Z/2015-01-02T13:30:00Z` - between two datetime values
861
     *    `2015-01-01T13:00:00Z/P1D2H30M` - time interval after datetime value
862
     *    `P1D2H30M/2015-01-02T13:30:00Z` - time interval before datetime value
863
     *    `P1D2H30M` - simply a date interval
864
     *    `P-1D2H30M` - a negative date interval (`-1 day, 2 hours, 30 minutes`)
865
     *
866
     * @param string $implodeString will be used to concatenate duration parts. Defaults to `, `.
867
     * @param string $negativeSign will be prefixed to the formatted duration, when it is negative. Defaults to `-`.
868
     * @return string the formatted duration.
869
     * @since 2.0.7
870
     */
871 2
    public function asDuration($value, $implodeString = ', ', $negativeSign = '-')
872
    {
873 2
        if ($value === null) {
874 2
            return $this->nullDisplay;
875
        }
876
877 2
        if ($value instanceof DateInterval) {
878 2
            $isNegative = $value->invert;
879 2
            $interval = $value;
880 2
        } elseif (is_numeric($value)) {
881 2
            $isNegative = $value < 0;
882 2
            $zeroDateTime = (new DateTime())->setTimestamp(0);
883 2
            $valueDateTime = (new DateTime())->setTimestamp(abs($value));
884 2
            $interval = $valueDateTime->diff($zeroDateTime);
885 2
        } elseif (strpos($value, 'P-') === 0) {
886 2
            $interval = new DateInterval('P' . substr($value, 2));
887 2
            $isNegative = true;
888
        } else {
889 2
            $interval = new DateInterval($value);
890 2
            $isNegative = $interval->invert;
891
        }
892
893 2
        if ($interval->y > 0) {
894 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 year} other{# years}}', ['delta' => $interval->y], $this->locale);
0 ignored issues
show
Coding Style Comprehensibility introduced by
$parts was never initialized. Although not strictly required by PHP, it is generally a good practice to add $parts = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
895
        }
896 2
        if ($interval->m > 0) {
897 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 month} other{# months}}', ['delta' => $interval->m], $this->locale);
0 ignored issues
show
Bug introduced by
The variable $parts does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
898
        }
899 2
        if ($interval->d > 0) {
900 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 day} other{# days}}', ['delta' => $interval->d], $this->locale);
901
        }
902 2
        if ($interval->h > 0) {
903 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 hour} other{# hours}}', ['delta' => $interval->h], $this->locale);
904
        }
905 2
        if ($interval->i > 0) {
906 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 minute} other{# minutes}}', ['delta' => $interval->i], $this->locale);
907
        }
908 2
        if ($interval->s > 0) {
909 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 second} other{# seconds}}', ['delta' => $interval->s], $this->locale);
910
        }
911 2
        if ($interval->s === 0 && empty($parts)) {
912 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 second} other{# seconds}}', ['delta' => $interval->s], $this->locale);
913 2
            $isNegative = false;
914
        }
915
916 2
        return empty($parts) ? $this->nullDisplay : (($isNegative ? $negativeSign : '') . implode($implodeString, $parts));
917
    }
918
919
920
    // number formats
921
922
923
    /**
924
     * Formats the value as an integer number by removing any decimal digits without rounding.
925
     *
926
     * @param mixed $value the value to be formatted.
927
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
928
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
929
     * @return string the formatted result.
930
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
931
     */
932 7
    public function asInteger($value, $options = [], $textOptions = [])
933
    {
934 7
        if ($value === null) {
935 5
            return $this->nullDisplay;
936
        }
937 7
        $value = $this->normalizeNumericValue($value);
938 5
        if ($this->_intlLoaded) {
939 4
            $f = $this->createNumberFormatter(NumberFormatter::DECIMAL, null, $options, $textOptions);
940 4
            $f->setAttribute(NumberFormatter::FRACTION_DIGITS, 0);
941 4
            if (($result = $f->format($value, NumberFormatter::TYPE_INT64)) === false) {
942
                throw new InvalidParamException('Formatting integer value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
943
            }
944 4
            return $result;
945
        } else {
946 1
            return number_format((int) $value, 0, $this->decimalSeparator, $this->thousandSeparator);
947
        }
948
    }
949
950
    /**
951
     * Formats the value as a decimal number.
952
     *
953
     * Property [[decimalSeparator]] will be used to represent the decimal point. The
954
     * value is rounded automatically to the defined decimal digits.
955
     *
956
     * @param mixed $value the value to be formatted.
957
     * @param int $decimals the number of digits after the decimal point.
958
     * If not given, the number of digits depends in the input value and is determined based on
959
     * `NumberFormatter::MIN_FRACTION_DIGITS` and `NumberFormatter::MAX_FRACTION_DIGITS`, which can be configured
960
     * using [[$numberFormatterOptions]].
961
     * If the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, the default value is `2`.
962
     * If you want consistent behavior between environments where intl is available and not, you should explicitly
963
     * specify a value here.
964
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
965
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
966
     * @return string the formatted result.
967
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
968
     * @see decimalSeparator
969
     * @see thousandSeparator
970
     */
971 12
    public function asDecimal($value, $decimals = null, $options = [], $textOptions = [])
972
    {
973 12
        if ($value === null) {
974 2
            return $this->nullDisplay;
975
        }
976 12
        $value = $this->normalizeNumericValue($value);
977
978 12
        if ($this->_intlLoaded) {
979 6
            $f = $this->createNumberFormatter(NumberFormatter::DECIMAL, $decimals, $options, $textOptions);
980 6
            if (($result = $f->format($value)) === false) {
981
                throw new InvalidParamException('Formatting decimal value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
982
            }
983 6
            return $result;
984
        } else {
985 6
            if ($decimals === null) {
986 4
                $decimals = 2;
987
            }
988 6
            return number_format($value, $decimals, $this->decimalSeparator, $this->thousandSeparator);
989
        }
990
    }
991
992
993
    /**
994
     * Formats the value as a percent number with "%" sign.
995
     *
996
     * @param mixed $value the value to be formatted. It must be a factor e.g. `0.75` will result in `75%`.
997
     * @param int $decimals the number of digits after the decimal point.
998
     * If not given, the number of digits depends in the input value and is determined based on
999
     * `NumberFormatter::MIN_FRACTION_DIGITS` and `NumberFormatter::MAX_FRACTION_DIGITS`, which can be configured
1000
     * using [[$numberFormatterOptions]].
1001
     * If the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, the default value is `0`.
1002
     * If you want consistent behavior between environments where intl is available and not, you should explicitly
1003
     * specify a value here.
1004
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1005
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1006
     * @return string the formatted result.
1007
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1008
     */
1009 2
    public function asPercent($value, $decimals = null, $options = [], $textOptions = [])
1010
    {
1011 2
        if ($value === null) {
1012 2
            return $this->nullDisplay;
1013
        }
1014 2
        $value = $this->normalizeNumericValue($value);
1015
1016 2
        if ($this->_intlLoaded) {
1017 1
            $f = $this->createNumberFormatter(NumberFormatter::PERCENT, $decimals, $options, $textOptions);
1018 1
            if (($result = $f->format($value)) === false) {
1019
                throw new InvalidParamException('Formatting percent value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
1020
            }
1021 1
            return $result;
1022
        } else {
1023 1
            if ($decimals === null) {
1024 1
                $decimals = 0;
1025
            }
1026 1
            $value *= 100;
1027 1
            return number_format($value, $decimals, $this->decimalSeparator, $this->thousandSeparator) . '%';
1028
        }
1029
    }
1030
1031
    /**
1032
     * Formats the value as a scientific number.
1033
     *
1034
     * @param mixed $value the value to be formatted.
1035
     * @param int $decimals the number of digits after the decimal point.
1036
     * If not given, the number of digits depends in the input value and is determined based on
1037
     * `NumberFormatter::MIN_FRACTION_DIGITS` and `NumberFormatter::MAX_FRACTION_DIGITS`, which can be configured
1038
     * using [[$numberFormatterOptions]].
1039
     * If the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, the default value depends on your PHP configuration.
1040
     * If you want consistent behavior between environments where intl is available and not, you should explicitly
1041
     * specify a value here.
1042
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1043
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1044
     * @return string the formatted result.
1045
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1046
     */
1047 2
    public function asScientific($value, $decimals = null, $options = [], $textOptions = [])
1048
    {
1049 2
        if ($value === null) {
1050 2
            return $this->nullDisplay;
1051
        }
1052 2
        $value = $this->normalizeNumericValue($value);
1053
1054 2
        if ($this->_intlLoaded) {
1055 1
            $f = $this->createNumberFormatter(NumberFormatter::SCIENTIFIC, $decimals, $options, $textOptions);
1056 1
            if (($result = $f->format($value)) === false) {
1057
                throw new InvalidParamException('Formatting scientific number value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
1058
            }
1059 1
            return $result;
1060
        } else {
1061 1
            if ($decimals !== null) {
1062 1
                return sprintf("%.{$decimals}E", $value);
1063
            } else {
1064 1
                return sprintf('%.E', $value);
1065
            }
1066
        }
1067
    }
1068
1069
    /**
1070
     * Formats the value as a currency number.
1071
     *
1072
     * This function does not require the [PHP intl extension](http://php.net/manual/en/book.intl.php) to be installed
1073
     * to work, but it is highly recommended to install it to get good formatting results.
1074
     *
1075
     * @param mixed $value the value to be formatted.
1076
     * @param string $currency the 3-letter ISO 4217 currency code indicating the currency to use.
1077
     * If null, [[currencyCode]] will be used.
1078
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1079
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1080
     * @return string the formatted result.
1081
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1082
     * @throws InvalidConfigException if no currency is given and [[currencyCode]] is not defined.
1083
     */
1084 4
    public function asCurrency($value, $currency = null, $options = [], $textOptions = [])
1085
    {
1086 4
        if ($value === null) {
1087 2
            return $this->nullDisplay;
1088
        }
1089 4
        $value = $this->normalizeNumericValue($value);
1090
1091 4
        if ($this->_intlLoaded) {
1092 3
            $currency = $currency ?: $this->currencyCode;
1093
            // currency code must be set before fraction digits
1094
            // http://php.net/manual/en/numberformatter.formatcurrency.php#114376
1095 3
            if ($currency && !isset($textOptions[NumberFormatter::CURRENCY_CODE])) {
1096 3
                $textOptions[NumberFormatter::CURRENCY_CODE] = $currency;
1097
            }
1098 3
            $formatter = $this->createNumberFormatter(NumberFormatter::CURRENCY, null, $options, $textOptions);
1099 3
            if ($currency === null) {
1100 2
                $result = $formatter->format($value);
1101
            } else {
1102 3
                $result = $formatter->formatCurrency($value, $currency);
1103
            }
1104 3
            if ($result === false) {
1105
                throw new InvalidParamException('Formatting currency value failed: ' . $formatter->getErrorCode() . ' ' . $formatter->getErrorMessage());
1106
            }
1107 3
            return $result;
1108
        } else {
1109 1
            if ($currency === null) {
1110 1
                if ($this->currencyCode === null) {
1111
                    throw new InvalidConfigException('The default currency code for the formatter is not defined and the php intl extension is not installed which could take the default currency from the locale.');
1112
                }
1113 1
                $currency = $this->currencyCode;
1114
            }
1115 1
            return $currency . ' ' . $this->asDecimal($value, 2, $options, $textOptions);
1116
        }
1117
    }
1118
1119
    /**
1120
     * Formats the value as a number spellout.
1121
     *
1122
     * This function requires the [PHP intl extension](http://php.net/manual/en/book.intl.php) to be installed.
1123
     *
1124
     * @param mixed $value the value to be formatted
1125
     * @return string the formatted result.
1126
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1127
     * @throws InvalidConfigException when the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available.
1128
     */
1129 1
    public function asSpellout($value)
1130
    {
1131 1
        if ($value === null) {
1132 1
            return $this->nullDisplay;
1133
        }
1134 1
        $value = $this->normalizeNumericValue($value);
1135 1
        if ($this->_intlLoaded) {
1136 1
            $f = $this->createNumberFormatter(NumberFormatter::SPELLOUT);
1137 1
            if (($result = $f->format($value)) === false) {
1138
                throw new InvalidParamException('Formatting number as spellout failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
1139
            }
1140 1
            return $result;
1141
        } else {
1142
            throw new InvalidConfigException('Format as Spellout is only supported when PHP intl extension is installed.');
1143
        }
1144
    }
1145
1146
    /**
1147
     * Formats the value as a ordinal value of a number.
1148
     *
1149
     * This function requires the [PHP intl extension](http://php.net/manual/en/book.intl.php) to be installed.
1150
     *
1151
     * @param mixed $value the value to be formatted
1152
     * @return string the formatted result.
1153
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1154
     * @throws InvalidConfigException when the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available.
1155
     */
1156 2
    public function asOrdinal($value)
1157
    {
1158 2
        if ($value === null) {
1159 1
            return $this->nullDisplay;
1160
        }
1161 2
        $value = $this->normalizeNumericValue($value);
1162 2
        if ($this->_intlLoaded) {
1163 2
            $f = $this->createNumberFormatter(NumberFormatter::ORDINAL);
1164 2
            if (($result = $f->format($value)) === false) {
1165
                throw new InvalidParamException('Formatting number as ordinal failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
1166
            }
1167 2
            return $result;
1168
        } else {
1169
            throw new InvalidConfigException('Format as Ordinal is only supported when PHP intl extension is installed.');
1170
        }
1171
    }
1172
1173
    /**
1174
     * Formats the value in bytes as a size in human readable form for example `12 KB`.
1175
     *
1176
     * This is the short form of [[asSize]].
1177
     *
1178
     * If [[sizeFormatBase]] is 1024, [binary prefixes](http://en.wikipedia.org/wiki/Binary_prefix) (e.g. kibibyte/KiB, mebibyte/MiB, ...)
1179
     * are used in the formatting result.
1180
     *
1181
     * @param string|int|float $value value in bytes to be formatted.
1182
     * @param int $decimals the number of digits after the decimal point.
1183
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1184
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1185
     * @return string the formatted result.
1186
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1187
     * @see sizeFormatBase
1188
     * @see asSize
1189
     */
1190 5
    public function asShortSize($value, $decimals = null, $options = [], $textOptions = [])
1191
    {
1192 5
        if ($value === null) {
1193 2
            return $this->nullDisplay;
1194
        }
1195
1196 5
        list($params, $position) = $this->formatSizeNumber($value, $decimals, $options, $textOptions);
1197
1198 5
        if ($this->sizeFormatBase == 1024) {
1199
            switch ($position) {
1200 5
                case 0:
1201 5
                    return Yii::t('yii', '{nFormatted} B', $params, $this->locale);
1202 3
                case 1:
1203 3
                    return Yii::t('yii', '{nFormatted} KiB', $params, $this->locale);
1204 3
                case 2:
1205 3
                    return Yii::t('yii', '{nFormatted} MiB', $params, $this->locale);
1206 2
                case 3:
1207 2
                    return Yii::t('yii', '{nFormatted} GiB', $params, $this->locale);
1208 2
                case 4:
1209
                    return Yii::t('yii', '{nFormatted} TiB', $params, $this->locale);
1210
                default:
1211 2
                    return Yii::t('yii', '{nFormatted} PiB', $params, $this->locale);
1212
            }
1213
        } else {
1214
            switch ($position) {
1215 2
                case 0:
1216 2
                    return Yii::t('yii', '{nFormatted} B', $params, $this->locale);
1217 2
                case 1:
1218 2
                    return Yii::t('yii', '{nFormatted} KB', $params, $this->locale);
1219 2
                case 2:
1220 2
                    return Yii::t('yii', '{nFormatted} MB', $params, $this->locale);
1221 2
                case 3:
1222 2
                    return Yii::t('yii', '{nFormatted} GB', $params, $this->locale);
1223 2
                case 4:
1224
                    return Yii::t('yii', '{nFormatted} TB', $params, $this->locale);
1225
                default:
1226 2
                    return Yii::t('yii', '{nFormatted} PB', $params, $this->locale);
1227
            }
1228
        }
1229
    }
1230
1231
    /**
1232
     * Formats the value in bytes as a size in human readable form, for example `12 kilobytes`.
1233
     *
1234
     * If [[sizeFormatBase]] is 1024, [binary prefixes](http://en.wikipedia.org/wiki/Binary_prefix) (e.g. kibibyte/KiB, mebibyte/MiB, ...)
1235
     * are used in the formatting result.
1236
     *
1237
     * @param string|int|float $value value in bytes to be formatted.
1238
     * @param int $decimals the number of digits after the decimal point.
1239
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1240
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1241
     * @return string the formatted result.
1242
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1243
     * @see sizeFormatBase
1244
     * @see asShortSize
1245
     */
1246 6
    public function asSize($value, $decimals = null, $options = [], $textOptions = [])
1247
    {
1248 6
        if ($value === null) {
1249 2
            return $this->nullDisplay;
1250
        }
1251
1252 6
        list($params, $position) = $this->formatSizeNumber($value, $decimals, $options, $textOptions);
1253
1254 6
        if ($this->sizeFormatBase == 1024) {
1255
            switch ($position) {
1256 6
                case 0:
1257 6
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{byte} other{bytes}}', $params, $this->locale);
1258 4
                case 1:
1259 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{kibibyte} other{kibibytes}}', $params, $this->locale);
1260 4
                case 2:
1261 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{mebibyte} other{mebibytes}}', $params, $this->locale);
1262 4
                case 3:
1263 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{gibibyte} other{gibibytes}}', $params, $this->locale);
1264 4
                case 4:
1265
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{tebibyte} other{tebibytes}}', $params, $this->locale);
1266
                default:
1267 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{pebibyte} other{pebibytes}}', $params, $this->locale);
1268
            }
1269
        } else {
1270
            switch ($position) {
1271 4
                case 0:
1272 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{byte} other{bytes}}', $params, $this->locale);
1273 4
                case 1:
1274 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{kilobyte} other{kilobytes}}', $params, $this->locale);
1275 4
                case 2:
1276 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{megabyte} other{megabytes}}', $params, $this->locale);
1277 4
                case 3:
1278 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{gigabyte} other{gigabytes}}', $params, $this->locale);
1279 4
                case 4:
1280
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{terabyte} other{terabytes}}', $params, $this->locale);
1281
                default:
1282 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{petabyte} other{petabytes}}', $params, $this->locale);
1283
            }
1284
        }
1285
    }
1286
1287
1288
    /**
1289
     * Given the value in bytes formats number part of the human readable form.
1290
     *
1291
     * @param string|int|float $value value in bytes to be formatted.
1292
     * @param int $decimals the number of digits after the decimal point
1293
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1294
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1295
     * @return array [parameters for Yii::t containing formatted number, internal position of size unit]
1296
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1297
     */
1298 9
    private function formatSizeNumber($value, $decimals, $options, $textOptions)
1299
    {
1300 9
        $value = $this->normalizeNumericValue($value);
1301
1302 9
        $position = 0;
1303
        do {
1304 9
            if (abs($value) < $this->sizeFormatBase) {
1305 9
                break;
1306
            }
1307 7
            $value /= $this->sizeFormatBase;
1308 7
            $position++;
1309 7
        } while ($position < 5);
1310
1311
        // no decimals for bytes
1312 9
        if ($position === 0) {
1313 9
            $decimals = 0;
1314 7
        } elseif ($decimals !== null) {
1315 6
            $value = round($value, $decimals);
1316
        }
1317
        // disable grouping for edge cases like 1023 to get 1023 B instead of 1,023 B
1318 9
        $oldThousandSeparator = $this->thousandSeparator;
1319 9
        $this->thousandSeparator = '';
1320 9
        if ($this->_intlLoaded) {
1321 5
            $options[NumberFormatter::GROUPING_USED] = false;
1322
        }
1323
        // format the size value
1324
        $params = [
1325
            // this is the unformatted number used for the plural rule
1326
            // abs() to make sure the plural rules work correctly on negative numbers, intl does not cover this
1327
            // http://english.stackexchange.com/questions/9735/is-1-singular-or-plural
1328 9
            'n' => abs($value),
1329
            // this is the formatted number used for display
1330 9
            'nFormatted' => $this->asDecimal($value, $decimals, $options, $textOptions),
1331
        ];
1332 9
        $this->thousandSeparator = $oldThousandSeparator;
1333
1334 9
        return [$params, $position];
1335
    }
1336
1337
    /**
1338
     * Normalizes a numeric input value
1339
     *
1340
     * - everything [empty](http://php.net/manual/en/function.empty.php) will result in `0`
1341
     * - a [numeric](http://php.net/manual/en/function.is-numeric.php) string will be casted to float
1342
     * - everything else will be returned if it is [numeric](http://php.net/manual/en/function.is-numeric.php),
1343
     *   otherwise an exception is thrown.
1344
     *
1345
     * @param mixed $value the input value
1346
     * @return float|int the normalized number value
1347
     * @throws InvalidParamException if the input value is not numeric.
1348
     */
1349 29
    protected function normalizeNumericValue($value)
1350
    {
1351 29
        if (empty($value)) {
1352 16
            return 0;
1353
        }
1354 29
        if (is_string($value) && is_numeric($value)) {
1355 16
            $value = (float) $value;
1356
        }
1357 29
        if (!is_numeric($value)) {
1358 2
            throw new InvalidParamException("'$value' is not a numeric value.");
1359
        }
1360 27
        return $value;
1361
    }
1362
1363
    /**
1364
     * Creates a number formatter based on the given type and format.
1365
     *
1366
     * You may override this method to create a number formatter based on patterns.
1367
     *
1368
     * @param int $style the type of the number formatter.
1369
     * Values: NumberFormatter::DECIMAL, ::CURRENCY, ::PERCENT, ::SCIENTIFIC, ::SPELLOUT, ::ORDINAL
1370
     * ::DURATION, ::PATTERN_RULEBASED, ::DEFAULT_STYLE, ::IGNORE
1371
     * @param int $decimals the number of digits after the decimal point.
1372
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1373
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1374
     * @return NumberFormatter the created formatter instance
1375
     */
1376 18
    protected function createNumberFormatter($style, $decimals = null, $options = [], $textOptions = [])
1377
    {
1378 18
        $formatter = new NumberFormatter($this->locale, $style);
1379
1380
        // set text attributes
1381 18
        foreach ($this->numberFormatterTextOptions as $name => $attribute) {
1382 1
            $formatter->setTextAttribute($name, $attribute);
1383
        }
1384 18
        foreach ($textOptions as $name => $attribute) {
1385 3
            $formatter->setTextAttribute($name, $attribute);
1386
        }
1387
1388
        // set attributes
1389 18
        foreach ($this->numberFormatterOptions as $name => $value) {
1390 9
            $formatter->setAttribute($name, $value);
1391
        }
1392 18
        foreach ($options as $name => $value) {
1393 7
            $formatter->setAttribute($name, $value);
1394
        }
1395 18
        if ($decimals !== null) {
1396 6
            $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $decimals);
1397 6
            $formatter->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $decimals);
1398
        }
1399
1400
        // set symbols
1401 18
        if ($this->decimalSeparator !== null) {
1402 4
            $formatter->setSymbol(NumberFormatter::DECIMAL_SEPARATOR_SYMBOL, $this->decimalSeparator);
1403
        }
1404 18
        if ($this->thousandSeparator !== null) {
1405 7
            $formatter->setSymbol(NumberFormatter::GROUPING_SEPARATOR_SYMBOL, $this->thousandSeparator);
1406 7
            $formatter->setSymbol(NumberFormatter::MONETARY_GROUPING_SEPARATOR_SYMBOL, $this->thousandSeparator);
1407
        }
1408 18
        foreach ($this->numberFormatterSymbols as $name => $symbol) {
1409 2
            $formatter->setSymbol($name, $symbol);
1410
        }
1411
1412 18
        return $formatter;
1413
    }
1414
}
1415