Completed
Push — master ( 7fda48...54278f )
by Carsten
55:20 queued 15:02
created

Formatter::asSize()   C

Complexity

Conditions 13
Paths 13

Size

Total Lines 40
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 13.0687

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 40
rs 5.1234
ccs 25
cts 27
cp 0.9259
cc 13
eloc 32
nc 13
nop 4
crap 13.0687

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 237
    public function init()
264
    {
265 237
        if ($this->timeZone === null) {
266 237
            $this->timeZone = Yii::$app->timeZone;
267 237
        }
268 237
        if ($this->locale === null) {
269 25
            $this->locale = Yii::$app->language;
270 25
        }
271 237
        if ($this->booleanFormat === null) {
272 237
            $this->booleanFormat = [Yii::t('yii', 'No', [], $this->locale), Yii::t('yii', 'Yes', [], $this->locale)];
273 237
        }
274 237
        if ($this->nullDisplay === null) {
275 237
            $this->nullDisplay = '<span class="not-set">' . Yii::t('yii', '(not set)', [], $this->locale) . '</span>';
276 237
        }
277 237
        $this->_intlLoaded = extension_loaded('intl');
278 237
        if (!$this->_intlLoaded) {
279 115
            if ($this->decimalSeparator === null) {
280 115
                $this->decimalSeparator = '.';
281 115
            }
282 115
            if ($this->thousandSeparator === null) {
283 115
                $this->thousandSeparator = ',';
284 115
            }
285 115
        }
286 237
    }
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 163
    public function asDate($value, $format = null)
489
    {
490 163
        if ($format === null) {
491 145
            $format = $this->dateFormat;
492 145
        }
493 163
        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 145
    public function asTime($value, $format = null)
521
    {
522 145
        if ($format === null) {
523 143
            $format = $this->timeFormat;
524 143
        }
525 145
        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 148
    public function asDatetime($value, $format = null)
553
    {
554 148
        if ($format === null) {
555 143
            $format = $this->datetimeFormat;
556 143
        }
557 148
        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 170
    private function formatDateTimeValue($value, $format, $type)
585
    {
586 170
        $timeZone = $this->timeZone;
587
        // avoid time zone conversion for date-only and time-only values
588 170
        if ($type === 'date' || $type === 'time') {
589 163
            list($timestamp, $hasTimeInfo, $hasDateInfo) = $this->normalizeDatetimeValue($value, true);
590 161
            if ($type === 'date' && !$hasTimeInfo || $type === 'time' && !$hasDateInfo) {
591 8
                $timeZone = $this->defaultTimeZone;
592 8
            }
593 161
        } else {
594 150
            $timestamp = $this->normalizeDatetimeValue($value);
595
        }
596 168
        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 168
        $year = $timestamp->format('Y');
602 168
        if ($this->_intlLoaded && !(PHP_INT_SIZE === 4 && ($year <= 1901 || $year >= 2038))) {
603 80
            if (strncmp($format, 'php:', 4) === 0) {
604 5
                $format = FormatConverter::convertDatePhpToIcu(substr($format, 4));
605 5
            }
606 80
            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 80
                $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, IntlDateFormatter::NONE, $timeZone, $this->calendar, $format);
616
            }
617 80
            if ($formatter === null) {
618
                throw new InvalidConfigException(intl_get_error_message());
619
            }
620
            // make IntlDateFormatter work with DateTimeImmutable
621 80
            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 80
            return $formatter->format($timestamp);
625
        } else {
626 88
            if (strncmp($format, 'php:', 4) === 0) {
627 11
                $format = substr($format, 4);
628 11
            } else {
629 83
                $format = FormatConverter::convertDateIcuToPhp($format, $type, $this->locale);
630
            }
631 88
            if ($timeZone != null) {
632 88
                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 78
                    $timestamp->setTimezone(new DateTimeZone($timeZone));
636
                }
637 88
            }
638 88
            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 175
     * or it is just a time value.
664
     * @throws InvalidParamException if the input value can not be evaluated as a date value.
665
     */
666 175
    protected function normalizeDatetimeValue($value, $checkDateTimeInfo = false)
667
    {
668 50
        // checking for DateTime and DateTimeInterface is not redundant, DateTimeInterface is only in PHP>5.5
669
        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 135
            // skip any processing
671 10
            return $checkDateTimeInfo ? [$value, true, true] : $value;
672 10
        }
673
        if (empty($value)) {
674 135
            $value = 0;
675 30
        }
676 30
        try {
677 108
            if (is_numeric($value)) { // process as unix timestamp, which is always in UTC
678 8
                $timestamp = new DateTime('@' . (int)$value, new DateTimeZone('UTC'));
679 100
                return $checkDateTimeInfo ? [$timestamp, true, true] : $timestamp;
680 18
            } 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
                return $checkDateTimeInfo ? [$timestamp, false, true] : $timestamp;
682
            } 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 85
                return $checkDateTimeInfo ? [$timestamp, true, true] : $timestamp;
684 83
            }
685 81
            // finally try to create a DateTime object with the value
686 81
            if ($checkDateTimeInfo) {
687
                $timestamp = new DateTime($value, new DateTimeZone($this->defaultTimeZone));
688 83
                $info = date_parse($value);
689
                return [
690 2
                    $timestamp,
691 2
                    !($info['hour'] === false && $info['minute'] === false && $info['second'] === false),
692 2
                    !($info['year'] === false && $info['month'] === false && $info['day'] === false)
693
                ];
694
            } else {
695
                return new DateTime($value, new DateTimeZone($this->defaultTimeZone));
696
            }
697
        } catch (\Exception $e) {
698
            throw new InvalidParamException("'$value' is not a valid date time value: " . $e->getMessage()
699
                . "\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 144
     * - 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 144
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
711 2
     * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
712
     *
713 144
     * @return string the formatted result.
714 144
     */
715
    public function asTimestamp($value)
716
    {
717
        if ($value === null) {
718
            return $this->nullDisplay;
719
        }
720
        $timestamp = $this->normalizeDatetimeValue($value);
721
        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 92
     * - a PHP DateInterval object (a positive time interval will refer to the past, a negative one to the future)
741
     *
742 92
     * @param int|string|DateTime $referenceTime if specified the value is used as a reference time instead of `now`
743 2
     * 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 92
     */
747 2
    public function asRelativeTime($value, $referenceTime = null)
748 2
    {
749 92
        if ($value === null) {
750
            return $this->nullDisplay;
751 92
        }
752
753
        if ($value instanceof DateInterval) {
754
            $interval = $value;
755
        } else {
756
            $timestamp = $this->normalizeDatetimeValue($value);
757
758
            if ($timestamp === false) {
759
                // $value is not a valid date/time value, so we try
760
                // to create a DateInterval with it
761 92
                try {
762
                    $interval = new DateInterval($value);
763 92
                } catch (\Exception $e) {
764
                    // invalid date/time and invalid interval
765
                    return $this->nullDisplay;
766 92
                }
767 92
            } else {
768
                $timeZone = new DateTimeZone($this->timeZone);
769
770 92
                if ($referenceTime === null) {
771
                    $dateNow = new DateTime('now', $timeZone);
772 92
                } else {
773
                    $dateNow = $this->normalizeDatetimeValue($referenceTime);
774
                    $dateNow->setTimezone($timeZone);
775
                }
776 92
777 92
                $dateThen = $timestamp->setTimezone($timeZone);
778 2
779
                $interval = $dateThen->diff($dateNow);
780 92
            }
781 2
        }
782
783 92
        if ($interval->invert) {
784 2
            if ($interval->y >= 1) {
785
                return Yii::t('yii', 'in {delta, plural, =1{a year} other{# years}}', ['delta' => $interval->y], $this->locale);
786 92
            }
787 92
            if ($interval->m >= 1) {
788
                return Yii::t('yii', 'in {delta, plural, =1{a month} other{# months}}', ['delta' => $interval->m], $this->locale);
789 2
            }
790 2
            if ($interval->d >= 1) {
791
                return Yii::t('yii', 'in {delta, plural, =1{a day} other{# days}}', ['delta' => $interval->d], $this->locale);
792 2
            }
793
            if ($interval->h >= 1) {
794
                return Yii::t('yii', 'in {delta, plural, =1{an hour} other{# hours}}', ['delta' => $interval->h], $this->locale);
795 2
            }
796
            if ($interval->i >= 1) {
797 92
                return Yii::t('yii', 'in {delta, plural, =1{a minute} other{# minutes}}', ['delta' => $interval->i], $this->locale);
798 2
            }
799
            if ($interval->s == 0) {
800 92
                return Yii::t('yii', 'just now', [], $this->locale);
801 2
            }
802
            return Yii::t('yii', 'in {delta, plural, =1{a second} other{# seconds}}', ['delta' => $interval->s], $this->locale);
803 92
        } else {
804 2
            if ($interval->y >= 1) {
805
                return Yii::t('yii', '{delta, plural, =1{a year} other{# years}} ago', ['delta' => $interval->y], $this->locale);
806 92
            }
807 92
            if ($interval->m >= 1) {
808
                return Yii::t('yii', '{delta, plural, =1{a month} other{# months}} ago', ['delta' => $interval->m], $this->locale);
809 2
            }
810 2
            if ($interval->d >= 1) {
811
                return Yii::t('yii', '{delta, plural, =1{a day} other{# days}} ago', ['delta' => $interval->d], $this->locale);
812 2
            }
813 2
            if ($interval->h >= 1) {
814
                return Yii::t('yii', '{delta, plural, =1{an hour} other{# hours}} ago', ['delta' => $interval->h], $this->locale);
815 2
            }
816
            if ($interval->i >= 1) {
817
                return Yii::t('yii', '{delta, plural, =1{a minute} other{# minutes}} ago', ['delta' => $interval->i], $this->locale);
818
            }
819
            if ($interval->s == 0) {
820
                return Yii::t('yii', 'just now', [], $this->locale);
821
            }
822
            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 2
     *    `P-1D2H30M` - a negative date interval (`-1 day, 2 hours, 30 minutes`)
838
     *
839 2
     * @param string $implodeString will be used to concatenate duration parts. Defaults to `, `.
840 2
     * @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 2
     */
844 2
    public function asDuration($value, $implodeString = ', ', $negativeSign = '-')
845 2
    {
846 2
        if ($value === null) {
847 2
            return $this->nullDisplay;
848 2
        }
849 2
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
            $interval = $valueDateTime->diff($zeroDateTime);
858
        } 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 2
        }
865 2
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
        if ($interval->s > 0) {
882 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 second} other{# seconds}}', ['delta' => $interval->s], $this->locale);
883
        }
884
        if ($interval->s === 0 && empty($parts)) {
885
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 second} other{# seconds}}', ['delta' => $interval->s], $this->locale);
886
            $isNegative = false;
887
        }
888
889
        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 7
     *
899
     * @param mixed $value the value to be formatted.
900 7
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
901 5
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
902
     * @return string the formatted result.
903 7
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
904 5
     */
905 4
    public function asInteger($value, $options = [], $textOptions = [])
906 4
    {
907 4
        if ($value === null) {
908
            return $this->nullDisplay;
909
        }
910 4
        $value = $this->normalizeNumericValue($value);
911
        if ($this->_intlLoaded) {
912 1
            $f = $this->createNumberFormatter(NumberFormatter::DECIMAL, null, $options, $textOptions);
913
            $f->setAttribute(NumberFormatter::FRACTION_DIGITS, 0);
914
            if (($result = $f->format($value, NumberFormatter::TYPE_INT64)) === false) {
915
                throw new InvalidParamException('Formatting integer value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
916
            }
917
            return $result;
918
        } else {
919
            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 12
     * @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 12
     * @return string the formatted result.
940 2
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
941
     * @see decimalSeparator
942 12
     * @see thousandSeparator
943
     */
944 12
    public function asDecimal($value, $decimals = null, $options = [], $textOptions = [])
945 6
    {
946 6
        if ($value === null) {
947
            return $this->nullDisplay;
948
        }
949 6
        $value = $this->normalizeNumericValue($value);
950
951 6
        if ($this->_intlLoaded) {
952 4
            $f = $this->createNumberFormatter(NumberFormatter::DECIMAL, $decimals, $options, $textOptions);
953 4
            if (($result = $f->format($value)) === false) {
954 6
                throw new InvalidParamException('Formatting decimal value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
955
            }
956
            return $result;
957
        } else {
958
            if ($decimals === null) {
959
                $decimals = 2;
960
            }
961
            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 2
     * If you want consistent behavior between environments where intl is available and not, you should explicitly
976
     * specify a value here.
977 2
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
978 2
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
979
     * @return string the formatted result.
980 2
     * @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 1
    {
984 1
        if ($value === null) {
985
            return $this->nullDisplay;
986
        }
987 1
        $value = $this->normalizeNumericValue($value);
988
989 1
        if ($this->_intlLoaded) {
990 1
            $f = $this->createNumberFormatter(NumberFormatter::PERCENT, $decimals, $options, $textOptions);
991 1
            if (($result = $f->format($value)) === false) {
992 1
                throw new InvalidParamException('Formatting percent value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
993 1
            }
994
            return $result;
995
        } else {
996
            if ($decimals === null) {
997
                $decimals = 0;
998
            }
999
            $value *= 100;
1000
            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 2
     * If you want consistent behavior between environments where intl is available and not, you should explicitly
1014
     * specify a value here.
1015 2
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1016 2
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1017
     * @return string the formatted result.
1018 2
     * @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 1
    {
1022 1
        if ($value === null) {
1023
            return $this->nullDisplay;
1024
        }
1025 1
        $value = $this->normalizeNumericValue($value);
1026
1027 1
        if ($this->_intlLoaded) {
1028 1
            $f = $this->createNumberFormatter(NumberFormatter::SCIENTIFIC, $decimals, $options, $textOptions);
1029
            if (($result = $f->format($value)) === false) {
1030 1
                throw new InvalidParamException('Formatting scientific number value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
1031
            }
1032
            return $result;
1033
        } else {
1034
            if ($decimals !== null) {
1035
                return sprintf("%.{$decimals}E", $value);
1036
            } else {
1037
                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 4
     * If null, [[currencyCode]] will be used.
1051
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1052 4
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1053 2
     * @return string the formatted result.
1054
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1055 4
     * @throws InvalidConfigException if no currency is given and [[currencyCode]] is not defined.
1056
     */
1057 4
    public function asCurrency($value, $currency = null, $options = [], $textOptions = [])
1058 3
    {
1059
        if ($value === null) {
1060
            return $this->nullDisplay;
1061 3
        }
1062 3
        $value = $this->normalizeNumericValue($value);
1063 3
1064 3
        if ($this->_intlLoaded) {
1065 3
            $currency = $currency ?: $this->currencyCode;
1066 2
            // currency code must be set before fraction digits
1067 2
            // http://php.net/manual/en/numberformatter.formatcurrency.php#114376
1068 3
            if ($currency && !isset($textOptions[NumberFormatter::CURRENCY_CODE])) {
1069
                $textOptions[NumberFormatter::CURRENCY_CODE] = $currency;
1070 3
            }
1071
            $formatter = $this->createNumberFormatter(NumberFormatter::CURRENCY, null, $options, $textOptions);
1072
            if ($currency === null) {
1073 3
                $result = $formatter->format($value);
1074
            } else {
1075 1
                $result = $formatter->formatCurrency($value, $currency);
1076 1
            }
1077
            if ($result === false) {
1078
                throw new InvalidParamException('Formatting currency value failed: ' . $formatter->getErrorCode() . ' ' . $formatter->getErrorMessage());
1079 1
            }
1080 1
            return $result;
1081 1
        } else {
1082
            if ($currency === null) {
1083
                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
                $currency = $this->currencyCode;
1087
            }
1088
            return $currency . ' ' . $this->asDecimal($value, 2, $options, $textOptions);
1089
        }
1090
    }
1091
1092
    /**
1093
     * Formats the value as a number spellout.
1094
     *
1095 1
     * This function requires the [PHP intl extension](http://php.net/manual/en/book.intl.php) to be installed.
1096
     *
1097 1
     * @param mixed $value the value to be formatted
1098 1
     * @return string the formatted result.
1099
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1100 1
     * @throws InvalidConfigException when the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available.
1101 1
     */
1102 1
    public function asSpellout($value)
1103 1
    {
1104
        if ($value === null) {
1105
            return $this->nullDisplay;
1106 1
        }
1107
        $value = $this->normalizeNumericValue($value);
1108
        if ($this->_intlLoaded) {
1109
            $f = $this->createNumberFormatter(NumberFormatter::SPELLOUT);
1110
            if (($result = $f->format($value)) === false) {
1111
                throw new InvalidParamException('Formatting number as spellout failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
1112
            }
1113
            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 1
     * This function requires the [PHP intl extension](http://php.net/manual/en/book.intl.php) to be installed.
1123
     *
1124 1
     * @param mixed $value the value to be formatted
1125 1
     * @return string the formatted result.
1126
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1127 1
     * @throws InvalidConfigException when the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available.
1128 1
     */
1129 1
    public function asOrdinal($value)
1130 1
    {
1131
        if ($value === null) {
1132
            return $this->nullDisplay;
1133 1
        }
1134
        $value = $this->normalizeNumericValue($value);
1135
        if ($this->_intlLoaded) {
1136
            $f = $this->createNumberFormatter(NumberFormatter::ORDINAL);
1137
            if (($result = $f->format($value)) === false) {
1138
                throw new InvalidParamException('Formatting number as ordinal failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
1139
            }
1140
            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 5
     * @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 5
     * @return string the formatted result.
1159 2
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1160
     * @see sizeFormatBase
1161
     * @see asSize
1162 5
     */
1163
    public function asShortSize($value, $decimals = null, $options = [], $textOptions = [])
1164 5
    {
1165
        if ($value === null) {
1166 5
            return $this->nullDisplay;
1167 5
        }
1168 3
1169 3
        list($params, $position) = $this->formatSizeNumber($value, $decimals, $options, $textOptions);
1170 3
1171 3
        if ($this->sizeFormatBase == 1024) {
1172 2
            switch ($position) {
1173 2
                case 0:
1174 2
                    return Yii::t('yii', '{nFormatted} B', $params, $this->locale);
1175
                case 1:
1176 2
                    return Yii::t('yii', '{nFormatted} KiB', $params, $this->locale);
1177 2
                case 2:
1178 2
                    return Yii::t('yii', '{nFormatted} MiB', $params, $this->locale);
1179
                case 3:
1180
                    return Yii::t('yii', '{nFormatted} GiB', $params, $this->locale);
1181 2
                case 4:
1182 2
                    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 2
        } else {
1187 2
            switch ($position) {
1188 2
                case 0:
1189 2
                    return Yii::t('yii', '{nFormatted} B', $params, $this->locale);
1190
                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
                case 3:
1195
                    return Yii::t('yii', '{nFormatted} GB', $params, $this->locale);
1196
                case 4:
1197
                    return Yii::t('yii', '{nFormatted} TB', $params, $this->locale);
1198
                default:
1199
                    return Yii::t('yii', '{nFormatted} PB', $params, $this->locale);
1200
            }
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 6
     * @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 6
     * @return string the formatted result.
1215 2
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1216
     * @see sizeFormatBase
1217
     * @see asShortSize
1218 6
     */
1219
    public function asSize($value, $decimals = null, $options = [], $textOptions = [])
1220 6
    {
1221
        if ($value === null) {
1222 6
            return $this->nullDisplay;
1223 6
        }
1224 4
1225 4
        list($params, $position) = $this->formatSizeNumber($value, $decimals, $options, $textOptions);
1226 4
1227 4
        if ($this->sizeFormatBase == 1024) {
1228 4
            switch ($position) {
1229 4
                case 0:
1230 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{byte} other{bytes}}', $params, $this->locale);
1231
                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
                case 3:
1236
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{gibibyte} other{gibibytes}}', $params, $this->locale);
1237 4
                case 4:
1238 4
                    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 4
        } else {
1243 4
            switch ($position) {
1244 4
                case 0:
1245 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{byte} other{bytes}}', $params, $this->locale);
1246
                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
                case 3:
1251
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{gigabyte} other{gigabytes}}', $params, $this->locale);
1252
                case 4:
1253
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{terabyte} other{terabytes}}', $params, $this->locale);
1254
                default:
1255
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{petabyte} other{petabytes}}', $params, $this->locale);
1256
            }
1257
        }
1258
    }
1259
1260
1261
    /**
1262
     * Given the value in bytes formats number part of the human readable form.
1263
     *
1264 9
     * @param string|int|float $value value in bytes to be formatted.
1265
     * @param int $decimals the number of digits after the decimal point
1266 9
     * @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 9
     * @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 9
     */
1271 9
    private function formatSizeNumber($value, $decimals, $options, $textOptions)
1272
    {
1273 7
        $value = $this->normalizeNumericValue($value);
1274 7
1275 7
        $position = 0;
1276
        do {
1277
            if (abs($value) < $this->sizeFormatBase) {
1278 9
                break;
1279 9
            }
1280 9
            $value /= $this->sizeFormatBase;
1281 6
            $position++;
1282 6
        } while ($position < 5);
1283
1284 9
        // no decimals for bytes
1285 9
        if ($position === 0) {
1286 9
            $decimals = 0;
1287 5
        } elseif ($decimals !== null) {
1288 5
            $value = round($value, $decimals);
1289
        }
1290
        // disable grouping for edge cases like 1023 to get 1023 B instead of 1,023 B
1291
        $oldThousandSeparator = $this->thousandSeparator;
1292
        $this->thousandSeparator = '';
1293
        if ($this->_intlLoaded) {
1294 9
            $options[NumberFormatter::GROUPING_USED] = false;
1295
        }
1296 9
        // format the size value
1297 9
        $params = [
1298 9
            // 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 9
            // http://english.stackexchange.com/questions/9735/is-1-singular-or-plural
1301
            'n' => abs($value),
1302
            // this is the formatted number used for display
1303
            'nFormatted' => $this->asDecimal($value, $decimals, $options, $textOptions),
1304
        ];
1305
        $this->thousandSeparator = $oldThousandSeparator;
1306
1307
        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 28
     * - 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 28
     *
1318 16
     * @param mixed $value the input value
1319
     * @return float|int the normalized number value
1320 28
     * @throws InvalidParamException if the input value is not numeric.
1321 15
     */
1322 15
    protected function normalizeNumericValue($value)
1323 28
    {
1324 2
        if (empty($value)) {
1325
            return 0;
1326 26
        }
1327
        if (is_string($value) && is_numeric($value)) {
1328
            $value = (float) $value;
1329
        }
1330
        if (!is_numeric($value)) {
1331
            throw new InvalidParamException("'$value' is not a numeric value.");
1332
        }
1333
        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 17
     * Values: NumberFormatter::DECIMAL, ::CURRENCY, ::PERCENT, ::SCIENTIFIC, ::SPELLOUT, ::ORDINAL
1343
     * ::DURATION, ::PATTERN_RULEBASED, ::DEFAULT_STYLE, ::IGNORE
1344 17
     * @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 17
     * @return NumberFormatter the created formatter instance
1348 1
     */
1349 17
    protected function createNumberFormatter($style, $decimals = null, $options = [], $textOptions = [])
1350 17
    {
1351 3
        $formatter = new NumberFormatter($this->locale, $style);
1352 17
1353
        // set text attributes
1354
        foreach ($this->numberFormatterTextOptions as $name => $attribute) {
1355 17
            $formatter->setTextAttribute($name, $attribute);
1356 9
        }
1357 17
        foreach ($textOptions as $name => $attribute) {
1358 17
            $formatter->setTextAttribute($name, $attribute);
1359 7
        }
1360 17
1361 17
        // set attributes
1362 6
        foreach ($this->numberFormatterOptions as $name => $value) {
1363 6
            $formatter->setAttribute($name, $value);
1364 6
        }
1365
        foreach ($options as $name => $value) {
1366
            $formatter->setAttribute($name, $value);
1367 17
        }
1368 4
        if ($decimals !== null) {
1369 4
            $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $decimals);
1370 17
            $formatter->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $decimals);
1371 7
        }
1372 7
1373 7
        // set symbols
1374 17
        if ($this->decimalSeparator !== null) {
1375 2
            $formatter->setSymbol(NumberFormatter::DECIMAL_SEPARATOR_SYMBOL, $this->decimalSeparator);
1376 17
        }
1377
        if ($this->thousandSeparator !== null) {
1378 17
            $formatter->setSymbol(NumberFormatter::GROUPING_SEPARATOR_SYMBOL, $this->thousandSeparator);
1379
            $formatter->setSymbol(NumberFormatter::MONETARY_GROUPING_SEPARATOR_SYMBOL, $this->thousandSeparator);
1380
        }
1381
        foreach ($this->numberFormatterSymbols as $name => $symbol) {
1382
            $formatter->setSymbol($name, $symbol);
1383
        }
1384
1385
        return $formatter;
1386
    }
1387
}
1388