Completed
Pull Request — 2.1 (#12704)
by Robert
08:59
created

Formatter::createNumberFormatter()   F

Complexity

Conditions 9
Paths 256

Size

Total Lines 38
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 9.0033

Importance

Changes 0
Metric Value
dl 0
loc 38
ccs 28
cts 29
cp 0.9655
rs 3
c 0
b 0
f 0
cc 9
eloc 21
nc 256
nop 4
crap 9.0033
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 integer 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 boolean 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 230
    public function init()
264
    {
265 230
        if ($this->timeZone === null) {
266 230
            $this->timeZone = Yii::$app->timeZone;
267 230
        }
268 230
        if ($this->locale === null) {
269 19
            $this->locale = Yii::$app->language;
270 19
        }
271 230
        if ($this->booleanFormat === null) {
272 230
            $this->booleanFormat = [Yii::t('yii', 'No', [], $this->locale), Yii::t('yii', 'Yes', [], $this->locale)];
273 230
        }
274 230
        if ($this->nullDisplay === null) {
275 230
            $this->nullDisplay = '<span class="not-set">' . Yii::t('yii', '(not set)', [], $this->locale) . '</span>';
276 230
        }
277 230
        $this->_intlLoaded = extension_loaded('intl');
278 230
        if (!$this->_intlLoaded) {
279 114
            if ($this->decimalSeparator === null) {
280 114
                $this->decimalSeparator = '.';
281 114
            }
282 114
            if ($this->thousandSeparator === null) {
283 114
                $this->thousandSeparator = ',';
284 114
            }
285 114
        }
286 230
    }
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 8
    public function format($value, $format)
302
    {
303 8
        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 3
            $params = [$value];
313
        }
314 8
        $method = 'as' . $format;
315 8
        if ($this->hasMethod($method)) {
316 8
            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 2
    public function asText($value)
347
    {
348 2
        if ($value === null) {
349 2
            return $this->nullDisplay;
350
        }
351 2
        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 integer|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 integer|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 integer|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 integer|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 values
588 170
        if ($type === 'date') {
589 163
            list($timestamp, $hasTimeInfo) = $this->normalizeDatetimeValue($value, true);
590 161
            if (!$hasTimeInfo) {
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) {
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) {
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 integer|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 boolean $checkTimeInfo whether to also check if the date/time value has some time information attached.
654
     * Defaults to `false`. If `true`, the method will then return an array with the first element being the normalized
655
     * timestamp and the second a boolean indicating whether the timestamp has time information or it is just a date value.
656
     * This parameter is available since version 2.0.1.
657
     * @return DateTime|array the normalized datetime value.
658
     * Since version 2.0.1 this may also return an array if `$checkTimeInfo` is true.
659
     * The first element of the array is the normalized timestamp and the second is a boolean indicating whether
660
     * the timestamp has time information or it is just a date value.
661
     * @throws InvalidParamException if the input value can not be evaluated as a date value.
662
     */
663 174
    protected function normalizeDatetimeValue($value, $checkTimeInfo = false)
664
    {
665
        // checking for DateTime and DateTimeInterface is not redundant, DateTimeInterface is only in PHP>5.5
666 174
        if ($value === null || $value instanceof DateTime || $value instanceof DateTimeInterface) {
667
            // skip any processing
668 49
            return $checkTimeInfo ? [$value, true] : $value;
669
        }
670 134
        if (empty($value)) {
671 10
            $value = 0;
672 10
        }
673
        try {
674 134
            if (is_numeric($value)) { // process as unix timestamp, which is always in UTC
675 29
                $timestamp = new DateTime('@' . $value, new DateTimeZone('UTC'));
676 29
                return $checkTimeInfo ? [$timestamp, true] : $timestamp;
677 107
            } 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)
678 8
                return $checkTimeInfo ? [$timestamp, false] : $timestamp;
679 99
            } 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)
680 17
                return $checkTimeInfo ? [$timestamp, true] : $timestamp;
681
            }
682
            // finally try to create a DateTime object with the value
683 85
            if ($checkTimeInfo) {
684 83
                $timestamp = new DateTime($value, new DateTimeZone($this->defaultTimeZone));
685 81
                $info = date_parse($value);
686 81
                return [$timestamp, !($info['hour'] === false && $info['minute'] === false && $info['second'] === false)];
687
            } else {
688 83
                return new DateTime($value, new DateTimeZone($this->defaultTimeZone));
689
            }
690 2
        } catch (\Exception $e) {
691 2
            throw new InvalidParamException("'$value' is not a valid date time value: " . $e->getMessage()
692 2
                . "\n" . print_r(DateTime::getLastErrors(), true), $e->getCode(), $e);
693
        }
694
    }
695
696
    /**
697
     * Formats a date, time or datetime in a float number as UNIX timestamp (seconds since 01-01-1970).
698
     * @param integer|string|DateTime $value the value to be formatted. The following
699
     * types of value are supported:
700
     *
701
     * - an integer representing a UNIX timestamp
702
     * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
703
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
704
     * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
705
     *
706
     * @return string the formatted result.
707
     */
708 143
    public function asTimestamp($value)
709
    {
710 143
        if ($value === null) {
711 2
            return $this->nullDisplay;
712
        }
713 143
        $timestamp = $this->normalizeDatetimeValue($value);
714 143
        return number_format($timestamp->format('U'), 0, '.', '');
715
    }
716
717
    /**
718
     * Formats the value as the time interval between a date and now in human readable form.
719
     *
720
     * This method can be used in three different ways:
721
     *
722
     * 1. Using a timestamp that is relative to `now`.
723
     * 2. Using a timestamp that is relative to the `$referenceTime`.
724
     * 3. Using a `DateInterval` object.
725
     *
726
     * @param integer|string|DateTime|DateInterval $value the value to be formatted. The following
727
     * types of value are supported:
728
     *
729
     * - an integer representing a UNIX timestamp
730
     * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
731
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
732
     * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
733
     * - a PHP DateInterval object (a positive time interval will refer to the past, a negative one to the future)
734
     *
735
     * @param integer|string|DateTime $referenceTime if specified the value is used as a reference time instead of `now`
736
     * when `$value` is not a `DateInterval` object.
737
     * @return string the formatted result.
738
     * @throws InvalidParamException if the input value can not be evaluated as a date value.
739
     */
740 92
    public function asRelativeTime($value, $referenceTime = null)
741
    {
742 92
        if ($value === null) {
743 2
            return $this->nullDisplay;
744
        }
745
746 92
        if ($value instanceof DateInterval) {
747 2
            $interval = $value;
748 2
        } else {
749 92
            $timestamp = $this->normalizeDatetimeValue($value);
750
751 92
            if ($timestamp === false) {
752
                // $value is not a valid date/time value, so we try
753
                // to create a DateInterval with it
754
                try {
755
                    $interval = new DateInterval($value);
756
                } catch (\Exception $e) {
757
                    // invalid date/time and invalid interval
758
                    return $this->nullDisplay;
759
                }
760
            } else {
761 92
                $timeZone = new DateTimeZone($this->timeZone);
762
763 92
                if ($referenceTime === null) {
764
                    $dateNow = new DateTime('now', $timeZone);
765
                } else {
766 92
                    $dateNow = $this->normalizeDatetimeValue($referenceTime);
767 92
                    $dateNow->setTimezone($timeZone);
768
                }
769
770 92
                $dateThen = $timestamp->setTimezone($timeZone);
771
772 92
                $interval = $dateThen->diff($dateNow);
773
            }
774
        }
775
776 92
        if ($interval->invert) {
777 92
            if ($interval->y >= 1) {
778 2
                return Yii::t('yii', 'in {delta, plural, =1{a year} other{# years}}', ['delta' => $interval->y], $this->locale);
779
            }
780 92
            if ($interval->m >= 1) {
781 2
                return Yii::t('yii', 'in {delta, plural, =1{a month} other{# months}}', ['delta' => $interval->m], $this->locale);
782
            }
783 92
            if ($interval->d >= 1) {
784 2
                return Yii::t('yii', 'in {delta, plural, =1{a day} other{# days}}', ['delta' => $interval->d], $this->locale);
785
            }
786 92
            if ($interval->h >= 1) {
787 92
                return Yii::t('yii', 'in {delta, plural, =1{an hour} other{# hours}}', ['delta' => $interval->h], $this->locale);
788
            }
789 2
            if ($interval->i >= 1) {
790 2
                return Yii::t('yii', 'in {delta, plural, =1{a minute} other{# minutes}}', ['delta' => $interval->i], $this->locale);
791
            }
792 2
            if ($interval->s == 0) {
793
                return Yii::t('yii', 'just now', [], $this->locale);
794
            }
795 2
            return Yii::t('yii', 'in {delta, plural, =1{a second} other{# seconds}}', ['delta' => $interval->s], $this->locale);
796
        } else {
797 92
            if ($interval->y >= 1) {
798 2
                return Yii::t('yii', '{delta, plural, =1{a year} other{# years}} ago', ['delta' => $interval->y], $this->locale);
799
            }
800 92
            if ($interval->m >= 1) {
801 2
                return Yii::t('yii', '{delta, plural, =1{a month} other{# months}} ago', ['delta' => $interval->m], $this->locale);
802
            }
803 92
            if ($interval->d >= 1) {
804 2
                return Yii::t('yii', '{delta, plural, =1{a day} other{# days}} ago', ['delta' => $interval->d], $this->locale);
805
            }
806 92
            if ($interval->h >= 1) {
807 92
                return Yii::t('yii', '{delta, plural, =1{an hour} other{# hours}} ago', ['delta' => $interval->h], $this->locale);
808
            }
809 2
            if ($interval->i >= 1) {
810 2
                return Yii::t('yii', '{delta, plural, =1{a minute} other{# minutes}} ago', ['delta' => $interval->i], $this->locale);
811
            }
812 2
            if ($interval->s == 0) {
813 2
                return Yii::t('yii', 'just now', [], $this->locale);
814
            }
815 2
            return Yii::t('yii', '{delta, plural, =1{a second} other{# seconds}} ago', ['delta' => $interval->s], $this->locale);
816
        }
817
    }
818
819
    /**
820
     * Represents the value as duration in human readable format.
821
     *
822
     * @param DateInterval|string|integer $value the value to be formatted. Acceptable formats:
823
     *  - [DateInterval object](http://php.net/manual/ru/class.dateinterval.php)
824
     *  - integer - number of seconds. For example: value `131` represents `2 minutes, 11 seconds`
825
     *  - ISO8601 duration format. For example, all of these values represent `1 day, 2 hours, 30 minutes` duration:
826
     *    `2015-01-01T13:00:00Z/2015-01-02T13:30:00Z` - between two datetime values
827
     *    `2015-01-01T13:00:00Z/P1D2H30M` - time interval after datetime value
828
     *    `P1D2H30M/2015-01-02T13:30:00Z` - time interval before datetime value
829
     *    `P1D2H30M` - simply a date interval
830
     *    `P-1D2H30M` - a negative date interval (`-1 day, 2 hours, 30 minutes`)
831
     *
832
     * @param string $implodeString will be used to concatenate duration parts. Defaults to `, `.
833
     * @param string $negativeSign will be prefixed to the formatted duration, when it is negative. Defaults to `-`.
834
     * @return string the formatted duration.
835
     * @since 2.0.7
836
     */
837 2
    public function asDuration($value, $implodeString = ', ', $negativeSign = '-')
838
    {
839 2
        if ($value === null) {
840 2
            return $this->nullDisplay;
841
        }
842
843 2
        if ($value instanceof DateInterval) {
844 2
            $isNegative = $value->invert;
845 2
            $interval = $value;
846 2
        } elseif (is_numeric($value)) {
847 2
            $isNegative = $value < 0;
848 2
            $zeroDateTime = (new DateTime())->setTimestamp(0);
849 2
            $valueDateTime = (new DateTime())->setTimestamp(abs($value));
850 2
            $interval = $valueDateTime->diff($zeroDateTime);
851 2
        } elseif (strpos($value, 'P-') === 0) {
852 2
            $interval = new DateInterval('P'.substr($value, 2));
853 2
            $isNegative = true;
854 2
        } else {
855 2
            $interval = new DateInterval($value);
856 2
            $isNegative = $interval->invert;
857
        }
858
859 2
        if ($interval->y > 0) {
860 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...
861 2
        }
862 2
        if ($interval->m > 0) {
863 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...
864 2
        }
865 2
        if ($interval->d > 0) {
866 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 day} other{# days}}', ['delta' => $interval->d], $this->locale);
867 2
        }
868 2
        if ($interval->h > 0) {
869 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 hour} other{# hours}}', ['delta' => $interval->h], $this->locale);
870 2
        }
871 2
        if ($interval->i > 0) {
872 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 minute} other{# minutes}}', ['delta' => $interval->i], $this->locale);
873 2
        }
874 2
        if ($interval->s > 0) {
875 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 second} other{# seconds}}', ['delta' => $interval->s], $this->locale);
876 2
        }
877 2
        if ($interval->s === 0 && empty($parts)) {
878 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 second} other{# seconds}}', ['delta' => $interval->s], $this->locale);
879 2
            $isNegative = false;
880 2
        }
881
882 2
        return empty($parts) ? $this->nullDisplay : (($isNegative ? $negativeSign : '') . implode($implodeString, $parts));
883
    }
884
885
886
    // number formats
887
888
889
    /**
890
     * Formats the value as an integer number by removing any decimal digits without rounding.
891
     *
892
     * @param mixed $value the value to be formatted.
893
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
894
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
895
     * @return string the formatted result.
896
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
897
     */
898 7
    public function asInteger($value, $options = [], $textOptions = [])
899
    {
900 7
        if ($value === null) {
901 5
            return $this->nullDisplay;
902
        }
903 7
        $value = $this->normalizeNumericValue($value);
904 5
        if ($this->_intlLoaded) {
905 4
            $f = $this->createNumberFormatter(NumberFormatter::DECIMAL, null, $options, $textOptions);
906 4
            $f->setAttribute(NumberFormatter::FRACTION_DIGITS, 0);
907 4
            if (($result = $f->format($value, NumberFormatter::TYPE_INT64)) === false) {
908
                throw new InvalidParamException('Formatting integer value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
909
            }
910 4
            return $result;
911
        } else {
912 1
            return number_format((int) $value, 0, $this->decimalSeparator, $this->thousandSeparator);
913
        }
914
    }
915
916
    /**
917
     * Formats the value as a decimal number.
918
     *
919
     * Property [[decimalSeparator]] will be used to represent the decimal point. The
920
     * value is rounded automatically to the defined decimal digits.
921
     *
922
     * @param mixed $value the value to be formatted.
923
     * @param integer $decimals the number of digits after the decimal point. If not given the number of digits is determined from the
924
     * [[locale]] and if the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available defaults to `2`.
925
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
926
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
927
     * @return string the formatted result.
928
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
929
     * @see decimalSeparator
930
     * @see thousandSeparator
931
     */
932 12
    public function asDecimal($value, $decimals = null, $options = [], $textOptions = [])
933
    {
934 12
        if ($value === null) {
935 2
            return $this->nullDisplay;
936
        }
937 12
        $value = $this->normalizeNumericValue($value);
938
939 12
        if ($this->_intlLoaded) {
940 6
            $f = $this->createNumberFormatter(NumberFormatter::DECIMAL, $decimals, $options, $textOptions);
941 6
            if (($result = $f->format($value)) === false) {
942
                throw new InvalidParamException('Formatting decimal value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
943
            }
944 6
            return $result;
945
        } else {
946 6
            if ($decimals === null) {
947 4
                $decimals = 2;
948 4
            }
949 6
            return number_format($value, $decimals, $this->decimalSeparator, $this->thousandSeparator);
950
        }
951
    }
952
953
954
    /**
955
     * Formats the value as a percent number with "%" sign.
956
     *
957
     * @param mixed $value the value to be formatted. It must be a factor e.g. `0.75` will result in `75%`.
958
     * @param integer $decimals the number of digits after the decimal point.
959
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
960
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
961
     * @return string the formatted result.
962
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
963
     */
964 2
    public function asPercent($value, $decimals = null, $options = [], $textOptions = [])
965
    {
966 2
        if ($value === null) {
967 2
            return $this->nullDisplay;
968
        }
969 2
        $value = $this->normalizeNumericValue($value);
970
971 2
        if ($this->_intlLoaded) {
972 1
            $f = $this->createNumberFormatter(NumberFormatter::PERCENT, $decimals, $options, $textOptions);
973 1
            if (($result = $f->format($value)) === false) {
974
                throw new InvalidParamException('Formatting percent value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
975
            }
976 1
            return $result;
977
        } else {
978 1
            if ($decimals === null) {
979 1
                $decimals = 0;
980 1
            }
981 1
            $value *= 100;
982 1
            return number_format($value, $decimals, $this->decimalSeparator, $this->thousandSeparator) . '%';
983
        }
984
    }
985
986
    /**
987
     * Formats the value as a scientific number.
988
     *
989
     * @param mixed $value the value to be formatted.
990
     * @param integer $decimals the number of digits after the decimal point.
991
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
992
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
993
     * @return string the formatted result.
994
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
995
     */
996 2
    public function asScientific($value, $decimals = null, $options = [], $textOptions = [])
997
    {
998 2
        if ($value === null) {
999 2
            return $this->nullDisplay;
1000
        }
1001 2
        $value = $this->normalizeNumericValue($value);
1002
1003 2
        if ($this->_intlLoaded) {
1004 1
            $f = $this->createNumberFormatter(NumberFormatter::SCIENTIFIC, $decimals, $options, $textOptions);
1005 1
            if (($result = $f->format($value)) === false) {
1006
                throw new InvalidParamException('Formatting scientific number value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
1007
            }
1008 1
            return $result;
1009
        } else {
1010 1
            if ($decimals !== null) {
1011 1
                return sprintf("%.{$decimals}E", $value);
1012
            } else {
1013 1
                return sprintf('%.E', $value);
1014
            }
1015
        }
1016
    }
1017
1018
    /**
1019
     * Formats the value as a currency number.
1020
     *
1021
     * This function does not require the [PHP intl extension](http://php.net/manual/en/book.intl.php) to be installed
1022
     * to work, but it is highly recommended to install it to get good formatting results.
1023
     *
1024
     * @param mixed $value the value to be formatted.
1025
     * @param string $currency the 3-letter ISO 4217 currency code indicating the currency to use.
1026
     * If null, [[currencyCode]] will be used.
1027
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1028
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1029
     * @return string the formatted result.
1030
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1031
     * @throws InvalidConfigException if no currency is given and [[currencyCode]] is not defined.
1032
     */
1033 3
    public function asCurrency($value, $currency = null, $options = [], $textOptions = [])
1034
    {
1035 3
        if ($value === null) {
1036 2
            return $this->nullDisplay;
1037
        }
1038 3
        $value = $this->normalizeNumericValue($value);
1039
1040 3
        if ($this->_intlLoaded) {
1041 2
            $formatter = $this->createNumberFormatter(NumberFormatter::CURRENCY, null, $options, $textOptions);
1042 2
            if ($currency === null) {
1043 1
                if ($this->currencyCode === null) {
1044 1
                    if (($result = $formatter->format($value)) === false) {
1045
                        throw new InvalidParamException('Formatting currency value failed: ' . $formatter->getErrorCode() . ' ' . $formatter->getErrorMessage());
1046
                    }
1047 1
                    return $result;
1048
                }
1049 1
                $currency = $this->currencyCode;
1050 1
            }
1051 2
            if (($result = $formatter->formatCurrency($value, $currency)) === false) {
1052
                throw new InvalidParamException('Formatting currency value failed: ' . $formatter->getErrorCode() . ' ' . $formatter->getErrorMessage());
1053
            }
1054 2
            return $result;
1055
        } else {
1056 1
            if ($currency === null) {
1057 1
                if ($this->currencyCode === null) {
1058
                    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.');
1059
                }
1060 1
                $currency = $this->currencyCode;
1061 1
            }
1062 1
            return $currency . ' ' . $this->asDecimal($value, 2, $options, $textOptions);
1063
        }
1064
    }
1065
1066
    /**
1067
     * Formats the value as a number spellout.
1068
     *
1069
     * This function requires the [PHP intl extension](http://php.net/manual/en/book.intl.php) to be installed.
1070
     *
1071
     * @param mixed $value the value to be formatted
1072
     * @return string the formatted result.
1073
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1074
     * @throws InvalidConfigException when the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available.
1075
     */
1076 1
    public function asSpellout($value)
1077
    {
1078 1
        if ($value === null) {
1079 1
            return $this->nullDisplay;
1080
        }
1081 1
        $value = $this->normalizeNumericValue($value);
1082 1
        if ($this->_intlLoaded) {
1083 1
            $f = $this->createNumberFormatter(NumberFormatter::SPELLOUT);
1084 1
            if (($result = $f->format($value)) === false) {
1085
                throw new InvalidParamException('Formatting number as spellout failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
1086
            }
1087 1
            return $result;
1088
        } else {
1089
            throw new InvalidConfigException('Format as Spellout is only supported when PHP intl extension is installed.');
1090
        }
1091
    }
1092
1093
    /**
1094
     * Formats the value as a ordinal value of a number.
1095
     *
1096
     * This function requires the [PHP intl extension](http://php.net/manual/en/book.intl.php) to be installed.
1097
     *
1098
     * @param mixed $value the value to be formatted
1099
     * @return string the formatted result.
1100
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1101
     * @throws InvalidConfigException when the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available.
1102
     */
1103 1
    public function asOrdinal($value)
1104
    {
1105 1
        if ($value === null) {
1106 1
            return $this->nullDisplay;
1107
        }
1108 1
        $value = $this->normalizeNumericValue($value);
1109 1
        if ($this->_intlLoaded) {
1110 1
            $f = $this->createNumberFormatter(NumberFormatter::ORDINAL);
1111 1
            if (($result = $f->format($value)) === false) {
1112
                throw new InvalidParamException('Formatting number as ordinal failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
1113
            }
1114 1
            return $result;
1115
        } else {
1116
            throw new InvalidConfigException('Format as Ordinal is only supported when PHP intl extension is installed.');
1117
        }
1118
    }
1119
1120
    /**
1121
     * Formats the value in bytes as a size in human readable form for example `12 KB`.
1122
     *
1123
     * This is the short form of [[asSize]].
1124
     *
1125
     * If [[sizeFormatBase]] is 1024, [binary prefixes](http://en.wikipedia.org/wiki/Binary_prefix) (e.g. kibibyte/KiB, mebibyte/MiB, ...)
1126
     * are used in the formatting result.
1127
     *
1128
     * @param string|integer|float $value value in bytes to be formatted.
1129
     * @param integer $decimals the number of digits after the decimal point.
1130
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1131
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1132
     * @return string the formatted result.
1133
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1134
     * @see sizeFormat
1135
     * @see asSize
1136
     */
1137 5
    public function asShortSize($value, $decimals = null, $options = [], $textOptions = [])
1138
    {
1139 5
        if ($value === null) {
1140 2
            return $this->nullDisplay;
1141
        }
1142
1143 5
        list($params, $position) = $this->formatSizeNumber($value, $decimals, $options, $textOptions);
1144
1145 5
        if ($this->sizeFormatBase == 1024) {
1146
            switch ($position) {
1147 5
                case 0:
1148 5
                    return Yii::t('yii', '{nFormatted} B', $params, $this->locale);
1149 3
                case 1:
1150 3
                    return Yii::t('yii', '{nFormatted} KiB', $params, $this->locale);
1151 3
                case 2:
1152 3
                    return Yii::t('yii', '{nFormatted} MiB', $params, $this->locale);
1153 2
                case 3:
1154 2
                    return Yii::t('yii', '{nFormatted} GiB', $params, $this->locale);
1155 2
                case 4:
1156
                    return Yii::t('yii', '{nFormatted} TiB', $params, $this->locale);
1157 2
                default:
1158 2
                    return Yii::t('yii', '{nFormatted} PiB', $params, $this->locale);
1159 2
            }
1160
        } else {
1161
            switch ($position) {
1162 2
                case 0:
1163 2
                    return Yii::t('yii', '{nFormatted} B', $params, $this->locale);
1164 2
                case 1:
1165 2
                    return Yii::t('yii', '{nFormatted} KB', $params, $this->locale);
1166 2
                case 2:
1167 2
                    return Yii::t('yii', '{nFormatted} MB', $params, $this->locale);
1168 2
                case 3:
1169 2
                    return Yii::t('yii', '{nFormatted} GB', $params, $this->locale);
1170 2
                case 4:
1171
                    return Yii::t('yii', '{nFormatted} TB', $params, $this->locale);
1172 2
                default:
1173 2
                    return Yii::t('yii', '{nFormatted} PB', $params, $this->locale);
1174 2
            }
1175
        }
1176
    }
1177
1178
    /**
1179
     * Formats the value in bytes as a size in human readable form, for example `12 kilobytes`.
1180
     *
1181
     * If [[sizeFormatBase]] is 1024, [binary prefixes](http://en.wikipedia.org/wiki/Binary_prefix) (e.g. kibibyte/KiB, mebibyte/MiB, ...)
1182
     * are used in the formatting result.
1183
     *
1184
     * @param string|integer|float $value value in bytes to be formatted.
1185
     * @param integer $decimals the number of digits after the decimal point.
1186
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1187
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1188
     * @return string the formatted result.
1189
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1190
     * @see sizeFormat
1191
     * @see asShortSize
1192
     */
1193 6
    public function asSize($value, $decimals = null, $options = [], $textOptions = [])
1194
    {
1195 6
        if ($value === null) {
1196 2
            return $this->nullDisplay;
1197
        }
1198
1199 6
        list($params, $position) = $this->formatSizeNumber($value, $decimals, $options, $textOptions);
1200
1201 6
        if ($this->sizeFormatBase == 1024) {
1202
            switch ($position) {
1203 6
                case 0:
1204 6
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{byte} other{bytes}}', $params, $this->locale);
1205 4
                case 1:
1206 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{kibibyte} other{kibibytes}}', $params, $this->locale);
1207 4
                case 2:
1208 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{mebibyte} other{mebibytes}}', $params, $this->locale);
1209 4
                case 3:
1210 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{gibibyte} other{gibibytes}}', $params, $this->locale);
1211 4
                case 4:
1212
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{tebibyte} other{tebibytes}}', $params, $this->locale);
1213 4
                default:
1214 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{pebibyte} other{pebibytes}}', $params, $this->locale);
1215 4
            }
1216
        } else {
1217
            switch ($position) {
1218 4
                case 0:
1219 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{byte} other{bytes}}', $params, $this->locale);
1220 4
                case 1:
1221 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{kilobyte} other{kilobytes}}', $params, $this->locale);
1222 4
                case 2:
1223 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{megabyte} other{megabytes}}', $params, $this->locale);
1224 4
                case 3:
1225 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{gigabyte} other{gigabytes}}', $params, $this->locale);
1226 4
                case 4:
1227
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{terabyte} other{terabytes}}', $params, $this->locale);
1228 4
                default:
1229 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{petabyte} other{petabytes}}', $params, $this->locale);
1230 4
            }
1231
        }
1232
    }
1233
1234
1235
    /**
1236
     * Given the value in bytes formats number part of the human readable form.
1237
     *
1238
     * @param string|integer|float $value value in bytes to be formatted.
1239
     * @param integer $decimals the number of digits after the decimal point
1240
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1241
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1242
     * @return array [parameters for Yii::t containing formatted number, internal position of size unit]
1243
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1244
     */
1245 9
    private function formatSizeNumber($value, $decimals, $options, $textOptions)
1246
    {
1247 9
        $value = $this->normalizeNumericValue($value);
1248
1249 9
        $position = 0;
1250
        do {
1251 9
            if (abs($value) < $this->sizeFormatBase) {
1252 9
                break;
1253
            }
1254 7
            $value /= $this->sizeFormatBase;
1255 7
            $position++;
1256 7
        } while ($position < 5);
1257
1258
        // no decimals for bytes
1259 9
        if ($position === 0) {
1260 9
            $decimals = 0;
1261 9
        } elseif ($decimals !== null) {
1262 6
            $value = round($value, $decimals);
1263 6
        }
1264
        // disable grouping for edge cases like 1023 to get 1023 B instead of 1,023 B
1265 9
        $oldThousandSeparator = $this->thousandSeparator;
1266 9
        $this->thousandSeparator = '';
1267 9
        if ($this->_intlLoaded) {
1268 5
            $options[NumberFormatter::GROUPING_USED] = false;
1269 5
        }
1270
        // format the size value
1271
        $params = [
1272
            // this is the unformatted number used for the plural rule
1273
            // abs() to make sure the plural rules work correctly on negative numbers, intl does not cover this
1274
            // http://english.stackexchange.com/questions/9735/is-1-singular-or-plural
1275 9
            'n' => abs($value),
1276
            // this is the formatted number used for display
1277 9
            'nFormatted' => $this->asDecimal($value, $decimals, $options, $textOptions),
1278 9
        ];
1279 9
        $this->thousandSeparator = $oldThousandSeparator;
1280
1281 9
        return [$params, $position];
1282
    }
1283
1284
    /**
1285
     * Normalizes a numeric input value
1286
     *
1287
     * - everything [empty](http://php.net/manual/en/function.empty.php) will result in `0`
1288
     * - a [numeric](http://php.net/manual/en/function.is-numeric.php) string will be casted to float
1289
     * - everything else will be returned if it is [numeric](http://php.net/manual/en/function.is-numeric.php),
1290
     *   otherwise an exception is thrown.
1291
     *
1292
     * @param mixed $value the input value
1293
     * @return float|integer the normalized number value
1294
     * @throws InvalidParamException if the input value is not numeric.
1295
     */
1296 27
    protected function normalizeNumericValue($value)
1297
    {
1298 27
        if (empty($value)) {
1299 16
            return 0;
1300
        }
1301 27
        if (is_string($value) && is_numeric($value)) {
1302 14
            $value = (float) $value;
1303 14
        }
1304 27
        if (!is_numeric($value)) {
1305 2
            throw new InvalidParamException("'$value' is not a numeric value.");
1306
        }
1307 25
        return $value;
1308
    }
1309
1310
    /**
1311
     * Creates a number formatter based on the given type and format.
1312
     *
1313
     * You may override this method to create a number formatter based on patterns.
1314
     *
1315
     * @param integer $style the type of the number formatter.
1316
     * Values: NumberFormatter::DECIMAL, ::CURRENCY, ::PERCENT, ::SCIENTIFIC, ::SPELLOUT, ::ORDINAL
1317
     * ::DURATION, ::PATTERN_RULEBASED, ::DEFAULT_STYLE, ::IGNORE
1318
     * @param integer $decimals the number of digits after the decimal point.
1319
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1320
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1321
     * @return NumberFormatter the created formatter instance
1322
     */
1323 16
    protected function createNumberFormatter($style, $decimals = null, $options = [], $textOptions = [])
1324
    {
1325 16
        $formatter = new NumberFormatter($this->locale, $style);
1326
1327
        // set text attributes
1328 16
        foreach ($this->numberFormatterTextOptions as $name => $attribute) {
1329 1
            $formatter->setTextAttribute($name, $attribute);
1330 16
        }
1331 16
        foreach ($textOptions as $name => $attribute) {
1332
            $formatter->setTextAttribute($name, $attribute);
1333 16
        }
1334
1335
        // set attributes
1336 16
        foreach ($this->numberFormatterOptions as $name => $value) {
1337 8
            $formatter->setAttribute($name, $value);
1338 16
        }
1339 16
        foreach ($options as $name => $value) {
1340 6
            $formatter->setAttribute($name, $value);
1341 16
        }
1342 16
        if ($decimals !== null) {
1343 6
            $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $decimals);
1344 6
            $formatter->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $decimals);
1345 6
        }
1346
1347
        // set symbols
1348 16
        if ($this->decimalSeparator !== null) {
1349 4
            $formatter->setSymbol(NumberFormatter::DECIMAL_SEPARATOR_SYMBOL, $this->decimalSeparator);
1350 4
        }
1351 16
        if ($this->thousandSeparator !== null) {
1352 7
            $formatter->setSymbol(NumberFormatter::GROUPING_SEPARATOR_SYMBOL, $this->thousandSeparator);
1353 7
            $formatter->setSymbol(NumberFormatter::MONETARY_GROUPING_SEPARATOR_SYMBOL, $this->thousandSeparator);
1354 7
        }
1355 16
        foreach ($this->numberFormatterSymbols as $name => $symbol) {
1356 1
            $formatter->setSymbol($name, $symbol);
1357 16
        }
1358
1359 16
        return $formatter;
1360
    }
1361
}
1362