GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — master ( 97c43c...6b8cf1 )
by Robert
18:45
created

Formatter   D

Complexity

Total Complexity 196

Size/Duplication

Total Lines 1341
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 94.21%

Importance

Changes 0
Metric Value
wmc 196
lcom 1
cbo 7
dl 0
loc 1341
rs 4.4102
c 0
b 0
f 0
ccs 439
cts 466
cp 0.9421

31 Methods

Rating   Name   Duplication   Size   Complexity  
C init() 0 24 8
A format() 0 20 4
A asRaw() 0 7 2
A asText() 0 7 2
A asNtext() 0 7 2
A asParagraphs() 0 7 2
A asHtml() 0 7 2
A asEmail() 0 7 2
A asImage() 0 7 2
A asUrl() 0 12 3
A asBoolean() 0 8 3
A asDate() 0 7 2
A asTime() 0 7 2
A asDatetime() 0 7 2
C formatDateTimeValue() 0 57 21
D normalizeDatetimeValue() 0 36 18
A asTimestamp() 0 8 2
C asRelativeTime() 0 78 19
F asDuration() 0 47 15
A asInteger() 0 17 4
B asDecimal() 0 20 5
B asPercent() 0 21 5
B asScientific() 0 21 5
D asCurrency() 0 34 10
A asSpellout() 0 16 4
A asOrdinal() 0 16 4
C asShortSize() 0 40 13
C asSize() 0 40 13
B formatSizeNumber() 0 38 6
B normalizeNumericValue() 0 13 5
F createNumberFormatter() 0 38 9

How to fix   Complexity   

Complex Class

Complex classes like Formatter often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Formatter, and based on these observations, apply Extract Interface, too.

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 DateInterval;
11
use DateTime;
12
use DateTimeInterface;
13
use DateTimeZone;
14
use IntlDateFormatter;
15
use NumberFormatter;
16
use Yii;
17
use yii\base\Component;
18
use yii\base\InvalidConfigException;
19
use yii\base\InvalidParamException;
20
use yii\helpers\FormatConverter;
21
use yii\helpers\HtmlPurifier;
22
use yii\helpers\Html;
23
24
/**
25
 * Formatter provides a set of commonly used data formatting methods.
26
 *
27
 * The formatting methods provided by Formatter are all named in the form of `asXyz()`.
28
 * The behavior of some of them may be configured via the properties of Formatter. For example,
29
 * by configuring [[dateFormat]], one may control how [[asDate()]] formats the value into a date string.
30
 *
31
 * Formatter is configured as an application component in [[\yii\base\Application]] by default.
32
 * You can access that instance via `Yii::$app->formatter`.
33
 *
34
 * The Formatter class is designed to format values according to a [[locale]]. For this feature to work
35
 * the [PHP intl extension](http://php.net/manual/en/book.intl.php) has to be installed.
36
 * Most of the methods however work also if the PHP intl extension is not installed by providing
37
 * a fallback implementation. Without intl month and day names are in English only.
38
 * Note that even if the intl extension is installed, formatting date and time values for years >=2038 or <=1901
39
 * on 32bit systems will fall back to the PHP implementation because intl uses a 32bit UNIX timestamp internally.
40
 * On a 64bit system the intl formatter is used in all cases if installed.
41
 *
42
 * @author Qiang Xue <[email protected]>
43
 * @author Enrica Ruedin <[email protected]>
44
 * @author Carsten Brandt <[email protected]>
45
 * @since 2.0
46
 */
47
class Formatter extends Component
48
{
49
    /**
50
     * @var string the text to be displayed when formatting a `null` value.
51
     * Defaults to `'<span class="not-set">(not set)</span>'`, where `(not set)`
52
     * will be translated according to [[locale]].
53
     */
54
    public $nullDisplay;
55
    /**
56
     * @var array the text to be displayed when formatting a boolean value. The first element corresponds
57
     * to the text displayed for `false`, the second element for `true`.
58
     * Defaults to `['No', 'Yes']`, where `Yes` and `No`
59
     * will be translated according to [[locale]].
60
     */
61
    public $booleanFormat;
62
    /**
63
     * @var string the locale ID that is used to localize the date and number formatting.
64
     * For number and date formatting this is only effective when the
65
     * [PHP intl extension](http://php.net/manual/en/book.intl.php) is installed.
66
     * If not set, [[\yii\base\Application::language]] will be used.
67
     */
68
    public $locale;
69
    /**
70
     * @var string the time zone to use for formatting time and date values.
71
     *
72
     * 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)
73
     * e.g. `UTC`, `Europe/Berlin` or `America/Chicago`.
74
     * Refer to the [php manual](http://www.php.net/manual/en/timezones.php) for available time zones.
75
     * If this property is not set, [[\yii\base\Application::timeZone]] will be used.
76
     *
77
     * 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.
78
     * If you store your data in a different time zone in the database, you have to adjust [[defaultTimeZone]] accordingly.
79
     */
80
    public $timeZone;
81
    /**
82
     * @var string the time zone that is assumed for input values if they do not include a time zone explicitly.
83
     *
84
     * The value must be a valid time zone identifier, e.g. `UTC`, `Europe/Berlin` or `America/Chicago`.
85
     * Please refer to the [php manual](http://www.php.net/manual/en/timezones.php) for available time zones.
86
     *
87
     * It defaults to `UTC` so you only have to adjust this value if you store datetime values in another time zone in your database.
88
     *
89
     * @since 2.0.1
90
     */
91
    public $defaultTimeZone = 'UTC';
92
    /**
93
     * @var string the default format string to be used to format a [[asDate()|date]].
94
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
95
     *
96
     * 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).
97
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
98
     * PHP [date()](http://php.net/manual/en/function.date.php)-function.
99
     *
100
     * For example:
101
     *
102
     * ```php
103
     * 'MM/dd/yyyy' // date in ICU format
104
     * 'php:m/d/Y' // the same date in PHP format
105
     * ```
106
     */
107
    public $dateFormat = 'medium';
108
    /**
109
     * @var string the default format string to be used to format a [[asTime()|time]].
110
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
111
     *
112
     * 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).
113
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
114
     * PHP [date()](http://php.net/manual/en/function.date.php)-function.
115
     *
116
     * For example:
117
     *
118
     * ```php
119
     * 'HH:mm:ss' // time in ICU format
120
     * 'php:H:i:s' // the same time in PHP format
121
     * ```
122
     */
123
    public $timeFormat = 'medium';
124
    /**
125
     * @var string the default format string to be used to format a [[asDatetime()|date and time]].
126
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
127
     *
128
     * 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).
129
     *
130
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
131
     * PHP [date()](http://php.net/manual/en/function.date.php)-function.
132
     *
133
     * For example:
134
     *
135
     * ```php
136
     * 'MM/dd/yyyy HH:mm:ss' // date and time in ICU format
137
     * 'php:m/d/Y H:i:s' // the same date and time in PHP format
138
     * ```
139
     */
140
    public $datetimeFormat = 'medium';
141
    /**
142
     * @var \IntlCalendar|int|null the calendar to be used for date formatting. The value of this property will be directly
143
     * passed to the [constructor of the `IntlDateFormatter` class](http://php.net/manual/en/intldateformatter.create.php).
144
     *
145
     * Defaults to `null`, which means the Gregorian calendar will be used. You may also explicitly pass the constant
146
     * `\IntlDateFormatter::GREGORIAN` for Gregorian calendar.
147
     *
148
     * To use an alternative calendar like for example the [Jalali calendar](https://en.wikipedia.org/wiki/Jalali_calendar),
149
     * set this property to `\IntlDateFormatter::TRADITIONAL`.
150
     * The calendar must then be specified in the [[locale]], for example for the persian calendar the configuration for the formatter would be:
151
     *
152
     * ```php
153
     * 'formatter' => [
154
     *     'locale' => 'fa_IR@calendar=persian',
155
     *     'calendar' => \IntlDateFormatter::TRADITIONAL,
156
     * ],
157
     * ```
158
     *
159
     * Available calendar names can be found in the [ICU manual](http://userguide.icu-project.org/datetime/calendar).
160
     *
161
     * Since PHP 5.5 you may also use an instance of the [[\IntlCalendar]] class.
162
     * Check the [PHP manual](http://php.net/manual/en/intldateformatter.create.php) for more details.
163
     *
164
     * If the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, setting this property will have no effect.
165
     *
166
     * @see http://php.net/manual/en/intldateformatter.create.php
167
     * @see http://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants.calendartypes
168
     * @see http://php.net/manual/en/class.intlcalendar.php
169
     * @since 2.0.7
170
     */
171
    public $calendar;
172
    /**
173
     * @var string the character displayed as the decimal point when formatting a number.
174
     * If not set, the decimal separator corresponding to [[locale]] will be used.
175
     * If [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, the default value is '.'.
176
     */
177
    public $decimalSeparator;
178
    /**
179
     * @var string the character displayed as the thousands separator (also called grouping separator) character when formatting a number.
180
     * If not set, the thousand separator corresponding to [[locale]] will be used.
181
     * If [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, the default value is ','.
182
     */
183
    public $thousandSeparator;
184
    /**
185
     * @var array a list of name value pairs that are passed to the
186
     * intl [Numberformatter::setAttribute()](http://php.net/manual/en/numberformatter.setattribute.php) method of all
187
     * the number formatter objects created by [[createNumberFormatter()]].
188
     * This property takes only effect if the [PHP intl extension](http://php.net/manual/en/book.intl.php) is installed.
189
     *
190
     * Please refer to the [PHP manual](http://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants.unumberformatattribute)
191
     * for the possible options.
192
     *
193
     * For example to adjust the maximum and minimum value of fraction digits you can configure this property like the following:
194
     *
195
     * ```php
196
     * [
197
     *     NumberFormatter::MIN_FRACTION_DIGITS => 0,
198
     *     NumberFormatter::MAX_FRACTION_DIGITS => 2,
199
     * ]
200
     * ```
201
     */
202
    public $numberFormatterOptions = [];
203
    /**
204
     * @var array a list of name value pairs that are passed to the
205
     * intl [Numberformatter::setTextAttribute()](http://php.net/manual/en/numberformatter.settextattribute.php) method of all
206
     * the number formatter objects created by [[createNumberFormatter()]].
207
     * This property takes only effect if the [PHP intl extension](http://php.net/manual/en/book.intl.php) is installed.
208
     *
209
     * Please refer to the [PHP manual](http://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants.unumberformattextattribute)
210
     * for the possible options.
211
     *
212
     * For example to change the minus sign for negative numbers you can configure this property like the following:
213
     *
214
     * ```php
215
     * [
216
     *     NumberFormatter::NEGATIVE_PREFIX => 'MINUS',
217
     * ]
218
     * ```
219
     */
220
    public $numberFormatterTextOptions = [];
221
    /**
222
     * @var array a list of name value pairs that are passed to the
223
     * intl [Numberformatter::setSymbol()](http://php.net/manual/en/numberformatter.setsymbol.php) method of all
224
     * the number formatter objects created by [[createNumberFormatter()]].
225
     * This property takes only effect if the [PHP intl extension](http://php.net/manual/en/book.intl.php) is installed.
226
     *
227
     * Please refer to the [PHP manual](http://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants.unumberformatsymbol)
228
     * for the possible options.
229
     *
230
     * For example to choose a custom currency symbol, e.g. [U+20BD](http://unicode-table.com/en/20BD/) instead of `руб.` for Russian Ruble:
231
     *
232
     * ```php
233
     * [
234
     *     NumberFormatter::CURRENCY_SYMBOL => '₽',
235
     * ]
236
     * ```
237
     *
238
     * @since 2.0.4
239
     */
240
    public $numberFormatterSymbols = [];
241
    /**
242
     * @var string the 3-letter ISO 4217 currency code indicating the default currency to use for [[asCurrency]].
243
     * If not set, the currency code corresponding to [[locale]] will be used.
244
     * Note that in this case the [[locale]] has to be specified with a country code, e.g. `en-US` otherwise it
245
     * is not possible to determine the default currency.
246
     */
247
    public $currencyCode;
248
    /**
249
     * @var int the base at which a kilobyte is calculated (1000 or 1024 bytes per kilobyte), used by [[asSize]] and [[asShortSize]].
250
     * Defaults to 1024.
251
     */
252
    public $sizeFormatBase = 1024;
253
254
    /**
255
     * @var bool whether the [PHP intl extension](http://php.net/manual/en/book.intl.php) is loaded.
256
     */
257
    private $_intlLoaded = false;
258
259
260
    /**
261
     * @inheritdoc
262
     */
263 242
    public function init()
264
    {
265 242
        if ($this->timeZone === null) {
266 242
            $this->timeZone = Yii::$app->timeZone;
267 242
        }
268 242
        if ($this->locale === null) {
269 25
            $this->locale = Yii::$app->language;
270 25
        }
271 242
        if ($this->booleanFormat === null) {
272 242
            $this->booleanFormat = [Yii::t('yii', 'No', [], $this->locale), Yii::t('yii', 'Yes', [], $this->locale)];
273 242
        }
274 242
        if ($this->nullDisplay === null) {
275 242
            $this->nullDisplay = '<span class="not-set">' . Yii::t('yii', '(not set)', [], $this->locale) . '</span>';
276 242
        }
277 242
        $this->_intlLoaded = extension_loaded('intl');
278 242
        if (!$this->_intlLoaded) {
279 116
            if ($this->decimalSeparator === null) {
280 116
                $this->decimalSeparator = '.';
281 116
            }
282 116
            if ($this->thousandSeparator === null) {
283 116
                $this->thousandSeparator = ',';
284 116
            }
285 116
        }
286 242
    }
287
288
    /**
289
     * Formats the value based on the given format type.
290
     * This method will call one of the "as" methods available in this class to do the formatting.
291
     * For type "xyz", the method "asXyz" will be used. For example, if the format is "html",
292
     * then [[asHtml()]] will be used. Format names are case insensitive.
293
     * @param mixed $value the value to be formatted.
294
     * @param string|array $format the format of the value, e.g., "html", "text". To specify additional
295
     * parameters of the formatting method, you may use an array. The first element of the array
296
     * specifies the format name, while the rest of the elements will be used as the parameters to the formatting
297
     * method. For example, a format of `['date', 'Y-m-d']` will cause the invocation of `asDate($value, 'Y-m-d')`.
298
     * @return string the formatting result.
299
     * @throws InvalidParamException if the format type is not supported by this class.
300
     */
301 10
    public function format($value, $format)
302
    {
303 10
        if (is_array($format)) {
304 7
            if (!isset($format[0])) {
305
                throw new InvalidParamException('The $format array must contain at least one element.');
306
            }
307 7
            $f = $format[0];
308 7
            $format[0] = $value;
309 7
            $params = $format;
310 7
            $format = $f;
311 7
        } else {
312 5
            $params = [$value];
313
        }
314 10
        $method = 'as' . $format;
315 10
        if ($this->hasMethod($method)) {
316 10
            return call_user_func_array([$this, $method], $params);
317
        } else {
318 2
            throw new InvalidParamException("Unknown format type: $format");
319
        }
320
    }
321
322
323
    // simple formats
324
325
326
    /**
327
     * Formats the value as is without any formatting.
328
     * This method simply returns back the parameter without any format.
329
     * The only exception is a `null` value which will be formatted using [[nullDisplay]].
330
     * @param mixed $value the value to be formatted.
331
     * @return string the formatted result.
332
     */
333 1
    public function asRaw($value)
334
    {
335 1
        if ($value === null) {
336 1
            return $this->nullDisplay;
337
        }
338 1
        return $value;
339
    }
340
341
    /**
342
     * Formats the value as an HTML-encoded plain text.
343
     * @param string $value the value to be formatted.
344
     * @return string the formatted result.
345
     */
346 4
    public function asText($value)
347
    {
348 4
        if ($value === null) {
349 2
            return $this->nullDisplay;
350
        }
351 4
        return Html::encode($value);
352
    }
353
354
    /**
355
     * Formats the value as an HTML-encoded plain text with newlines converted into breaks.
356
     * @param string $value the value to be formatted.
357
     * @return string the formatted result.
358
     */
359 1
    public function asNtext($value)
360
    {
361 1
        if ($value === null) {
362 1
            return $this->nullDisplay;
363
        }
364 1
        return nl2br(Html::encode($value));
365
    }
366
367
    /**
368
     * Formats the value as HTML-encoded text paragraphs.
369
     * Each text paragraph is enclosed within a `<p>` tag.
370
     * One or multiple consecutive empty lines divide two paragraphs.
371
     * @param string $value the value to be formatted.
372
     * @return string the formatted result.
373
     */
374 1
    public function asParagraphs($value)
375
    {
376 1
        if ($value === null) {
377 1
            return $this->nullDisplay;
378
        }
379 1
        return str_replace('<p></p>', '', '<p>' . preg_replace('/\R{2,}/u', "</p>\n<p>", Html::encode($value)) . '</p>');
380
    }
381
382
    /**
383
     * Formats the value as HTML text.
384
     * The value will be purified using [[HtmlPurifier]] to avoid XSS attacks.
385
     * Use [[asRaw()]] if you do not want any purification of the value.
386
     * @param string $value the value to be formatted.
387
     * @param array|null $config the configuration for the HTMLPurifier class.
388
     * @return string the formatted result.
389
     */
390
    public function asHtml($value, $config = null)
391
    {
392
        if ($value === null) {
393
            return $this->nullDisplay;
394
        }
395
        return HtmlPurifier::process($value, $config);
396
    }
397
398
    /**
399
     * Formats the value as a mailto link.
400
     * @param string $value the value to be formatted.
401
     * @param array $options the tag options in terms of name-value pairs. See [[Html::mailto()]].
402
     * @return string the formatted result.
403
     */
404 1
    public function asEmail($value, $options = [])
405
    {
406 1
        if ($value === null) {
407 1
            return $this->nullDisplay;
408
        }
409 1
        return Html::mailto(Html::encode($value), $value, $options);
410
    }
411
412
    /**
413
     * Formats the value as an image tag.
414
     * @param mixed $value the value to be formatted.
415
     * @param array $options the tag options in terms of name-value pairs. See [[Html::img()]].
416
     * @return string the formatted result.
417
     */
418 1
    public function asImage($value, $options = [])
419
    {
420 1
        if ($value === null) {
421 1
            return $this->nullDisplay;
422
        }
423 1
        return Html::img($value, $options);
424
    }
425
426
    /**
427
     * Formats the value as a hyperlink.
428
     * @param mixed $value the value to be formatted.
429
     * @param array $options the tag options in terms of name-value pairs. See [[Html::a()]].
430
     * @return string the formatted result.
431
     */
432 1
    public function asUrl($value, $options = [])
433
    {
434 1
        if ($value === null) {
435 1
            return $this->nullDisplay;
436
        }
437 1
        $url = $value;
438 1
        if (strpos($url, '://') === false) {
439 1
            $url = 'http://' . $url;
440 1
        }
441
442 1
        return Html::a(Html::encode($value), $url, $options);
443
    }
444
445
    /**
446
     * Formats the value as a boolean.
447
     * @param mixed $value the value to be formatted.
448
     * @return string the formatted result.
449
     * @see booleanFormat
450
     */
451 1
    public function asBoolean($value)
452
    {
453 1
        if ($value === null) {
454 1
            return $this->nullDisplay;
455
        }
456
457 1
        return $value ? $this->booleanFormat[1] : $this->booleanFormat[0];
458
    }
459
460
461
    // date and time formats
462
463
464
    /**
465
     * Formats the value as a date.
466
     * @param int|string|DateTime $value the value to be formatted. The following
467
     * types of value are supported:
468
     *
469
     * - an integer representing a UNIX timestamp
470
     * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
471
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
472
     * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
473
     *
474
     * @param string $format the format used to convert the value into a date string.
475
     * If null, [[dateFormat]] will be used.
476
     *
477
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
478
     * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime).
479
     *
480
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
481
     * PHP [date()](http://php.net/manual/en/function.date.php)-function.
482
     *
483
     * @return string the formatted result.
484
     * @throws InvalidParamException if the input value can not be evaluated as a date value.
485
     * @throws InvalidConfigException if the date format is invalid.
486
     * @see dateFormat
487
     */
488 167
    public function asDate($value, $format = null)
489
    {
490 167
        if ($format === null) {
491 146
            $format = $this->dateFormat;
492 146
        }
493 167
        return $this->formatDateTimeValue($value, $format, 'date');
494
    }
495
496
    /**
497
     * Formats the value as a time.
498
     * @param int|string|DateTime $value the value to be formatted. The following
499
     * types of value are supported:
500
     *
501
     * - an integer representing a UNIX timestamp
502
     * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
503
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
504
     * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
505
     *
506
     * @param string $format the format used to convert the value into a date string.
507
     * If null, [[timeFormat]] will be used.
508
     *
509
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
510
     * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime).
511
     *
512
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
513
     * PHP [date()](http://php.net/manual/en/function.date.php)-function.
514
     *
515
     * @return string the formatted result.
516
     * @throws InvalidParamException if the input value can not be evaluated as a date value.
517
     * @throws InvalidConfigException if the date format is invalid.
518
     * @see timeFormat
519
     */
520 148
    public function asTime($value, $format = null)
521
    {
522 148
        if ($format === null) {
523 144
            $format = $this->timeFormat;
524 144
        }
525 148
        return $this->formatDateTimeValue($value, $format, 'time');
526
    }
527
528
    /**
529
     * Formats the value as a datetime.
530
     * @param int|string|DateTime $value the value to be formatted. The following
531
     * types of value are supported:
532
     *
533
     * - an integer representing a UNIX timestamp
534
     * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
535
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
536
     * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
537
     *
538
     * @param string $format the format used to convert the value into a date string.
539
     * If null, [[dateFormat]] will be used.
540
     *
541
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
542
     * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime).
543
     *
544
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
545
     * PHP [date()](http://php.net/manual/en/function.date.php)-function.
546
     *
547
     * @return string the formatted result.
548
     * @throws InvalidParamException if the input value can not be evaluated as a date value.
549
     * @throws InvalidConfigException if the date format is invalid.
550
     * @see datetimeFormat
551
     */
552 152
    public function asDatetime($value, $format = null)
553
    {
554 152
        if ($format === null) {
555 144
            $format = $this->datetimeFormat;
556 144
        }
557 152
        return $this->formatDateTimeValue($value, $format, 'datetime');
558
    }
559
560
    /**
561
     * @var array map of short format names to IntlDateFormatter constant values.
562
     */
563
    private $_dateFormats = [
564
        'short'  => 3, // IntlDateFormatter::SHORT,
565
        'medium' => 2, // IntlDateFormatter::MEDIUM,
566
        'long'   => 1, // IntlDateFormatter::LONG,
567
        'full'   => 0, // IntlDateFormatter::FULL,
568
    ];
569
570
    /**
571
     * @param int|string|DateTime $value the value to be formatted. The following
572
     * types of value are supported:
573
     *
574
     * - an integer representing a UNIX timestamp
575
     * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
576
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
577
     * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
578
     *
579
     * @param string $format the format used to convert the value into a date string.
580
     * @param string $type 'date', 'time', or 'datetime'.
581
     * @throws InvalidConfigException if the date format is invalid.
582
     * @return string the formatted result.
583
     */
584 175
    private function formatDateTimeValue($value, $format, $type)
585
    {
586 175
        $timeZone = $this->timeZone;
587
        // avoid time zone conversion for date-only and time-only values
588 175
        if ($type === 'date' || $type === 'time') {
589 169
            list($timestamp, $hasTimeInfo, $hasDateInfo) = $this->normalizeDatetimeValue($value, true);
590 167
            if ($type === 'date' && !$hasTimeInfo || $type === 'time' && !$hasDateInfo) {
591 11
                $timeZone = $this->defaultTimeZone;
592 11
            }
593 167
        } else {
594 152
            $timestamp = $this->normalizeDatetimeValue($value);
595
        }
596 173
        if ($timestamp === null) {
597 6
            return $this->nullDisplay;
598
        }
599
600
        // intl does not work with dates >=2038 or <=1901 on 32bit machines, fall back to PHP
601 173
        $year = $timestamp->format('Y');
602 173
        if ($this->_intlLoaded && !(PHP_INT_SIZE === 4 && ($year <= 1901 || $year >= 2038))) {
603 84
            if (strncmp($format, 'php:', 4) === 0) {
604 5
                $format = FormatConverter::convertDatePhpToIcu(substr($format, 4));
605 5
            }
606 84
            if (isset($this->_dateFormats[$format])) {
607 3
                if ($type === 'date') {
608 1
                    $formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], IntlDateFormatter::NONE, $timeZone, $this->calendar);
609 3
                } elseif ($type === 'time') {
610 1
                    $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, $this->_dateFormats[$format], $timeZone, $this->calendar);
611 1
                } else {
612 1
                    $formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], $this->_dateFormats[$format], $timeZone, $this->calendar);
613
                }
614 3
            } else {
615 84
                $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, IntlDateFormatter::NONE, $timeZone, $this->calendar, $format);
616
            }
617 84
            if ($formatter === null) {
618
                throw new InvalidConfigException(intl_get_error_message());
619
            }
620
            // make IntlDateFormatter work with DateTimeImmutable
621 84
            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...
622 14
                $timestamp = new DateTime($timestamp->format(DateTime::ISO8601), $timestamp->getTimezone());
623 14
            }
624 84
            return $formatter->format($timestamp);
625
        } else {
626 89
            if (strncmp($format, 'php:', 4) === 0) {
627 11
                $format = substr($format, 4);
628 11
            } else {
629 84
                $format = FormatConverter::convertDateIcuToPhp($format, $type, $this->locale);
630
            }
631 89
            if ($timeZone != null) {
632 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...
633 13
                    $timestamp = $timestamp->setTimezone(new DateTimeZone($timeZone));
634 13
                } else {
635 79
                    $timestamp->setTimezone(new DateTimeZone($timeZone));
636
                }
637 89
            }
638 89
            return $timestamp->format($format);
639
        }
640
    }
641
642
    /**
643
     * Normalizes the given datetime value as a DateTime object that can be taken by various date/time formatting methods.
644
     *
645
     * @param int|string|DateTime $value the datetime value to be normalized. The following
646
     * types of value are supported:
647
     *
648
     * - an integer representing a UNIX timestamp
649
     * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
650
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
651
     * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
652
     *
653
     * @param bool $checkDateTimeInfo whether to also check if the date/time value has some time and date information attached.
654
     * Defaults to `false`. If `true`, the method will then return an array with the first element being the normalized
655
     * timestamp, the second a boolean indicating whether the timestamp has time information and third a boolean indicating
656
     * whether the timestamp has date information.
657
     * This parameter is available since version 2.0.1.
658
     * @return DateTime|array the normalized datetime value.
659
     * Since version 2.0.1 this may also return an array if `$checkTimeInfo` is true.
660
     * The first element of the array is the normalized timestamp and the second is a boolean indicating whether
661
     * the timestamp has time information or it is just a date value.
662
     * Since version 2.0.12 the array has third boolean element indicating whether the timestamp has date information 
663
     * or it is just a time value.
664
     * @throws InvalidParamException if the input value can not be evaluated as a date value.
665
     */
666 180
    protected function normalizeDatetimeValue($value, $checkDateTimeInfo = false)
667
    {
668
        // checking for DateTime and DateTimeInterface is not redundant, DateTimeInterface is only in PHP>5.5
669 180
        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...
670
            // skip any processing
671 50
            return $checkDateTimeInfo ? [$value, true, true] : $value;
672
        }
673 140
        if (empty($value)) {
674 10
            $value = 0;
675 10
        }
676
        try {
677 140
            if (is_numeric($value)) { // process as unix timestamp, which is always in UTC
678 31
                $timestamp = new DateTime('@' . (int)$value, new DateTimeZone('UTC'));
679 31
                return $checkDateTimeInfo ? [$timestamp, true, true] : $timestamp;
680 112
            } 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)
681 9
                return $checkDateTimeInfo ? [$timestamp, false, true] : $timestamp;
682 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)
683 19
                return $checkDateTimeInfo ? [$timestamp, true, true] : $timestamp;
684
            }
685
            // finally try to create a DateTime object with the value
686 88
            if ($checkDateTimeInfo) {
687 86
                $timestamp = new DateTime($value, new DateTimeZone($this->defaultTimeZone));
688 84
                $info = date_parse($value);
689
                return [
690 84
                    $timestamp,
691 84
                    !($info['hour'] === false && $info['minute'] === false && $info['second'] === false),
692 84
                    !($info['year'] === false && $info['month'] === false && $info['day'] === false)
693 84
                ];
694
            } else {
695 86
                return new DateTime($value, new DateTimeZone($this->defaultTimeZone));
696
            }
697 2
        } catch (\Exception $e) {
698 2
            throw new InvalidParamException("'$value' is not a valid date time value: " . $e->getMessage()
699 2
                . "\n" . print_r(DateTime::getLastErrors(), true), $e->getCode(), $e);
700
        }
701
    }
702
703
    /**
704
     * Formats a date, time or datetime in a float number as UNIX timestamp (seconds since 01-01-1970).
705
     * @param int|string|DateTime $value the value to be formatted. The following
706
     * types of value are supported:
707
     *
708
     * - an integer representing a UNIX timestamp
709
     * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
710
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
711
     * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
712
     *
713
     * @return string the formatted result.
714
     */
715 145
    public function asTimestamp($value)
716
    {
717 145
        if ($value === null) {
718 2
            return $this->nullDisplay;
719
        }
720 145
        $timestamp = $this->normalizeDatetimeValue($value);
721 145
        return number_format($timestamp->format('U'), 0, '.', '');
722
    }
723
724
    /**
725
     * Formats the value as the time interval between a date and now in human readable form.
726
     *
727
     * This method can be used in three different ways:
728
     *
729
     * 1. Using a timestamp that is relative to `now`.
730
     * 2. Using a timestamp that is relative to the `$referenceTime`.
731
     * 3. Using a `DateInterval` object.
732
     *
733
     * @param int|string|DateTime|DateInterval $value the value to be formatted. The following
734
     * types of value are supported:
735
     *
736
     * - an integer representing a UNIX timestamp
737
     * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
738
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
739
     * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
740
     * - a PHP DateInterval object (a positive time interval will refer to the past, a negative one to the future)
741
     *
742
     * @param int|string|DateTime $referenceTime if specified the value is used as a reference time instead of `now`
743
     * when `$value` is not a `DateInterval` object.
744
     * @return string the formatted result.
745
     * @throws InvalidParamException if the input value can not be evaluated as a date value.
746
     */
747 92
    public function asRelativeTime($value, $referenceTime = null)
748
    {
749 92
        if ($value === null) {
750 2
            return $this->nullDisplay;
751
        }
752
753 92
        if ($value instanceof DateInterval) {
754 2
            $interval = $value;
755 2
        } else {
756 92
            $timestamp = $this->normalizeDatetimeValue($value);
757
758 92
            if ($timestamp === false) {
759
                // $value is not a valid date/time value, so we try
760
                // to create a DateInterval with it
761
                try {
762
                    $interval = new DateInterval($value);
763
                } catch (\Exception $e) {
764
                    // invalid date/time and invalid interval
765
                    return $this->nullDisplay;
766
                }
767
            } else {
768 92
                $timeZone = new DateTimeZone($this->timeZone);
769
770 92
                if ($referenceTime === null) {
771
                    $dateNow = new DateTime('now', $timeZone);
772
                } else {
773 92
                    $dateNow = $this->normalizeDatetimeValue($referenceTime);
774 92
                    $dateNow->setTimezone($timeZone);
775
                }
776
777 92
                $dateThen = $timestamp->setTimezone($timeZone);
778
779 92
                $interval = $dateThen->diff($dateNow);
780
            }
781
        }
782
783 92
        if ($interval->invert) {
784 92
            if ($interval->y >= 1) {
785 2
                return Yii::t('yii', 'in {delta, plural, =1{a year} other{# years}}', ['delta' => $interval->y], $this->locale);
786
            }
787 92
            if ($interval->m >= 1) {
788 2
                return Yii::t('yii', 'in {delta, plural, =1{a month} other{# months}}', ['delta' => $interval->m], $this->locale);
789
            }
790 92
            if ($interval->d >= 1) {
791 2
                return Yii::t('yii', 'in {delta, plural, =1{a day} other{# days}}', ['delta' => $interval->d], $this->locale);
792
            }
793 92
            if ($interval->h >= 1) {
794 92
                return Yii::t('yii', 'in {delta, plural, =1{an hour} other{# hours}}', ['delta' => $interval->h], $this->locale);
795
            }
796 2
            if ($interval->i >= 1) {
797 2
                return Yii::t('yii', 'in {delta, plural, =1{a minute} other{# minutes}}', ['delta' => $interval->i], $this->locale);
798
            }
799 2
            if ($interval->s == 0) {
800
                return Yii::t('yii', 'just now', [], $this->locale);
801
            }
802 2
            return Yii::t('yii', 'in {delta, plural, =1{a second} other{# seconds}}', ['delta' => $interval->s], $this->locale);
803
        } else {
804 92
            if ($interval->y >= 1) {
805 2
                return Yii::t('yii', '{delta, plural, =1{a year} other{# years}} ago', ['delta' => $interval->y], $this->locale);
806
            }
807 92
            if ($interval->m >= 1) {
808 2
                return Yii::t('yii', '{delta, plural, =1{a month} other{# months}} ago', ['delta' => $interval->m], $this->locale);
809
            }
810 92
            if ($interval->d >= 1) {
811 2
                return Yii::t('yii', '{delta, plural, =1{a day} other{# days}} ago', ['delta' => $interval->d], $this->locale);
812
            }
813 92
            if ($interval->h >= 1) {
814 92
                return Yii::t('yii', '{delta, plural, =1{an hour} other{# hours}} ago', ['delta' => $interval->h], $this->locale);
815
            }
816 2
            if ($interval->i >= 1) {
817 2
                return Yii::t('yii', '{delta, plural, =1{a minute} other{# minutes}} ago', ['delta' => $interval->i], $this->locale);
818
            }
819 2
            if ($interval->s == 0) {
820 2
                return Yii::t('yii', 'just now', [], $this->locale);
821
            }
822 2
            return Yii::t('yii', '{delta, plural, =1{a second} other{# seconds}} ago', ['delta' => $interval->s], $this->locale);
823
        }
824
    }
825
826
    /**
827
     * Represents the value as duration in human readable format.
828
     *
829
     * @param DateInterval|string|int $value the value to be formatted. Acceptable formats:
830
     *  - [DateInterval object](http://php.net/manual/ru/class.dateinterval.php)
831
     *  - integer - number of seconds. For example: value `131` represents `2 minutes, 11 seconds`
832
     *  - ISO8601 duration format. For example, all of these values represent `1 day, 2 hours, 30 minutes` duration:
833
     *    `2015-01-01T13:00:00Z/2015-01-02T13:30:00Z` - between two datetime values
834
     *    `2015-01-01T13:00:00Z/P1D2H30M` - time interval after datetime value
835
     *    `P1D2H30M/2015-01-02T13:30:00Z` - time interval before datetime value
836
     *    `P1D2H30M` - simply a date interval
837
     *    `P-1D2H30M` - a negative date interval (`-1 day, 2 hours, 30 minutes`)
838
     *
839
     * @param string $implodeString will be used to concatenate duration parts. Defaults to `, `.
840
     * @param string $negativeSign will be prefixed to the formatted duration, when it is negative. Defaults to `-`.
841
     * @return string the formatted duration.
842
     * @since 2.0.7
843
     */
844 2
    public function asDuration($value, $implodeString = ', ', $negativeSign = '-')
845
    {
846 2
        if ($value === null) {
847 2
            return $this->nullDisplay;
848
        }
849
850 2
        if ($value instanceof DateInterval) {
851 2
            $isNegative = $value->invert;
852 2
            $interval = $value;
853 2
        } elseif (is_numeric($value)) {
854 2
            $isNegative = $value < 0;
855 2
            $zeroDateTime = (new DateTime())->setTimestamp(0);
856 2
            $valueDateTime = (new DateTime())->setTimestamp(abs($value));
857 2
            $interval = $valueDateTime->diff($zeroDateTime);
858 2
        } elseif (strpos($value, 'P-') === 0) {
859 2
            $interval = new DateInterval('P'.substr($value, 2));
860 2
            $isNegative = true;
861 2
        } else {
862 2
            $interval = new DateInterval($value);
863 2
            $isNegative = $interval->invert;
864
        }
865
866 2
        if ($interval->y > 0) {
867 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...
868 2
        }
869 2
        if ($interval->m > 0) {
870 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...
871 2
        }
872 2
        if ($interval->d > 0) {
873 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 day} other{# days}}', ['delta' => $interval->d], $this->locale);
874 2
        }
875 2
        if ($interval->h > 0) {
876 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 hour} other{# hours}}', ['delta' => $interval->h], $this->locale);
877 2
        }
878 2
        if ($interval->i > 0) {
879 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 minute} other{# minutes}}', ['delta' => $interval->i], $this->locale);
880 2
        }
881 2
        if ($interval->s > 0) {
882 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 second} other{# seconds}}', ['delta' => $interval->s], $this->locale);
883 2
        }
884 2
        if ($interval->s === 0 && empty($parts)) {
885 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 second} other{# seconds}}', ['delta' => $interval->s], $this->locale);
886 2
            $isNegative = false;
887 2
        }
888
889 2
        return empty($parts) ? $this->nullDisplay : (($isNegative ? $negativeSign : '') . implode($implodeString, $parts));
890
    }
891
892
893
    // number formats
894
895
896
    /**
897
     * Formats the value as an integer number by removing any decimal digits without rounding.
898
     *
899
     * @param mixed $value the value to be formatted.
900
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
901
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
902
     * @return string the formatted result.
903
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
904
     */
905 7
    public function asInteger($value, $options = [], $textOptions = [])
906
    {
907 7
        if ($value === null) {
908 5
            return $this->nullDisplay;
909
        }
910 7
        $value = $this->normalizeNumericValue($value);
911 5
        if ($this->_intlLoaded) {
912 4
            $f = $this->createNumberFormatter(NumberFormatter::DECIMAL, null, $options, $textOptions);
913 4
            $f->setAttribute(NumberFormatter::FRACTION_DIGITS, 0);
914 4
            if (($result = $f->format($value, NumberFormatter::TYPE_INT64)) === false) {
915
                throw new InvalidParamException('Formatting integer value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
916
            }
917 4
            return $result;
918
        } else {
919 1
            return number_format((int) $value, 0, $this->decimalSeparator, $this->thousandSeparator);
920
        }
921
    }
922
923
    /**
924
     * Formats the value as a decimal number.
925
     *
926
     * Property [[decimalSeparator]] will be used to represent the decimal point. The
927
     * value is rounded automatically to the defined decimal digits.
928
     *
929
     * @param mixed $value the value to be formatted.
930
     * @param int $decimals the number of digits after the decimal point.
931
     * If not given, the number of digits depends in the input value and is determined based on
932
     * `NumberFormatter::MIN_FRACTION_DIGITS` and `NumberFormatter::MAX_FRACTION_DIGITS`, which can be configured
933
     * using [[$numberFormatterOptions]].
934
     * If the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, the default value is `2`.
935
     * If you want consistent behavior between environments where intl is available and not, you should explicitly
936
     * specify a value here.
937
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
938
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
939
     * @return string the formatted result.
940
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
941
     * @see decimalSeparator
942
     * @see thousandSeparator
943
     */
944 12
    public function asDecimal($value, $decimals = null, $options = [], $textOptions = [])
945
    {
946 12
        if ($value === null) {
947 2
            return $this->nullDisplay;
948
        }
949 12
        $value = $this->normalizeNumericValue($value);
950
951 12
        if ($this->_intlLoaded) {
952 6
            $f = $this->createNumberFormatter(NumberFormatter::DECIMAL, $decimals, $options, $textOptions);
953 6
            if (($result = $f->format($value)) === false) {
954
                throw new InvalidParamException('Formatting decimal value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
955
            }
956 6
            return $result;
957
        } else {
958 6
            if ($decimals === null) {
959 4
                $decimals = 2;
960 4
            }
961 6
            return number_format($value, $decimals, $this->decimalSeparator, $this->thousandSeparator);
962
        }
963
    }
964
965
966
    /**
967
     * Formats the value as a percent number with "%" sign.
968
     *
969
     * @param mixed $value the value to be formatted. It must be a factor e.g. `0.75` will result in `75%`.
970
     * @param int $decimals the number of digits after the decimal point.
971
     * If not given, the number of digits depends in the input value and is determined based on
972
     * `NumberFormatter::MIN_FRACTION_DIGITS` and `NumberFormatter::MAX_FRACTION_DIGITS`, which can be configured
973
     * using [[$numberFormatterOptions]].
974
     * If the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, the default value is `0`.
975
     * If you want consistent behavior between environments where intl is available and not, you should explicitly
976
     * specify a value here.
977
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
978
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
979
     * @return string the formatted result.
980
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
981
     */
982 2
    public function asPercent($value, $decimals = null, $options = [], $textOptions = [])
983
    {
984 2
        if ($value === null) {
985 2
            return $this->nullDisplay;
986
        }
987 2
        $value = $this->normalizeNumericValue($value);
988
989 2
        if ($this->_intlLoaded) {
990 1
            $f = $this->createNumberFormatter(NumberFormatter::PERCENT, $decimals, $options, $textOptions);
991 1
            if (($result = $f->format($value)) === false) {
992
                throw new InvalidParamException('Formatting percent value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
993
            }
994 1
            return $result;
995
        } else {
996 1
            if ($decimals === null) {
997 1
                $decimals = 0;
998 1
            }
999 1
            $value *= 100;
1000 1
            return number_format($value, $decimals, $this->decimalSeparator, $this->thousandSeparator) . '%';
1001
        }
1002
    }
1003
1004
    /**
1005
     * Formats the value as a scientific number.
1006
     *
1007
     * @param mixed $value the value to be formatted.
1008
     * @param int $decimals the number of digits after the decimal point.
1009
     * If not given, the number of digits depends in the input value and is determined based on
1010
     * `NumberFormatter::MIN_FRACTION_DIGITS` and `NumberFormatter::MAX_FRACTION_DIGITS`, which can be configured
1011
     * using [[$numberFormatterOptions]].
1012
     * If the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, the default value depends on your PHP configuration.
1013
     * If you want consistent behavior between environments where intl is available and not, you should explicitly
1014
     * specify a value here.
1015
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1016
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1017
     * @return string the formatted result.
1018
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1019
     */
1020 2
    public function asScientific($value, $decimals = null, $options = [], $textOptions = [])
1021
    {
1022 2
        if ($value === null) {
1023 2
            return $this->nullDisplay;
1024
        }
1025 2
        $value = $this->normalizeNumericValue($value);
1026
1027 2
        if ($this->_intlLoaded) {
1028 1
            $f = $this->createNumberFormatter(NumberFormatter::SCIENTIFIC, $decimals, $options, $textOptions);
1029 1
            if (($result = $f->format($value)) === false) {
1030
                throw new InvalidParamException('Formatting scientific number value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
1031
            }
1032 1
            return $result;
1033
        } else {
1034 1
            if ($decimals !== null) {
1035 1
                return sprintf("%.{$decimals}E", $value);
1036
            } else {
1037 1
                return sprintf('%.E', $value);
1038
            }
1039
        }
1040
    }
1041
1042
    /**
1043
     * Formats the value as a currency number.
1044
     *
1045
     * This function does not require the [PHP intl extension](http://php.net/manual/en/book.intl.php) to be installed
1046
     * to work, but it is highly recommended to install it to get good formatting results.
1047
     *
1048
     * @param mixed $value the value to be formatted.
1049
     * @param string $currency the 3-letter ISO 4217 currency code indicating the currency to use.
1050
     * If null, [[currencyCode]] will be used.
1051
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1052
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1053
     * @return string the formatted result.
1054
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1055
     * @throws InvalidConfigException if no currency is given and [[currencyCode]] is not defined.
1056
     */
1057 4
    public function asCurrency($value, $currency = null, $options = [], $textOptions = [])
1058
    {
1059 4
        if ($value === null) {
1060 2
            return $this->nullDisplay;
1061
        }
1062 4
        $value = $this->normalizeNumericValue($value);
1063
1064 4
        if ($this->_intlLoaded) {
1065 3
            $currency = $currency ?: $this->currencyCode;
1066
            // currency code must be set before fraction digits
1067
            // http://php.net/manual/en/numberformatter.formatcurrency.php#114376
1068 3
            if ($currency && !isset($textOptions[NumberFormatter::CURRENCY_CODE])) {
1069 3
                $textOptions[NumberFormatter::CURRENCY_CODE] = $currency;
1070 3
            }
1071 3
            $formatter = $this->createNumberFormatter(NumberFormatter::CURRENCY, null, $options, $textOptions);
1072 3
            if ($currency === null) {
1073 2
                $result = $formatter->format($value);
1074 2
            } else {
1075 3
                $result = $formatter->formatCurrency($value, $currency);
1076
            }
1077 3
            if ($result === false) {
1078
                throw new InvalidParamException('Formatting currency value failed: ' . $formatter->getErrorCode() . ' ' . $formatter->getErrorMessage());
1079
            }
1080 3
            return $result;
1081
        } else {
1082 1
            if ($currency === null) {
1083 1
                if ($this->currencyCode === null) {
1084
                    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.');
1085
                }
1086 1
                $currency = $this->currencyCode;
1087 1
            }
1088 1
            return $currency . ' ' . $this->asDecimal($value, 2, $options, $textOptions);
1089
        }
1090
    }
1091
1092
    /**
1093
     * Formats the value as a number spellout.
1094
     *
1095
     * This function requires the [PHP intl extension](http://php.net/manual/en/book.intl.php) to be installed.
1096
     *
1097
     * @param mixed $value the value to be formatted
1098
     * @return string the formatted result.
1099
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1100
     * @throws InvalidConfigException when the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available.
1101
     */
1102 1
    public function asSpellout($value)
1103
    {
1104 1
        if ($value === null) {
1105 1
            return $this->nullDisplay;
1106
        }
1107 1
        $value = $this->normalizeNumericValue($value);
1108 1
        if ($this->_intlLoaded) {
1109 1
            $f = $this->createNumberFormatter(NumberFormatter::SPELLOUT);
1110 1
            if (($result = $f->format($value)) === false) {
1111
                throw new InvalidParamException('Formatting number as spellout failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
1112
            }
1113 1
            return $result;
1114
        } else {
1115
            throw new InvalidConfigException('Format as Spellout is only supported when PHP intl extension is installed.');
1116
        }
1117
    }
1118
1119
    /**
1120
     * Formats the value as a ordinal value of a number.
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 asOrdinal($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::ORDINAL);
1137 1
            if (($result = $f->format($value)) === false) {
1138
                throw new InvalidParamException('Formatting number as ordinal failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
1139
            }
1140 1
            return $result;
1141
        } else {
1142
            throw new InvalidConfigException('Format as Ordinal is only supported when PHP intl extension is installed.');
1143
        }
1144
    }
1145
1146
    /**
1147
     * Formats the value in bytes as a size in human readable form for example `12 KB`.
1148
     *
1149
     * This is the short form of [[asSize]].
1150
     *
1151
     * If [[sizeFormatBase]] is 1024, [binary prefixes](http://en.wikipedia.org/wiki/Binary_prefix) (e.g. kibibyte/KiB, mebibyte/MiB, ...)
1152
     * are used in the formatting result.
1153
     *
1154
     * @param string|int|float $value value in bytes to be formatted.
1155
     * @param int $decimals the number of digits after the decimal point.
1156
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1157
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1158
     * @return string the formatted result.
1159
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1160
     * @see sizeFormatBase
1161
     * @see asSize
1162
     */
1163 5
    public function asShortSize($value, $decimals = null, $options = [], $textOptions = [])
1164
    {
1165 5
        if ($value === null) {
1166 2
            return $this->nullDisplay;
1167
        }
1168
1169 5
        list($params, $position) = $this->formatSizeNumber($value, $decimals, $options, $textOptions);
1170
1171 5
        if ($this->sizeFormatBase == 1024) {
1172
            switch ($position) {
1173 5
                case 0:
1174 5
                    return Yii::t('yii', '{nFormatted} B', $params, $this->locale);
1175 3
                case 1:
1176 3
                    return Yii::t('yii', '{nFormatted} KiB', $params, $this->locale);
1177 3
                case 2:
1178 3
                    return Yii::t('yii', '{nFormatted} MiB', $params, $this->locale);
1179 2
                case 3:
1180 2
                    return Yii::t('yii', '{nFormatted} GiB', $params, $this->locale);
1181 2
                case 4:
1182
                    return Yii::t('yii', '{nFormatted} TiB', $params, $this->locale);
1183 2
                default:
1184 2
                    return Yii::t('yii', '{nFormatted} PiB', $params, $this->locale);
1185 2
            }
1186
        } else {
1187
            switch ($position) {
1188 2
                case 0:
1189 2
                    return Yii::t('yii', '{nFormatted} B', $params, $this->locale);
1190 2
                case 1:
1191 2
                    return Yii::t('yii', '{nFormatted} KB', $params, $this->locale);
1192 2
                case 2:
1193 2
                    return Yii::t('yii', '{nFormatted} MB', $params, $this->locale);
1194 2
                case 3:
1195 2
                    return Yii::t('yii', '{nFormatted} GB', $params, $this->locale);
1196 2
                case 4:
1197
                    return Yii::t('yii', '{nFormatted} TB', $params, $this->locale);
1198 2
                default:
1199 2
                    return Yii::t('yii', '{nFormatted} PB', $params, $this->locale);
1200 2
            }
1201
        }
1202
    }
1203
1204
    /**
1205
     * Formats the value in bytes as a size in human readable form, for example `12 kilobytes`.
1206
     *
1207
     * If [[sizeFormatBase]] is 1024, [binary prefixes](http://en.wikipedia.org/wiki/Binary_prefix) (e.g. kibibyte/KiB, mebibyte/MiB, ...)
1208
     * are used in the formatting result.
1209
     *
1210
     * @param string|int|float $value value in bytes to be formatted.
1211
     * @param int $decimals the number of digits after the decimal point.
1212
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1213
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1214
     * @return string the formatted result.
1215
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1216
     * @see sizeFormatBase
1217
     * @see asShortSize
1218
     */
1219 6
    public function asSize($value, $decimals = null, $options = [], $textOptions = [])
1220
    {
1221 6
        if ($value === null) {
1222 2
            return $this->nullDisplay;
1223
        }
1224
1225 6
        list($params, $position) = $this->formatSizeNumber($value, $decimals, $options, $textOptions);
1226
1227 6
        if ($this->sizeFormatBase == 1024) {
1228
            switch ($position) {
1229 6
                case 0:
1230 6
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{byte} other{bytes}}', $params, $this->locale);
1231 4
                case 1:
1232 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{kibibyte} other{kibibytes}}', $params, $this->locale);
1233 4
                case 2:
1234 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{mebibyte} other{mebibytes}}', $params, $this->locale);
1235 4
                case 3:
1236 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{gibibyte} other{gibibytes}}', $params, $this->locale);
1237 4
                case 4:
1238
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{tebibyte} other{tebibytes}}', $params, $this->locale);
1239 4
                default:
1240 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{pebibyte} other{pebibytes}}', $params, $this->locale);
1241 4
            }
1242
        } else {
1243
            switch ($position) {
1244 4
                case 0:
1245 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{byte} other{bytes}}', $params, $this->locale);
1246 4
                case 1:
1247 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{kilobyte} other{kilobytes}}', $params, $this->locale);
1248 4
                case 2:
1249 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{megabyte} other{megabytes}}', $params, $this->locale);
1250 4
                case 3:
1251 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{gigabyte} other{gigabytes}}', $params, $this->locale);
1252 4
                case 4:
1253
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{terabyte} other{terabytes}}', $params, $this->locale);
1254 4
                default:
1255 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{petabyte} other{petabytes}}', $params, $this->locale);
1256 4
            }
1257
        }
1258
    }
1259
1260
1261
    /**
1262
     * Given the value in bytes formats number part of the human readable form.
1263
     *
1264
     * @param string|int|float $value value in bytes to be formatted.
1265
     * @param int $decimals the number of digits after the decimal point
1266
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1267
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1268
     * @return array [parameters for Yii::t containing formatted number, internal position of size unit]
1269
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1270
     */
1271 9
    private function formatSizeNumber($value, $decimals, $options, $textOptions)
1272
    {
1273 9
        $value = $this->normalizeNumericValue($value);
1274
1275 9
        $position = 0;
1276
        do {
1277 9
            if (abs($value) < $this->sizeFormatBase) {
1278 9
                break;
1279
            }
1280 7
            $value /= $this->sizeFormatBase;
1281 7
            $position++;
1282 7
        } while ($position < 5);
1283
1284
        // no decimals for bytes
1285 9
        if ($position === 0) {
1286 9
            $decimals = 0;
1287 9
        } elseif ($decimals !== null) {
1288 6
            $value = round($value, $decimals);
1289 6
        }
1290
        // disable grouping for edge cases like 1023 to get 1023 B instead of 1,023 B
1291 9
        $oldThousandSeparator = $this->thousandSeparator;
1292 9
        $this->thousandSeparator = '';
1293 9
        if ($this->_intlLoaded) {
1294 5
            $options[NumberFormatter::GROUPING_USED] = false;
1295 5
        }
1296
        // format the size value
1297
        $params = [
1298
            // this is the unformatted number used for the plural rule
1299
            // abs() to make sure the plural rules work correctly on negative numbers, intl does not cover this
1300
            // http://english.stackexchange.com/questions/9735/is-1-singular-or-plural
1301 9
            'n' => abs($value),
1302
            // this is the formatted number used for display
1303 9
            'nFormatted' => $this->asDecimal($value, $decimals, $options, $textOptions),
1304 9
        ];
1305 9
        $this->thousandSeparator = $oldThousandSeparator;
1306
1307 9
        return [$params, $position];
1308
    }
1309
1310
    /**
1311
     * Normalizes a numeric input value
1312
     *
1313
     * - everything [empty](http://php.net/manual/en/function.empty.php) will result in `0`
1314
     * - a [numeric](http://php.net/manual/en/function.is-numeric.php) string will be casted to float
1315
     * - everything else will be returned if it is [numeric](http://php.net/manual/en/function.is-numeric.php),
1316
     *   otherwise an exception is thrown.
1317
     *
1318
     * @param mixed $value the input value
1319
     * @return float|int the normalized number value
1320
     * @throws InvalidParamException if the input value is not numeric.
1321
     */
1322 28
    protected function normalizeNumericValue($value)
1323
    {
1324 28
        if (empty($value)) {
1325 16
            return 0;
1326
        }
1327 28
        if (is_string($value) && is_numeric($value)) {
1328 15
            $value = (float) $value;
1329 15
        }
1330 28
        if (!is_numeric($value)) {
1331 2
            throw new InvalidParamException("'$value' is not a numeric value.");
1332
        }
1333 26
        return $value;
1334
    }
1335
1336
    /**
1337
     * Creates a number formatter based on the given type and format.
1338
     *
1339
     * You may override this method to create a number formatter based on patterns.
1340
     *
1341
     * @param int $style the type of the number formatter.
1342
     * Values: NumberFormatter::DECIMAL, ::CURRENCY, ::PERCENT, ::SCIENTIFIC, ::SPELLOUT, ::ORDINAL
1343
     * ::DURATION, ::PATTERN_RULEBASED, ::DEFAULT_STYLE, ::IGNORE
1344
     * @param int $decimals the number of digits after the decimal point.
1345
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1346
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1347
     * @return NumberFormatter the created formatter instance
1348
     */
1349 17
    protected function createNumberFormatter($style, $decimals = null, $options = [], $textOptions = [])
1350
    {
1351 17
        $formatter = new NumberFormatter($this->locale, $style);
1352
1353
        // set text attributes
1354 17
        foreach ($this->numberFormatterTextOptions as $name => $attribute) {
1355 1
            $formatter->setTextAttribute($name, $attribute);
1356 17
        }
1357 17
        foreach ($textOptions as $name => $attribute) {
1358 3
            $formatter->setTextAttribute($name, $attribute);
1359 17
        }
1360
1361
        // set attributes
1362 17
        foreach ($this->numberFormatterOptions as $name => $value) {
1363 9
            $formatter->setAttribute($name, $value);
1364 17
        }
1365 17
        foreach ($options as $name => $value) {
1366 7
            $formatter->setAttribute($name, $value);
1367 17
        }
1368 17
        if ($decimals !== null) {
1369 6
            $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $decimals);
1370 6
            $formatter->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $decimals);
1371 6
        }
1372
1373
        // set symbols
1374 17
        if ($this->decimalSeparator !== null) {
1375 4
            $formatter->setSymbol(NumberFormatter::DECIMAL_SEPARATOR_SYMBOL, $this->decimalSeparator);
1376 4
        }
1377 17
        if ($this->thousandSeparator !== null) {
1378 7
            $formatter->setSymbol(NumberFormatter::GROUPING_SEPARATOR_SYMBOL, $this->thousandSeparator);
1379 7
            $formatter->setSymbol(NumberFormatter::MONETARY_GROUPING_SEPARATOR_SYMBOL, $this->thousandSeparator);
1380 7
        }
1381 17
        foreach ($this->numberFormatterSymbols as $name => $symbol) {
1382 2
            $formatter->setSymbol($name, $symbol);
1383 17
        }
1384
1385 17
        return $formatter;
1386
    }
1387
}
1388