Passed
Push — master ( 9dbdd9...d5a428 )
by Alexander
04:15
created

framework/i18n/Formatter.php (4 issues)

1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\i18n;
9
10
use Closure;
11
use DateInterval;
12
use DateTime;
13
use DateTimeInterface;
14
use DateTimeZone;
15
use IntlDateFormatter;
16
use NumberFormatter;
17
use Yii;
18
use yii\base\Component;
19
use yii\base\InvalidArgumentException;
20
use yii\base\InvalidConfigException;
21
use yii\helpers\FormatConverter;
22
use yii\helpers\Html;
23
use yii\helpers\HtmlPurifier;
24
25
/**
26
 * Formatter provides a set of commonly used data formatting methods.
27
 *
28
 * The formatting methods provided by Formatter are all named in the form of `asXyz()`.
29
 * The behavior of some of them may be configured via the properties of Formatter. For example,
30
 * by configuring [[dateFormat]], one may control how [[asDate()]] formats the value into a date string.
31
 *
32
 * Formatter is configured as an application component in [[\yii\base\Application]] by default.
33
 * You can access that instance via `Yii::$app->formatter`.
34
 *
35
 * The Formatter class is designed to format values according to a [[locale]]. For this feature to work
36
 * the [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) has to be installed.
37
 * Most of the methods however work also if the PHP intl extension is not installed by providing
38
 * a fallback implementation. Without intl month and day names are in English only.
39
 * Note that even if the intl extension is installed, formatting date and time values for years >=2038 or <=1901
40
 * on 32bit systems will fall back to the PHP implementation because intl uses a 32bit UNIX timestamp internally.
41
 * On a 64bit system the intl formatter is used in all cases if installed.
42
 *
43
 * > Note: The Formatter class is meant to be used for formatting values for display to users in different
44
 * > languages and time zones. If you need to format a date or time in machine readable format, use the
45
 * > PHP [date()](https://secure.php.net/manual/en/function.date.php) function instead.
46
 *
47
 * @author Qiang Xue <[email protected]>
48
 * @author Enrica Ruedin <[email protected]>
49
 * @author Carsten Brandt <[email protected]>
50
 * @since 2.0
51
 */
52
class Formatter extends Component
53
{
54
    /**
55
     * @since 2.0.13
56
     */
57
    const UNIT_SYSTEM_METRIC = 'metric';
58
    /**
59
     * @since 2.0.13
60
     */
61
    const UNIT_SYSTEM_IMPERIAL = 'imperial';
62
    /**
63
     * @since 2.0.13
64
     */
65
    const FORMAT_WIDTH_LONG = 'long';
66
    /**
67
     * @since 2.0.13
68
     */
69
    const FORMAT_WIDTH_SHORT = 'short';
70
    /**
71
     * @since 2.0.13
72
     */
73
    const UNIT_LENGTH = 'length';
74
    /**
75
     * @since 2.0.13
76
     */
77
    const UNIT_WEIGHT = 'mass';
78
79
    /**
80
     * @var string the text to be displayed when formatting a `null` value.
81
     * Defaults to `'<span class="not-set">(not set)</span>'`, where `(not set)`
82
     * will be translated according to [[locale]].
83
     */
84
    public $nullDisplay;
85
    /**
86
     * @var array the text to be displayed when formatting a boolean value. The first element corresponds
87
     * to the text displayed for `false`, the second element for `true`.
88
     * Defaults to `['No', 'Yes']`, where `Yes` and `No`
89
     * will be translated according to [[locale]].
90
     */
91
    public $booleanFormat;
92
    /**
93
     * @var string the locale ID that is used to localize the date and number formatting.
94
     * For number and date formatting this is only effective when the
95
     * [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) is installed.
96
     * If not set, [[\yii\base\Application::language]] will be used.
97
     */
98
    public $locale;
99
    /**
100
     * @var string the language code (e.g. `en-US`, `en`) that is used to translate internal messages.
101
     * If not set, [[locale]] will be used (without the `@calendar` param, if included).
102
     *
103
     * @since 2.0.28
104
     */
105
    public $language;
106
    /**
107
     * @var string the time zone to use for formatting time and date values.
108
     *
109
     * This can be any value that may be passed to [date_default_timezone_set()](https://secure.php.net/manual/en/function.date-default-timezone-set.php)
110
     * e.g. `UTC`, `Europe/Berlin` or `America/Chicago`.
111
     * Refer to the [php manual](https://secure.php.net/manual/en/timezones.php) for available time zones.
112
     * If this property is not set, [[\yii\base\Application::timeZone]] will be used.
113
     *
114
     * 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.
115
     * If you store your data in a different time zone in the database, you have to adjust [[defaultTimeZone]] accordingly.
116
     */
117
    public $timeZone;
118
    /**
119
     * @var string the time zone that is assumed for input values if they do not include a time zone explicitly.
120
     *
121
     * The value must be a valid time zone identifier, e.g. `UTC`, `Europe/Berlin` or `America/Chicago`.
122
     * Please refer to the [php manual](https://secure.php.net/manual/en/timezones.php) for available time zones.
123
     *
124
     * It defaults to `UTC` so you only have to adjust this value if you store datetime values in another time zone in your database.
125
     *
126
     * Note that a UNIX timestamp is always in UTC by its definition. That means that specifying a default time zone different from
127
     * UTC has no effect on date values given as UNIX timestamp.
128
     *
129
     * @since 2.0.1
130
     */
131
    public $defaultTimeZone = 'UTC';
132
    /**
133
     * @var string the default format string to be used to format a [[asDate()|date]].
134
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
135
     *
136
     * 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).
137
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
138
     * PHP [date()](https://secure.php.net/manual/en/function.date.php)-function.
139
     *
140
     * For example:
141
     *
142
     * ```php
143
     * 'MM/dd/yyyy' // date in ICU format
144
     * 'php:m/d/Y' // the same date in PHP format
145
     * ```
146
     */
147
    public $dateFormat = 'medium';
148
    /**
149
     * @var string the default format string to be used to format a [[asTime()|time]].
150
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
151
     *
152
     * 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).
153
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
154
     * PHP [date()](https://secure.php.net/manual/en/function.date.php)-function.
155
     *
156
     * For example:
157
     *
158
     * ```php
159
     * 'HH:mm:ss' // time in ICU format
160
     * 'php:H:i:s' // the same time in PHP format
161
     * ```
162
     */
163
    public $timeFormat = 'medium';
164
    /**
165
     * @var string the default format string to be used to format a [[asDatetime()|date and time]].
166
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
167
     *
168
     * 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).
169
     *
170
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
171
     * PHP [date()](https://secure.php.net/manual/en/function.date.php)-function.
172
     *
173
     * For example:
174
     *
175
     * ```php
176
     * 'MM/dd/yyyy HH:mm:ss' // date and time in ICU format
177
     * 'php:m/d/Y H:i:s' // the same date and time in PHP format
178
     * ```
179
     */
180
    public $datetimeFormat = 'medium';
181
    /**
182
     * @var \IntlCalendar|int|null the calendar to be used for date formatting. The value of this property will be directly
183
     * passed to the [constructor of the `IntlDateFormatter` class](https://secure.php.net/manual/en/intldateformatter.create.php).
184
     *
185
     * Defaults to `null`, which means the Gregorian calendar will be used. You may also explicitly pass the constant
186
     * `\IntlDateFormatter::GREGORIAN` for Gregorian calendar.
187
     *
188
     * To use an alternative calendar like for example the [Jalali calendar](https://en.wikipedia.org/wiki/Jalali_calendar),
189
     * set this property to `\IntlDateFormatter::TRADITIONAL`.
190
     * The calendar must then be specified in the [[locale]], for example for the persian calendar the configuration for the formatter would be:
191
     *
192
     * ```php
193
     * 'formatter' => [
194
     *     'locale' => 'fa_IR@calendar=persian',
195
     *     'calendar' => \IntlDateFormatter::TRADITIONAL,
196
     * ],
197
     * ```
198
     *
199
     * Available calendar names can be found in the [ICU manual](http://userguide.icu-project.org/datetime/calendar).
200
     *
201
     * Since PHP 5.5 you may also use an instance of the [[\IntlCalendar]] class.
202
     * Check the [PHP manual](https://secure.php.net/manual/en/intldateformatter.create.php) for more details.
203
     *
204
     * If the [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) is not available, setting this property will have no effect.
205
     *
206
     * @see https://secure.php.net/manual/en/intldateformatter.create.php
207
     * @see https://secure.php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants.calendartypes
208
     * @see https://secure.php.net/manual/en/class.intlcalendar.php
209
     * @since 2.0.7
210
     */
211
    public $calendar;
212
    /**
213
     * @var string the character displayed as the decimal point when formatting a number.
214
     * If not set, the decimal separator corresponding to [[locale]] will be used.
215
     * If [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) is not available, the default value is '.'.
216
     */
217
    public $decimalSeparator;
218
    /**
219
     * @var string the character displayed as the decimal point when formatting a currency.
220
     * If not set, the currency decimal separator corresponding to [[locale]] will be used.
221
     * If [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) is not available, setting this property will have no effect.
222
     * @since 2.0.35
223
     */
224
    public $currencyDecimalSeparator;
225
    /**
226
     * @var string the character displayed as the thousands separator (also called grouping separator) character when formatting a number.
227
     * If not set, the thousand separator corresponding to [[locale]] will be used.
228
     * If [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) is not available, the default value is ','.
229
     */
230
    public $thousandSeparator;
231
    /**
232
     * @var array a list of name value pairs that are passed to the
233
     * intl [NumberFormatter::setAttribute()](https://secure.php.net/manual/en/numberformatter.setattribute.php) method of all
234
     * the number formatter objects created by [[createNumberFormatter()]].
235
     * This property takes only effect if the [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) is installed.
236
     *
237
     * Please refer to the [PHP manual](https://secure.php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants.unumberformatattribute)
238
     * for the possible options.
239
     *
240
     * For example to adjust the maximum and minimum value of fraction digits you can configure this property like the following:
241
     *
242
     * ```php
243
     * [
244
     *     NumberFormatter::MIN_FRACTION_DIGITS => 0,
245
     *     NumberFormatter::MAX_FRACTION_DIGITS => 2,
246
     * ]
247
     * ```
248
     */
249
    public $numberFormatterOptions = [];
250
    /**
251
     * @var array a list of name value pairs that are passed to the
252
     * intl [NumberFormatter::setTextAttribute()](https://secure.php.net/manual/en/numberformatter.settextattribute.php) method of all
253
     * the number formatter objects created by [[createNumberFormatter()]].
254
     * This property takes only effect if the [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) is installed.
255
     *
256
     * Please refer to the [PHP manual](https://secure.php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants.unumberformattextattribute)
257
     * for the possible options.
258
     *
259
     * For example to change the minus sign for negative numbers you can configure this property like the following:
260
     *
261
     * ```php
262
     * [
263
     *     NumberFormatter::NEGATIVE_PREFIX => 'MINUS',
264
     * ]
265
     * ```
266
     */
267
    public $numberFormatterTextOptions = [];
268
    /**
269
     * @var array a list of name value pairs that are passed to the
270
     * intl [NumberFormatter::setSymbol()](https://secure.php.net/manual/en/numberformatter.setsymbol.php) method of all
271
     * the number formatter objects created by [[createNumberFormatter()]].
272
     * This property takes only effect if the [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) is installed.
273
     *
274
     * Please refer to the [PHP manual](https://secure.php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants.unumberformatsymbol)
275
     * for the possible options.
276
     *
277
     * For example to choose a custom currency symbol, e.g. [U+20BD](http://unicode-table.com/en/20BD/) instead of `руб.` for Russian Ruble:
278
     *
279
     * ```php
280
     * [
281
     *     NumberFormatter::CURRENCY_SYMBOL => '₽',
282
     * ]
283
     * ```
284
     *
285
     * @since 2.0.4
286
     */
287
    public $numberFormatterSymbols = [];
288
    /**
289
     * @var string the 3-letter ISO 4217 currency code indicating the default currency to use for [[asCurrency]].
290
     * If not set, the currency code corresponding to [[locale]] will be used.
291
     * Note that in this case the [[locale]] has to be specified with a country code, e.g. `en-US` otherwise it
292
     * is not possible to determine the default currency.
293
     */
294
    public $currencyCode;
295
    /**
296
     * @var int the base at which a kilobyte is calculated (1000 or 1024 bytes per kilobyte), used by [[asSize]] and [[asShortSize]].
297
     * Defaults to 1024.
298
     */
299
    public $sizeFormatBase = 1024;
300
    /**
301
     * @var string default system of measure units. Defaults to [[UNIT_SYSTEM_METRIC]].
302
     * Possible values:
303
     *  - [[UNIT_SYSTEM_METRIC]]
304
     *  - [[UNIT_SYSTEM_IMPERIAL]]
305
     *
306
     * @see asLength
307
     * @see asWeight
308
     * @since 2.0.13
309
     */
310
    public $systemOfUnits = self::UNIT_SYSTEM_METRIC;
311
    /**
312
     * @var array configuration of weight and length measurement units.
313
     * This array contains the most usable measurement units, but you can change it
314
     * in case you have some special requirements.
315
     *
316
     * For example, you can add smaller measure unit:
317
     *
318
     * ```php
319
     * $this->measureUnits[self::UNIT_LENGTH][self::UNIT_SYSTEM_METRIC] = [
320
     *     'nanometer' => 0.000001
321
     * ]
322
     * ```
323
     * @see asLength
324
     * @see asWeight
325
     * @since 2.0.13
326
     */
327
    public $measureUnits = [
328
        self::UNIT_LENGTH => [
329
            self::UNIT_SYSTEM_IMPERIAL => [
330
                'inch' => 1,
331
                'foot' => 12,
332
                'yard' => 36,
333
                'chain' => 792,
334
                'furlong' => 7920,
335
                'mile' => 63360,
336
            ],
337
            self::UNIT_SYSTEM_METRIC => [
338
                'millimeter' => 1,
339
                'centimeter' => 10,
340
                'meter' => 1000,
341
                'kilometer' => 1000000,
342
            ],
343
        ],
344
        self::UNIT_WEIGHT => [
345
            self::UNIT_SYSTEM_IMPERIAL => [
346
                'grain' => 1,
347
                'drachm' => 27.34375,
348
                'ounce' => 437.5,
349
                'pound' => 7000,
350
                'stone' => 98000,
351
                'quarter' => 196000,
352
                'hundredweight' => 784000,
353
                'ton' => 15680000,
354
            ],
355
            self::UNIT_SYSTEM_METRIC => [
356
                'gram' => 1,
357
                'kilogram' => 1000,
358
                'ton' => 1000000,
359
            ],
360
        ],
361
    ];
362
    /**
363
     * @var array The base units that are used as multipliers for smallest possible unit from [[measureUnits]].
364
     * @since 2.0.13
365
     */
366
    public $baseUnits = [
367
        self::UNIT_LENGTH => [
368
            self::UNIT_SYSTEM_IMPERIAL => 12, // 1 feet = 12 inches
369
            self::UNIT_SYSTEM_METRIC => 1000, // 1 meter = 1000 millimeters
370
        ],
371
        self::UNIT_WEIGHT => [
372
            self::UNIT_SYSTEM_IMPERIAL => 7000, // 1 pound = 7000 grains
373
            self::UNIT_SYSTEM_METRIC => 1000, // 1 kilogram = 1000 grams
374
        ],
375
    ];
376
377
    /**
378
     * @var bool whether the [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) is loaded.
379
     */
380
    private $_intlLoaded = false;
381
    /**
382
     * @var \ResourceBundle cached ResourceBundle object used to read unit translations
383
     */
384
    private $_resourceBundle;
385
    /**
386
     * @var array cached unit translation patterns
387
     */
388
    private $_unitMessages = [];
389
390
391
    /**
392
     * {@inheritdoc}
393
     */
394 311
    public function init()
395
    {
396 311
        if ($this->timeZone === null) {
397 311
            $this->timeZone = Yii::$app->timeZone;
398
        }
399 311
        if ($this->locale === null) {
400 36
            $this->locale = Yii::$app->language;
401
        }
402 311
        if ($this->language === null) {
403 311
            $this->language = strtok($this->locale, '@');
404
        }
405 311
        if ($this->booleanFormat === null) {
406 311
            $this->booleanFormat = [Yii::t('yii', 'No', [], $this->language), Yii::t('yii', 'Yes', [], $this->language)];
407
        }
408 311
        if ($this->nullDisplay === null) {
409 311
            $this->nullDisplay = '<span class="not-set">' . Yii::t('yii', '(not set)', [], $this->language) . '</span>';
410
        }
411 311
        $this->_intlLoaded = extension_loaded('intl');
412 311
        if (!$this->_intlLoaded) {
413 121
            if ($this->decimalSeparator === null) {
414 121
                $this->decimalSeparator = '.';
415
            }
416 121
            if ($this->thousandSeparator === null) {
417 121
                $this->thousandSeparator = ',';
418
            }
419
        }
420 311
    }
421
422
    /**
423
     * Formats the value based on the given format type.
424
     * This method will call one of the "as" methods available in this class to do the formatting.
425
     * For type "xyz", the method "asXyz" will be used. For example, if the format is "html",
426
     * then [[asHtml()]] will be used. Format names are case insensitive.
427
     * @param mixed $value the value to be formatted.
428
     * @param string|array|Closure $format the format of the value, e.g., "html", "text" or an anonymous function
429
     * returning the formatted value.
430
     *
431
     * To specify additional parameters of the formatting method, you may use an array.
432
     * The first element of the array specifies the format name, while the rest of the elements will be used as the
433
     * parameters to the formatting method. For example, a format of `['date', 'Y-m-d']` will cause the invocation
434
     * of `asDate($value, 'Y-m-d')`.
435
     *
436
     * The anonymous function signature should be: `function($value, $formatter)`,
437
     * where `$value` is the value that should be formatted and `$formatter` is an instance of the Formatter class,
438
     * which can be used to call other formatting functions.
439
     * The possibility to use an anonymous function is available since version 2.0.13.
440
     * @return string the formatting result.
441
     * @throws InvalidArgumentException if the format type is not supported by this class.
442
     */
443 11
    public function format($value, $format)
444
    {
445 11
        if ($format instanceof Closure) {
446
            return call_user_func($format, $value, $this);
447 11
        } elseif (is_array($format)) {
448 7
            if (!isset($format[0])) {
449
                throw new InvalidArgumentException('The $format array must contain at least one element.');
450
            }
451 7
            $f = $format[0];
452 7
            $format[0] = $value;
453 7
            $params = $format;
454 7
            $format = $f;
455
        } else {
456 6
            $params = [$value];
457
        }
458 11
        $method = 'as' . $format;
459 11
        if ($this->hasMethod($method)) {
460 11
            return call_user_func_array([$this, $method], $params);
461
        }
462
463 2
        throw new InvalidArgumentException("Unknown format type: $format");
464
    }
465
466
467
    // simple formats
468
469
470
    /**
471
     * Formats the value as is without any formatting.
472
     * This method simply returns back the parameter without any format.
473
     * The only exception is a `null` value which will be formatted using [[nullDisplay]].
474
     * @param mixed $value the value to be formatted.
475
     * @return string the formatted result.
476
     */
477 1
    public function asRaw($value)
478
    {
479 1
        if ($value === null) {
480 1
            return $this->nullDisplay;
481
        }
482
483 1
        return $value;
484
    }
485
486
    /**
487
     * Formats the value as an HTML-encoded plain text.
488
     * @param string|null $value the value to be formatted.
489
     * @return string the formatted result.
490
     */
491 5
    public function asText($value)
492
    {
493 5
        if ($value === null) {
494 2
            return $this->nullDisplay;
495
        }
496
497 5
        return Html::encode($value);
498
    }
499
500
    /**
501
     * Formats the value as an HTML-encoded plain text with newlines converted into breaks.
502
     * @param string|null $value the value to be formatted.
503
     * @return string the formatted result.
504
     */
505 1
    public function asNtext($value)
506
    {
507 1
        if ($value === null) {
508 1
            return $this->nullDisplay;
509
        }
510
511 1
        return nl2br(Html::encode($value));
512
    }
513
514
    /**
515
     * Formats the value as HTML-encoded text paragraphs.
516
     * Each text paragraph is enclosed within a `<p>` tag.
517
     * One or multiple consecutive empty lines divide two paragraphs.
518
     * @param string|null $value the value to be formatted.
519
     * @return string the formatted result.
520
     */
521 1
    public function asParagraphs($value)
522
    {
523 1
        if ($value === null) {
524 1
            return $this->nullDisplay;
525
        }
526
527 1
        return str_replace('<p></p>', '', '<p>' . preg_replace('/\R{2,}/u', "</p>\n<p>", Html::encode($value)) . '</p>');
528
    }
529
530
    /**
531
     * Formats the value as HTML text.
532
     * The value will be purified using [[HtmlPurifier]] to avoid XSS attacks.
533
     * Use [[asRaw()]] if you do not want any purification of the value.
534
     * @param string|null $value the value to be formatted.
535
     * @param array|null $config the configuration for the HTMLPurifier class.
536
     * @return string the formatted result.
537
     */
538
    public function asHtml($value, $config = null)
539
    {
540
        if ($value === null) {
541
            return $this->nullDisplay;
542
        }
543
544
        return HtmlPurifier::process($value, $config);
545
    }
546
547
    /**
548
     * Formats the value as a mailto link.
549
     * @param string|null $value the value to be formatted.
550
     * @param array $options the tag options in terms of name-value pairs. See [[Html::mailto()]].
551
     * @return string the formatted result.
552
     */
553 1
    public function asEmail($value, $options = [])
554
    {
555 1
        if ($value === null) {
556 1
            return $this->nullDisplay;
557
        }
558
559 1
        return Html::mailto(Html::encode($value), $value, $options);
560
    }
561
562
    /**
563
     * Formats the value as an image tag.
564
     * @param mixed $value the value to be formatted.
565
     * @param array $options the tag options in terms of name-value pairs. See [[Html::img()]].
566
     * @return string the formatted result.
567
     */
568 1
    public function asImage($value, $options = [])
569
    {
570 1
        if ($value === null) {
571 1
            return $this->nullDisplay;
572
        }
573
574 1
        return Html::img($value, $options);
575
    }
576
577
    /**
578
     * Formats the value as a hyperlink.
579
     * @param mixed $value the value to be formatted.
580
     * @param array $options the tag options in terms of name-value pairs. See [[Html::a()]].
581
     * @return string the formatted result.
582
     */
583 1
    public function asUrl($value, $options = [])
584
    {
585 1
        if ($value === null) {
586 1
            return $this->nullDisplay;
587
        }
588 1
        $url = $value;
589 1
        if (strpos($url, '://') === false) {
590 1
            $url = 'http://' . $url;
591
        }
592
593 1
        return Html::a(Html::encode($value), $url, $options);
594
    }
595
596
    /**
597
     * Formats the value as a boolean.
598
     * @param mixed $value the value to be formatted.
599
     * @return string the formatted result.
600
     * @see booleanFormat
601
     */
602 1
    public function asBoolean($value)
603
    {
604 1
        if ($value === null) {
605 1
            return $this->nullDisplay;
606
        }
607
608 1
        return $value ? $this->booleanFormat[1] : $this->booleanFormat[0];
609
    }
610
611
612
    // date and time formats
613
614
615
    /**
616
     * Formats the value as a date.
617
     * @param int|string|DateTime|DateTimeInterface $value the value to be formatted. The following
618
     * types of value are supported:
619
     *
620
     * - an integer representing a UNIX timestamp. A UNIX timestamp is always in UTC by its definition.
621
     * - a string that can be [parsed to create a DateTime object](https://secure.php.net/manual/en/datetime.formats.php).
622
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
623
     * - a PHP [DateTime](https://secure.php.net/manual/en/class.datetime.php) object. You may set the time zone
624
     *   for the DateTime object to specify the source time zone.
625
     *
626
     * The formatter will convert date values according to [[timeZone]] before formatting it.
627
     * If no timezone conversion should be performed, you need to set [[defaultTimeZone]] and [[timeZone]] to the same value.
628
     * Also no conversion will be performed on values that have no time information, e.g. `"2017-06-05"`.
629
     *
630
     * @param string|null $format the format used to convert the value into a date string.
631
     * If null, [[dateFormat]] will be used.
632
     *
633
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
634
     * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime).
635
     *
636
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
637
     * PHP [date()](https://secure.php.net/manual/en/function.date.php)-function.
638
     *
639
     * @return string the formatted result.
640
     * @throws InvalidArgumentException if the input value can not be evaluated as a date value.
641
     * @throws InvalidConfigException if the date format is invalid.
642
     * @see dateFormat
643
     */
644 168
    public function asDate($value, $format = null)
645
    {
646 168
        if ($format === null) {
647 146
            $format = $this->dateFormat;
648
        }
649
650 168
        return $this->formatDateTimeValue($value, $format, 'date');
651
    }
652
653
    /**
654
     * Formats the value as a time.
655
     * @param int|string|DateTime|DateTimeInterface $value the value to be formatted. The following
656
     * types of value are supported:
657
     *
658
     * - an integer representing a UNIX timestamp. A UNIX timestamp is always in UTC by its definition.
659
     * - a string that can be [parsed to create a DateTime object](https://secure.php.net/manual/en/datetime.formats.php).
660
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
661
     * - a PHP [DateTime](https://secure.php.net/manual/en/class.datetime.php) object. You may set the time zone
662
     *   for the DateTime object to specify the source time zone.
663
     *
664
     * The formatter will convert date values according to [[timeZone]] before formatting it.
665
     * If no timezone conversion should be performed, you need to set [[defaultTimeZone]] and [[timeZone]] to the same value.
666
     *
667
     * @param string|null $format the format used to convert the value into a date string.
668
     * If null, [[timeFormat]] will be used.
669
     *
670
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
671
     * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime).
672
     *
673
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
674
     * PHP [date()](https://secure.php.net/manual/en/function.date.php)-function.
675
     *
676
     * @return string the formatted result.
677
     * @throws InvalidArgumentException if the input value can not be evaluated as a date value.
678
     * @throws InvalidConfigException if the date format is invalid.
679
     * @see timeFormat
680
     */
681 150
    public function asTime($value, $format = null)
682
    {
683 150
        if ($format === null) {
684 146
            $format = $this->timeFormat;
685
        }
686
687 150
        return $this->formatDateTimeValue($value, $format, 'time');
688
    }
689
690
    /**
691
     * Formats the value as a datetime.
692
     * @param int|string|DateTime|DateTimeInterface $value the value to be formatted. The following
693
     * types of value are supported:
694
     *
695
     * - an integer representing a UNIX timestamp. A UNIX timestamp is always in UTC by its definition.
696
     * - a string that can be [parsed to create a DateTime object](https://secure.php.net/manual/en/datetime.formats.php).
697
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
698
     * - a PHP [DateTime](https://secure.php.net/manual/en/class.datetime.php) object. You may set the time zone
699
     *   for the DateTime object to specify the source time zone.
700
     *
701
     * The formatter will convert date values according to [[timeZone]] before formatting it.
702
     * If no timezone conversion should be performed, you need to set [[defaultTimeZone]] and [[timeZone]] to the same value.
703
     *
704
     * @param string|null $format the format used to convert the value into a date string.
705
     * If null, [[datetimeFormat]] will be used.
706
     *
707
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
708
     * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime).
709
     *
710
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
711
     * PHP [date()](https://secure.php.net/manual/en/function.date.php)-function.
712
     *
713
     * @return string the formatted result.
714
     * @throws InvalidArgumentException if the input value can not be evaluated as a date value.
715
     * @throws InvalidConfigException if the date format is invalid.
716
     * @see datetimeFormat
717
     */
718 153
    public function asDatetime($value, $format = null)
719
    {
720 153
        if ($format === null) {
721 144
            $format = $this->datetimeFormat;
722
        }
723
724 153
        return $this->formatDateTimeValue($value, $format, 'datetime');
725
    }
726
727
    /**
728
     * @var array map of short format names to IntlDateFormatter constant values.
729
     */
730
    private $_dateFormats = [
731
        'short' => 3, // IntlDateFormatter::SHORT,
732
        'medium' => 2, // IntlDateFormatter::MEDIUM,
733
        'long' => 1, // IntlDateFormatter::LONG,
734
        'full' => 0, // IntlDateFormatter::FULL,
735
    ];
736
737
    /**
738
     * @param int|string|DateTime|DateTimeInterface $value the value to be formatted. The following
739
     * types of value are supported:
740
     *
741
     * - an integer representing a UNIX timestamp
742
     * - a string that can be [parsed to create a DateTime object](https://secure.php.net/manual/en/datetime.formats.php).
743
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
744
     * - a PHP [DateTime](https://secure.php.net/manual/en/class.datetime.php) object
745
     *
746
     * @param string $format the format used to convert the value into a date string.
747
     * @param string $type 'date', 'time', or 'datetime'.
748
     * @throws InvalidConfigException if the date format is invalid.
749
     * @return string the formatted result.
750
     */
751 179
    private function formatDateTimeValue($value, $format, $type)
752
    {
753 179
        $timeZone = $this->timeZone;
754
        // avoid time zone conversion for date-only and time-only values
755 179
        if ($type === 'date' || $type === 'time') {
756 172
            list($timestamp, $hasTimeInfo, $hasDateInfo) = $this->normalizeDatetimeValue($value, true);
757 170
            if ($type === 'date' && !$hasTimeInfo || $type === 'time' && !$hasDateInfo) {
0 ignored issues
show
Consider adding parentheses for clarity. Current Interpretation: ($type === 'date' && ! $...time' && ! $hasDateInfo, Probably Intended Meaning: $type === 'date' && (! $...ime' && ! $hasDateInfo)
Loading history...
758 170
                $timeZone = $this->defaultTimeZone;
759
            }
760
        } else {
761 153
            $timestamp = $this->normalizeDatetimeValue($value);
762
        }
763 177
        if ($timestamp === null) {
764 6
            return $this->nullDisplay;
765
        }
766
767
        // intl does not work with dates >=2038 or <=1901 on 32bit machines, fall back to PHP
768 177
        $year = $timestamp->format('Y');
769 177
        if ($this->_intlLoaded && !(PHP_INT_SIZE === 4 && ($year <= 1901 || $year >= 2038))) {
770 86
            if (strncmp($format, 'php:', 4) === 0) {
771 7
                $format = FormatConverter::convertDatePhpToIcu(substr($format, 4));
772
            }
773 86
            if (isset($this->_dateFormats[$format])) {
774 3
                if ($type === 'date') {
775 1
                    $formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], IntlDateFormatter::NONE, $timeZone, $this->calendar);
776 2
                } elseif ($type === 'time') {
777 1
                    $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, $this->_dateFormats[$format], $timeZone, $this->calendar);
778
                } else {
779 3
                    $formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], $this->_dateFormats[$format], $timeZone, $this->calendar);
780
                }
781
            } else {
782 86
                $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, IntlDateFormatter::NONE, $timeZone, $this->calendar, $format);
783
            }
784 86
            if ($formatter === null) {
785
                throw new InvalidConfigException(intl_get_error_message());
786
            }
787
            // make IntlDateFormatter work with DateTimeImmutable
788 86
            if ($timestamp instanceof \DateTimeImmutable) {
789 14
                $timestamp = new DateTime($timestamp->format(DateTime::ISO8601), $timestamp->getTimezone());
790
            }
791
792 86
            return $formatter->format($timestamp);
793
        }
794
795 91
        if (strncmp($format, 'php:', 4) === 0) {
796 13
            $format = substr($format, 4);
797
        } else {
798 85
            $format = FormatConverter::convertDateIcuToPhp($format, $type, $this->locale);
799
        }
800 91
        if ($timeZone != null) {
801 91
            if ($timestamp instanceof \DateTimeImmutable) {
802 13
                $timestamp = $timestamp->setTimezone(new DateTimeZone($timeZone));
803
            } else {
804 81
                $timestamp->setTimezone(new DateTimeZone($timeZone));
805
            }
806
        }
807
808 91
        return $timestamp->format($format);
809
    }
810
811
    /**
812
     * Normalizes the given datetime value as a DateTime object that can be taken by various date/time formatting methods.
813
     *
814
     * @param int|string|DateTime|DateTimeInterface $value the datetime value to be normalized. The following
815
     * types of value are supported:
816
     *
817
     * - an integer representing a UNIX timestamp
818
     * - a string that can be [parsed to create a DateTime object](https://secure.php.net/manual/en/datetime.formats.php).
819
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
820
     * - a PHP [DateTime](https://secure.php.net/manual/en/class.datetime.php) object
821
     *
822
     * @param bool $checkDateTimeInfo whether to also check if the date/time value has some time and date information attached.
823
     * Defaults to `false`. If `true`, the method will then return an array with the first element being the normalized
824
     * timestamp, the second a boolean indicating whether the timestamp has time information and third a boolean indicating
825
     * whether the timestamp has date information.
826
     * This parameter is available since version 2.0.1.
827
     * @return DateTime|array the normalized datetime value.
828
     * Since version 2.0.1 this may also return an array if `$checkDateTimeInfo` is true.
829
     * The first element of the array is the normalized timestamp and the second is a boolean indicating whether
830
     * the timestamp has time information or it is just a date value.
831
     * Since version 2.0.12 the array has third boolean element indicating whether the timestamp has date information
832
     * or it is just a time value.
833
     * @throws InvalidArgumentException if the input value can not be evaluated as a date value.
834
     */
835 184
    protected function normalizeDatetimeValue($value, $checkDateTimeInfo = false)
836
    {
837
        // checking for DateTime and DateTimeInterface is not redundant, DateTimeInterface is only in PHP>5.5
838 184
        if ($value === null || $value instanceof DateTime || $value instanceof DateTimeInterface) {
839
            // skip any processing
840 50
            return $checkDateTimeInfo ? [$value, true, true] : $value;
841
        }
842 144
        if (empty($value)) {
843 10
            $value = 0;
844
        }
845
        try {
846 144
            if (is_numeric($value)) { // process as unix timestamp, which is always in UTC
847 32
                $timestamp = new DateTime('@' . (int) $value, new DateTimeZone('UTC'));
848 32
                return $checkDateTimeInfo ? [$timestamp, true, true] : $timestamp;
849 117
            } 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)
850 12
                return $checkDateTimeInfo ? [$timestamp, false, true] : $timestamp;
851 105
            } 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)
852 19
                return $checkDateTimeInfo ? [$timestamp, true, true] : $timestamp;
853
            }
854
            // finally try to create a DateTime object with the value
855 90
            if ($checkDateTimeInfo) {
856 88
                $timestamp = new DateTime($value, new DateTimeZone($this->defaultTimeZone));
857 86
                $info = date_parse($value);
858
                return [
859 86
                    $timestamp,
860 86
                    !($info['hour'] === false && $info['minute'] === false && $info['second'] === false),
861 86
                    !($info['year'] === false && $info['month'] === false && $info['day'] === false && empty($info['zone'])),
862
                ];
863
            }
864
865 86
            return new DateTime($value, new DateTimeZone($this->defaultTimeZone));
866 2
        } catch (\Exception $e) {
867 2
            throw new InvalidArgumentException("'$value' is not a valid date time value: " . $e->getMessage()
868 2
                . "\n" . print_r(DateTime::getLastErrors(), true), $e->getCode(), $e);
869
        }
870
    }
871
872
    /**
873
     * Formats a date, time or datetime in a float number as UNIX timestamp (seconds since 01-01-1970).
874
     * @param int|string|DateTime|DateTimeInterface|null $value the value to be formatted. The following
875
     * types of value are supported:
876
     *
877
     * - an integer representing a UNIX timestamp
878
     * - a string that can be [parsed to create a DateTime object](https://secure.php.net/manual/en/datetime.formats.php).
879
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
880
     * - a PHP [DateTime](https://secure.php.net/manual/en/class.datetime.php) object
881
     *
882
     * @return string the formatted result.
883
     */
884 145
    public function asTimestamp($value)
885
    {
886 145
        if ($value === null) {
887 2
            return $this->nullDisplay;
888
        }
889 145
        $timestamp = $this->normalizeDatetimeValue($value);
890 145
        return number_format($timestamp->format('U'), 0, '.', '');
891
    }
892
893
    /**
894
     * Formats the value as the time interval between a date and now in human readable form.
895
     *
896
     * This method can be used in three different ways:
897
     *
898
     * 1. Using a timestamp that is relative to `now`.
899
     * 2. Using a timestamp that is relative to the `$referenceTime`.
900
     * 3. Using a `DateInterval` object.
901
     *
902
     * @param int|string|DateTime|DateTimeInterface|DateInterval $value the value to be formatted. The following
903
     * types of value are supported:
904
     *
905
     * - an integer representing a UNIX timestamp
906
     * - a string that can be [parsed to create a DateTime object](https://secure.php.net/manual/en/datetime.formats.php).
907
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
908
     * - a PHP [DateTime](https://secure.php.net/manual/en/class.datetime.php) object
909
     * - a PHP DateInterval object (a positive time interval will refer to the past, a negative one to the future)
910
     *
911
     * @param int|string|DateTime|DateTimeInterface|null $referenceTime if specified the value is used as a reference time instead of `now`
912
     * when `$value` is not a `DateInterval` object.
913
     * @return string the formatted result.
914
     * @throws InvalidArgumentException if the input value can not be evaluated as a date value.
915
     */
916 92
    public function asRelativeTime($value, $referenceTime = null)
917
    {
918 92
        if ($value === null) {
919 2
            return $this->nullDisplay;
920
        }
921
922 92
        if ($value instanceof DateInterval) {
923 2
            $interval = $value;
924
        } else {
925 92
            $timestamp = $this->normalizeDatetimeValue($value);
926
927 92
            if ($timestamp === false) {
928
                // $value is not a valid date/time value, so we try
929
                // to create a DateInterval with it
930
                try {
931
                    $interval = new DateInterval($value);
932
                } catch (\Exception $e) {
933
                    // invalid date/time and invalid interval
934
                    return $this->nullDisplay;
935
                }
936
            } else {
937 92
                $timeZone = new DateTimeZone($this->timeZone);
938
939 92
                if ($referenceTime === null) {
940
                    $dateNow = new DateTime('now', $timeZone);
941
                } else {
942 92
                    $dateNow = $this->normalizeDatetimeValue($referenceTime);
943 92
                    $dateNow->setTimezone($timeZone);
944
                }
945
946 92
                $dateThen = $timestamp->setTimezone($timeZone);
947
948 92
                $interval = $dateThen->diff($dateNow);
949
            }
950
        }
951
952 92
        if ($interval->invert) {
953 92
            if ($interval->y >= 1) {
954 2
                return Yii::t('yii', 'in {delta, plural, =1{a year} other{# years}}', ['delta' => $interval->y], $this->language);
955
            }
956 92
            if ($interval->m >= 1) {
957 2
                return Yii::t('yii', 'in {delta, plural, =1{a month} other{# months}}', ['delta' => $interval->m], $this->language);
958
            }
959 92
            if ($interval->d >= 1) {
960 2
                return Yii::t('yii', 'in {delta, plural, =1{a day} other{# days}}', ['delta' => $interval->d], $this->language);
961
            }
962 92
            if ($interval->h >= 1) {
963 92
                return Yii::t('yii', 'in {delta, plural, =1{an hour} other{# hours}}', ['delta' => $interval->h], $this->language);
964
            }
965 2
            if ($interval->i >= 1) {
966 2
                return Yii::t('yii', 'in {delta, plural, =1{a minute} other{# minutes}}', ['delta' => $interval->i], $this->language);
967
            }
968 2
            if ($interval->s == 0) {
969
                return Yii::t('yii', 'just now', [], $this->language);
970
            }
971
972 2
            return Yii::t('yii', 'in {delta, plural, =1{a second} other{# seconds}}', ['delta' => $interval->s], $this->language);
973
        }
974
975 92
        if ($interval->y >= 1) {
976 2
            return Yii::t('yii', '{delta, plural, =1{a year} other{# years}} ago', ['delta' => $interval->y], $this->language);
977
        }
978 92
        if ($interval->m >= 1) {
979 2
            return Yii::t('yii', '{delta, plural, =1{a month} other{# months}} ago', ['delta' => $interval->m], $this->language);
980
        }
981 92
        if ($interval->d >= 1) {
982 2
            return Yii::t('yii', '{delta, plural, =1{a day} other{# days}} ago', ['delta' => $interval->d], $this->language);
983
        }
984 92
        if ($interval->h >= 1) {
985 92
            return Yii::t('yii', '{delta, plural, =1{an hour} other{# hours}} ago', ['delta' => $interval->h], $this->language);
986
        }
987 2
        if ($interval->i >= 1) {
988 2
            return Yii::t('yii', '{delta, plural, =1{a minute} other{# minutes}} ago', ['delta' => $interval->i], $this->language);
989
        }
990 2
        if ($interval->s == 0) {
991 2
            return Yii::t('yii', 'just now', [], $this->language);
992
        }
993
994 2
        return Yii::t('yii', '{delta, plural, =1{a second} other{# seconds}} ago', ['delta' => $interval->s], $this->language);
995
    }
996
997
    /**
998
     * Represents the value as duration in human readable format.
999
     *
1000
     * @param DateInterval|string|int|null $value the value to be formatted. Acceptable formats:
1001
     *  - [DateInterval object](https://secure.php.net/manual/ru/class.dateinterval.php)
1002
     *  - integer - number of seconds. For example: value `131` represents `2 minutes, 11 seconds`
1003
     *  - ISO8601 duration format. For example, all of these values represent `1 day, 2 hours, 30 minutes` duration:
1004
     *    `2015-01-01T13:00:00Z/2015-01-02T13:30:00Z` - between two datetime values
1005
     *    `2015-01-01T13:00:00Z/P1D2H30M` - time interval after datetime value
1006
     *    `P1D2H30M/2015-01-02T13:30:00Z` - time interval before datetime value
1007
     *    `P1D2H30M` - simply a date interval
1008
     *    `P-1D2H30M` - a negative date interval (`-1 day, 2 hours, 30 minutes`)
1009
     *
1010
     * @param string $implodeString will be used to concatenate duration parts. Defaults to `, `.
1011
     * @param string $negativeSign will be prefixed to the formatted duration, when it is negative. Defaults to `-`.
1012
     * @return string the formatted duration.
1013
     * @since 2.0.7
1014
     */
1015 2
    public function asDuration($value, $implodeString = ', ', $negativeSign = '-')
1016
    {
1017 2
        if ($value === null) {
1018 2
            return $this->nullDisplay;
1019
        }
1020
1021 2
        if ($value instanceof DateInterval) {
1022 2
            $isNegative = $value->invert;
1023 2
            $interval = $value;
1024 2
        } elseif (is_numeric($value)) {
1025 2
            $isNegative = $value < 0;
1026 2
            $zeroDateTime = (new DateTime())->setTimestamp(0);
1027 2
            $valueDateTime = (new DateTime())->setTimestamp(abs($value));
1028 2
            $interval = $valueDateTime->diff($zeroDateTime);
1029 2
        } elseif (strncmp($value, 'P-', 2) === 0) {
1030 2
            $interval = new DateInterval('P' . substr($value, 2));
1031 2
            $isNegative = true;
1032
        } else {
1033 2
            $interval = new DateInterval($value);
1034 2
            $isNegative = $interval->invert;
1035
        }
1036
1037 2
        $parts = [];
1038 2
        if ($interval->y > 0) {
1039 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 year} other{# years}}', ['delta' => $interval->y], $this->language);
1040
        }
1041 2
        if ($interval->m > 0) {
1042 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 month} other{# months}}', ['delta' => $interval->m], $this->language);
1043
        }
1044 2
        if ($interval->d > 0) {
1045 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 day} other{# days}}', ['delta' => $interval->d], $this->language);
1046
        }
1047 2
        if ($interval->h > 0) {
1048 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 hour} other{# hours}}', ['delta' => $interval->h], $this->language);
1049
        }
1050 2
        if ($interval->i > 0) {
1051 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 minute} other{# minutes}}', ['delta' => $interval->i], $this->language);
1052
        }
1053 2
        if ($interval->s > 0) {
1054 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 second} other{# seconds}}', ['delta' => $interval->s], $this->language);
1055
        }
1056 2
        if ($interval->s === 0 && empty($parts)) {
1057 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 second} other{# seconds}}', ['delta' => $interval->s], $this->language);
1058 2
            $isNegative = false;
1059
        }
1060
1061 2
        return empty($parts) ? $this->nullDisplay : (($isNegative ? $negativeSign : '') . implode($implodeString, $parts));
1062
    }
1063
1064
1065
    // number formats
1066
1067
1068
    /**
1069
     * Formats the value as an integer number by removing any decimal digits without rounding.
1070
     *
1071
     * Since 2.0.16 numbers that are mispresented after normalization are formatted as strings using fallback function
1072
     * without [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) support. For very big numbers it's
1073
     * recommended to pass them as strings and not use scientific notation otherwise the output might be wrong.
1074
     *
1075
     * @param mixed $value the value to be formatted.
1076
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1077
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1078
     * @return string the formatted result.
1079
     * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
1080
     */
1081 8
    public function asInteger($value, $options = [], $textOptions = [])
1082
    {
1083 8
        if ($value === null) {
1084 5
            return $this->nullDisplay;
1085
        }
1086
1087 8
        $normalizedValue = $this->normalizeNumericValue($value);
1088
1089 6
        if ($this->isNormalizedValueMispresented($value, $normalizedValue)) {
1090 5
            return $this->asIntegerStringFallback((string) $value);
1091
        }
1092
1093 6
        if ($this->_intlLoaded) {
1094 5
            $f = $this->createNumberFormatter(NumberFormatter::DECIMAL, null, $options, $textOptions);
1095 5
            $f->setAttribute(NumberFormatter::FRACTION_DIGITS, 0);
1096 5
            if (($result = $f->format($normalizedValue, NumberFormatter::TYPE_INT64)) === false) {
1097
                throw new InvalidArgumentException('Formatting integer value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
1098
            }
1099
1100 5
            return $result;
1101
        }
1102
1103 1
        return number_format((int) $normalizedValue, 0, $this->decimalSeparator, $this->thousandSeparator);
1104
    }
1105
1106
    /**
1107
     * Formats the value as a decimal number.
1108
     *
1109
     * Property [[decimalSeparator]] will be used to represent the decimal point. The
1110
     * value is rounded automatically to the defined decimal digits.
1111
     *
1112
     * Since 2.0.16 numbers that are mispresented after normalization are formatted as strings using fallback function
1113
     * without [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) support. For very big numbers it's
1114
     * recommended to pass them as strings and not use scientific notation otherwise the output might be wrong.
1115
     *
1116
     * @param mixed $value the value to be formatted.
1117
     * @param int|null $decimals the number of digits after the decimal point.
1118
     * If not given, the number of digits depends in the input value and is determined based on
1119
     * `NumberFormatter::MIN_FRACTION_DIGITS` and `NumberFormatter::MAX_FRACTION_DIGITS`, which can be configured
1120
     * using [[$numberFormatterOptions]].
1121
     * If the PHP intl extension is not available, the default value is `2`.
1122
     * If you want consistent behavior between environments where intl is available and not, you should explicitly
1123
     * specify a value here.
1124
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1125
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1126
     * @return string the formatted result.
1127
     * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
1128
     * @see decimalSeparator
1129
     * @see thousandSeparator
1130
     */
1131 58
    public function asDecimal($value, $decimals = null, $options = [], $textOptions = [])
1132
    {
1133 58
        if ($value === null) {
1134 2
            return $this->nullDisplay;
1135
        }
1136
1137 58
        $normalizedValue = $this->normalizeNumericValue($value);
1138
1139 58
        if ($this->isNormalizedValueMispresented($value, $normalizedValue)) {
1140 2
            return $this->asDecimalStringFallback((string) $value, $decimals);
1141
        }
1142
1143 58
        if ($this->_intlLoaded) {
1144 50
            $f = $this->createNumberFormatter(NumberFormatter::DECIMAL, $decimals, $options, $textOptions);
1145 50
            if (($result = $f->format($normalizedValue)) === false) {
1146
                throw new InvalidArgumentException('Formatting decimal value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
1147
            }
1148
1149 50
            return $result;
1150
        }
1151
1152 8
        if ($decimals === null) {
1153 6
            $decimals = 2;
1154
        }
1155
1156 8
        return number_format($normalizedValue, $decimals, $this->decimalSeparator, $this->thousandSeparator);
1157
    }
1158
1159
    /**
1160
     * Formats the value as a percent number with "%" sign.
1161
     *
1162
     * Since 2.0.16 numbers that are mispresented after normalization are formatted as strings using fallback function
1163
     * without [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) support. For very big numbers it's
1164
     * recommended to pass them as strings and not use scientific notation otherwise the output might be wrong.
1165
     *
1166
     * @param mixed $value the value to be formatted. It must be a factor e.g. `0.75` will result in `75%`.
1167
     * @param int|null $decimals the number of digits after the decimal point.
1168
     * If not given, the number of digits depends in the input value and is determined based on
1169
     * `NumberFormatter::MIN_FRACTION_DIGITS` and `NumberFormatter::MAX_FRACTION_DIGITS`, which can be configured
1170
     * using [[$numberFormatterOptions]].
1171
     * If the PHP intl extension is not available, the default value is `0`.
1172
     * If you want consistent behavior between environments where intl is available and not, you should explicitly
1173
     * specify a value here.
1174
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1175
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1176
     * @return string the formatted result.
1177
     * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
1178
     */
1179 2
    public function asPercent($value, $decimals = null, $options = [], $textOptions = [])
1180
    {
1181 2
        if ($value === null) {
1182 2
            return $this->nullDisplay;
1183
        }
1184
1185 2
        $normalizedValue = $this->normalizeNumericValue($value);
1186
1187 2
        if ($this->isNormalizedValueMispresented($value, $normalizedValue)) {
1188 2
            return $this->asPercentStringFallback((string) $value, $decimals);
1189
        }
1190
1191 2
        if ($this->_intlLoaded) {
1192 1
            $f = $this->createNumberFormatter(NumberFormatter::PERCENT, $decimals, $options, $textOptions);
1193 1
            if (($result = $f->format($normalizedValue)) === false) {
1194
                throw new InvalidArgumentException('Formatting percent value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
1195
            }
1196
1197 1
            return $result;
1198
        }
1199
1200 1
        if ($decimals === null) {
1201 1
            $decimals = 0;
1202
        }
1203
1204 1
        $normalizedValue *= 100;
1205 1
        return number_format($normalizedValue, $decimals, $this->decimalSeparator, $this->thousandSeparator) . '%';
1206
    }
1207
1208
    /**
1209
     * Formats the value as a scientific number.
1210
     *
1211
     * @param mixed $value the value to be formatted.
1212
     * @param int|null $decimals the number of digits after the decimal point.
1213
     * If not given, the number of digits depends in the input value and is determined based on
1214
     * `NumberFormatter::MIN_FRACTION_DIGITS` and `NumberFormatter::MAX_FRACTION_DIGITS`, which can be configured
1215
     * using [[$numberFormatterOptions]].
1216
     * If the [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) is not available, the default value depends on your PHP configuration.
1217
     * If you want consistent behavior between environments where intl is available and not, you should explicitly
1218
     * specify a value here.
1219
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1220
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1221
     * @return string the formatted result.
1222
     * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
1223
     */
1224 1
    public function asScientific($value, $decimals = null, $options = [], $textOptions = [])
1225
    {
1226 1
        if ($value === null) {
1227 1
            return $this->nullDisplay;
1228
        }
1229 1
        $value = $this->normalizeNumericValue($value);
1230
1231 1
        if ($this->_intlLoaded) {
1232
            $f = $this->createNumberFormatter(NumberFormatter::SCIENTIFIC, $decimals, $options, $textOptions);
1233
            if (($result = $f->format($value)) === false) {
1234
                throw new InvalidArgumentException('Formatting scientific number value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
1235
            }
1236
1237
            return $result;
1238
        }
1239
1240 1
        if ($decimals !== null) {
1241 1
            return sprintf("%.{$decimals}E", $value);
1242
        }
1243
1244 1
        return sprintf('%.E', $value);
1245
    }
1246
1247
    /**
1248
     * Formats the value as a currency number.
1249
     *
1250
     * This function does not require the [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) to be installed
1251
     * to work, but it is highly recommended to install it to get good formatting results.
1252
     *
1253
     * Since 2.0.16 numbers that are mispresented after normalization are formatted as strings using fallback function
1254
     * without PHP intl extension support. For very big numbers it's recommended to pass them as strings and not use
1255
     * scientific notation otherwise the output might be wrong.
1256
     *
1257
     * @param mixed $value the value to be formatted.
1258
     * @param string|null $currency the 3-letter ISO 4217 currency code indicating the currency to use.
1259
     * If null, [[currencyCode]] will be used.
1260
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1261
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1262
     * @return string the formatted result.
1263
     * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
1264
     * @throws InvalidConfigException if no currency is given and [[currencyCode]] is not defined.
1265
     */
1266 5
    public function asCurrency($value, $currency = null, $options = [], $textOptions = [])
1267
    {
1268 5
        if ($value === null) {
1269 2
            return $this->nullDisplay;
1270
        }
1271
1272 5
        $normalizedValue = $this->normalizeNumericValue($value);
1273
1274 5
        if ($this->isNormalizedValueMispresented($value, $normalizedValue)) {
1275 3
            return $this->asCurrencyStringFallback((string) $value, $currency);
1276
        }
1277
1278 4
        if ($this->_intlLoaded) {
1279 3
            $currency = $currency ?: $this->currencyCode;
1280
            // currency code must be set before fraction digits
1281
            // https://secure.php.net/manual/en/numberformatter.formatcurrency.php#114376
1282 3
            if ($currency && !isset($textOptions[NumberFormatter::CURRENCY_CODE])) {
1283 3
                $textOptions[NumberFormatter::CURRENCY_CODE] = $currency;
1284
            }
1285 3
            $formatter = $this->createNumberFormatter(NumberFormatter::CURRENCY, null, $options, $textOptions);
1286 3
            if ($currency === null) {
1287 2
                $result = $formatter->format($normalizedValue);
1288
            } else {
1289 3
                $result = $formatter->formatCurrency($normalizedValue, $currency);
1290
            }
1291 3
            if ($result === false) {
1292
                throw new InvalidArgumentException('Formatting currency value failed: ' . $formatter->getErrorCode() . ' ' . $formatter->getErrorMessage());
1293
            }
1294
1295 3
            return $result;
1296
        }
1297
1298 1
        if ($currency === null) {
1299 1
            if ($this->currencyCode === null) {
1300
                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.');
1301
            }
1302 1
            $currency = $this->currencyCode;
1303
        }
1304
1305 1
        return $currency . ' ' . $this->asDecimal($normalizedValue, 2, $options, $textOptions);
1306
    }
1307
1308
    /**
1309
     * Formats the value as a number spellout.
1310
     *
1311
     * This function requires the [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) to be installed.
1312
     *
1313
     * This formatter does not work well with very big numbers.
1314
     *
1315
     * @param mixed $value the value to be formatted
1316
     * @return string the formatted result.
1317
     * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
1318
     * @throws InvalidConfigException when the [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) is not available.
1319
     */
1320 1
    public function asSpellout($value)
1321
    {
1322 1
        if ($value === null) {
1323 1
            return $this->nullDisplay;
1324
        }
1325 1
        $value = $this->normalizeNumericValue($value);
1326 1
        if ($this->_intlLoaded) {
1327 1
            $f = $this->createNumberFormatter(NumberFormatter::SPELLOUT);
1328 1
            if (($result = $f->format($value)) === false) {
1329
                throw new InvalidArgumentException('Formatting number as spellout failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
1330
            }
1331
1332 1
            return $result;
1333
        }
1334
1335
        throw new InvalidConfigException('Format as Spellout is only supported when PHP intl extension is installed.');
1336
    }
1337
1338
    /**
1339
     * Formats the value as a ordinal value of a number.
1340
     *
1341
     * This function requires the [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) to be installed.
1342
     *
1343
     * This formatter does not work well with very big numbers.
1344
     *
1345
     * @param mixed $value the value to be formatted
1346
     * @return string the formatted result.
1347
     * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
1348
     * @throws InvalidConfigException when the [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) is not available.
1349
     */
1350 2
    public function asOrdinal($value)
1351
    {
1352 2
        if ($value === null) {
1353 1
            return $this->nullDisplay;
1354
        }
1355 2
        $value = $this->normalizeNumericValue($value);
1356 2
        if ($this->_intlLoaded) {
1357 2
            $f = $this->createNumberFormatter(NumberFormatter::ORDINAL);
1358 2
            if (($result = $f->format($value)) === false) {
1359
                throw new InvalidArgumentException('Formatting number as ordinal failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
1360
            }
1361
1362 2
            return $result;
1363
        }
1364
1365
        throw new InvalidConfigException('Format as Ordinal is only supported when PHP intl extension is installed.');
1366
    }
1367
1368
    /**
1369
     * Formats the value in bytes as a size in human readable form for example `12 kB`.
1370
     *
1371
     * This is the short form of [[asSize]].
1372
     *
1373
     * If [[sizeFormatBase]] is 1024, [binary prefixes](http://en.wikipedia.org/wiki/Binary_prefix) (e.g. kibibyte/KiB, mebibyte/MiB, ...)
1374
     * are used in the formatting result.
1375
     *
1376
     * @param string|int|float|null $value value in bytes to be formatted.
1377
     * @param int|null $decimals the number of digits after the decimal point.
1378
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1379
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1380
     * @return string the formatted result.
1381
     * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
1382
     * @see sizeFormatBase
1383
     * @see asSize
1384
     */
1385 5
    public function asShortSize($value, $decimals = null, $options = [], $textOptions = [])
1386
    {
1387 5
        if ($value === null) {
1388 2
            return $this->nullDisplay;
1389
        }
1390
1391 5
        list($params, $position) = $this->formatNumber($value, $decimals, 4, $this->sizeFormatBase, $options, $textOptions);
1392
1393 5
        if ($this->sizeFormatBase == 1024) {
1394
            switch ($position) {
1395 5
                case 0:
1396 5
                    return Yii::t('yii', '{nFormatted} B', $params, $this->language);
1397 3
                case 1:
1398 3
                    return Yii::t('yii', '{nFormatted} KiB', $params, $this->language);
1399 3
                case 2:
1400 3
                    return Yii::t('yii', '{nFormatted} MiB', $params, $this->language);
1401 2
                case 3:
1402 2
                    return Yii::t('yii', '{nFormatted} GiB', $params, $this->language);
1403 2
                case 4:
1404
                    return Yii::t('yii', '{nFormatted} TiB', $params, $this->language);
1405
                default:
1406 2
                    return Yii::t('yii', '{nFormatted} PiB', $params, $this->language);
1407
            }
1408
        } else {
1409
            switch ($position) {
1410 2
                case 0:
1411 2
                    return Yii::t('yii', '{nFormatted} B', $params, $this->language);
1412 2
                case 1:
1413 2
                    return Yii::t('yii', '{nFormatted} kB', $params, $this->language);
1414 2
                case 2:
1415 2
                    return Yii::t('yii', '{nFormatted} MB', $params, $this->language);
1416 2
                case 3:
1417 2
                    return Yii::t('yii', '{nFormatted} GB', $params, $this->language);
1418 2
                case 4:
1419
                    return Yii::t('yii', '{nFormatted} TB', $params, $this->language);
1420
                default:
1421 2
                    return Yii::t('yii', '{nFormatted} PB', $params, $this->language);
1422
            }
1423
        }
1424
    }
1425
1426
    /**
1427
     * Formats the value in bytes as a size in human readable form, for example `12 kilobytes`.
1428
     *
1429
     * If [[sizeFormatBase]] is 1024, [binary prefixes](http://en.wikipedia.org/wiki/Binary_prefix) (e.g. kibibyte/KiB, mebibyte/MiB, ...)
1430
     * are used in the formatting result.
1431
     *
1432
     * @param string|int|float|null $value value in bytes to be formatted.
1433
     * @param int|null $decimals the number of digits after the decimal point.
1434
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1435
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1436
     * @return string the formatted result.
1437
     * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
1438
     * @see sizeFormatBase
1439
     * @see asShortSize
1440
     */
1441 6
    public function asSize($value, $decimals = null, $options = [], $textOptions = [])
1442
    {
1443 6
        if ($value === null) {
1444 2
            return $this->nullDisplay;
1445
        }
1446
1447 6
        list($params, $position) = $this->formatNumber($value, $decimals, 4, $this->sizeFormatBase, $options, $textOptions);
1448
1449 6
        if ($this->sizeFormatBase == 1024) {
1450
            switch ($position) {
1451 6
                case 0:
1452 6
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{byte} other{bytes}}', $params, $this->language);
1453 4
                case 1:
1454 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{kibibyte} other{kibibytes}}', $params, $this->language);
1455 4
                case 2:
1456 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{mebibyte} other{mebibytes}}', $params, $this->language);
1457 4
                case 3:
1458 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{gibibyte} other{gibibytes}}', $params, $this->language);
1459 4
                case 4:
1460
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{tebibyte} other{tebibytes}}', $params, $this->language);
1461
                default:
1462 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{pebibyte} other{pebibytes}}', $params, $this->language);
1463
            }
1464
        } else {
1465
            switch ($position) {
1466 4
                case 0:
1467 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{byte} other{bytes}}', $params, $this->language);
1468 4
                case 1:
1469 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{kilobyte} other{kilobytes}}', $params, $this->language);
1470 4
                case 2:
1471 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{megabyte} other{megabytes}}', $params, $this->language);
1472 4
                case 3:
1473 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{gigabyte} other{gigabytes}}', $params, $this->language);
1474 4
                case 4:
1475
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{terabyte} other{terabytes}}', $params, $this->language);
1476
                default:
1477 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{petabyte} other{petabytes}}', $params, $this->language);
1478
            }
1479
        }
1480
    }
1481
1482
    /**
1483
     * Formats the value as a length in human readable form for example `12 meters`.
1484
     * Check properties [[baseUnits]] if you need to change unit of value as the multiplier
1485
     * of the smallest unit and [[systemOfUnits]] to switch between [[UNIT_SYSTEM_METRIC]] or [[UNIT_SYSTEM_IMPERIAL]].
1486
     *
1487
     * @param float|int $value value to be formatted.
1488
     * @param int|null $decimals the number of digits after the decimal point.
1489
     * @param array $numberOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1490
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1491
     * @return string the formatted result.
1492
     * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
1493
     * @throws InvalidConfigException when INTL is not installed or does not contain required information.
1494
     * @see asLength
1495
     * @since 2.0.13
1496
     * @author John Was <[email protected]>
1497
     */
1498 13
    public function asLength($value, $decimals = null, $numberOptions = [], $textOptions = [])
1499
    {
1500 13
        return $this->formatUnit(self::UNIT_LENGTH, self::FORMAT_WIDTH_LONG, $value, null, null, $decimals, $numberOptions, $textOptions);
1501
    }
1502
1503
    /**
1504
     * Formats the value as a length in human readable form for example `12 m`.
1505
     * This is the short form of [[asLength]].
1506
     *
1507
     * Check properties [[baseUnits]] if you need to change unit of value as the multiplier
1508
     * of the smallest unit and [[systemOfUnits]] to switch between [[UNIT_SYSTEM_METRIC]] or [[UNIT_SYSTEM_IMPERIAL]].
1509
     *
1510
     * @param float|int $value value to be formatted.
1511
     * @param int|null $decimals the number of digits after the decimal point.
1512
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1513
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1514
     * @return string the formatted result.
1515
     * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
1516
     * @throws InvalidConfigException when INTL is not installed or does not contain required information.
1517
     * @see asLength
1518
     * @since 2.0.13
1519
     * @author John Was <[email protected]>
1520
     */
1521 14
    public function asShortLength($value, $decimals = null, $options = [], $textOptions = [])
1522
    {
1523 14
        return $this->formatUnit(self::UNIT_LENGTH, self::FORMAT_WIDTH_SHORT, $value, null, null, $decimals, $options, $textOptions);
1524
    }
1525
1526
    /**
1527
     * Formats the value as a weight in human readable form for example `12 kilograms`.
1528
     * Check properties [[baseUnits]] if you need to change unit of value as the multiplier
1529
     * of the smallest unit and [[systemOfUnits]] to switch between [[UNIT_SYSTEM_METRIC]] or [[UNIT_SYSTEM_IMPERIAL]].
1530
     *
1531
     * @param float|int $value value to be formatted.
1532
     * @param int|null $decimals the number of digits after the decimal point.
1533
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1534
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1535
     * @return string the formatted result.
1536
     * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
1537
     * @throws InvalidConfigException when INTL is not installed or does not contain required information.
1538
     * @since 2.0.13
1539
     * @author John Was <[email protected]>
1540
     */
1541 14
    public function asWeight($value, $decimals = null, $options = [], $textOptions = [])
1542
    {
1543 14
        return $this->formatUnit(self::UNIT_WEIGHT, self::FORMAT_WIDTH_LONG, $value, null, null, $decimals, $options, $textOptions);
1544
    }
1545
1546
    /**
1547
     * Formats the value as a weight in human readable form for example `12 kg`.
1548
     * This is the short form of [[asWeight]].
1549
     *
1550
     * Check properties [[baseUnits]] if you need to change unit of value as the multiplier
1551
     * of the smallest unit and [[systemOfUnits]] to switch between [[UNIT_SYSTEM_METRIC]] or [[UNIT_SYSTEM_IMPERIAL]].
1552
     *
1553
     * @param float|int $value value to be formatted.
1554
     * @param int|null $decimals the number of digits after the decimal point.
1555
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1556
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1557
     * @return string the formatted result.
1558
     * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
1559
     * @throws InvalidConfigException when INTL is not installed or does not contain required information.
1560
     * @since 2.0.13
1561
     * @author John Was <[email protected]>
1562
     */
1563 13
    public function asShortWeight($value, $decimals = null, $options = [], $textOptions = [])
1564
    {
1565 13
        return $this->formatUnit(self::UNIT_WEIGHT, self::FORMAT_WIDTH_SHORT, $value, null, null, $decimals, $options, $textOptions);
1566
    }
1567
1568
    /**
1569
     * @param string $unitType one of [[UNIT_WEIGHT]], [[UNIT_LENGTH]]
1570
     * @param string $unitFormat one of [[FORMAT_WIDTH_SHORT]], [[FORMAT_WIDTH_LONG]]
1571
     * @param float|int|null $value to be formatted
1572
     * @param float $baseUnit unit of value as the multiplier of the smallest unit. When `null`, property [[baseUnits]]
1573
     * will be used to determine base unit using $unitType and $unitSystem.
1574
     * @param string $unitSystem either [[UNIT_SYSTEM_METRIC]] or [[UNIT_SYSTEM_IMPERIAL]]. When `null`, property [[systemOfUnits]] will be used.
1575
     * @param int $decimals the number of digits after the decimal point.
1576
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1577
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1578
     * @return string
1579
     * @throws InvalidConfigException when INTL is not installed or does not contain required information
1580
     */
1581 54
    private function formatUnit($unitType, $unitFormat, $value, $baseUnit, $unitSystem, $decimals, $options, $textOptions)
1582
    {
1583 54
        if ($value === null) {
1584 4
            return $this->nullDisplay;
1585
        }
1586 50
        if ($unitSystem === null) {
1587 50
            $unitSystem = $this->systemOfUnits;
1588
        }
1589 50
        if ($baseUnit === null) {
1590 50
            $baseUnit = $this->baseUnits[$unitType][$unitSystem];
1591
        }
1592
1593 50
        $multipliers = array_values($this->measureUnits[$unitType][$unitSystem]);
1594
1595 50
        list($params, $position) = $this->formatNumber(
1596 50
            $this->normalizeNumericValue($value) * $baseUnit,
1597 46
            $decimals,
1598 46
            null,
1599 46
            $multipliers,
1600 46
            $options,
1601 46
            $textOptions
1602
        );
1603
1604 46
        $message = $this->getUnitMessage($unitType, $unitFormat, $unitSystem, $position);
1605
1606 44
        return (new \MessageFormatter($this->locale, $message))->format([
1607 44
            '0' => $params['nFormatted'],
1608 44
            'n' => $params['n'],
1609
        ]);
1610
    }
1611
1612
    /**
1613
     * @param string $unitType one of [[UNIT_WEIGHT]], [[UNIT_LENGTH]]
1614
     * @param string $unitFormat one of [[FORMAT_WIDTH_SHORT]], [[FORMAT_WIDTH_LONG]]
1615
     * @param string $system either [[UNIT_SYSTEM_METRIC]] or [[UNIT_SYSTEM_IMPERIAL]]. When `null`, property [[systemOfUnits]] will be used.
1616
     * @param int $position internal position of size unit
1617
     * @return string
1618
     * @throws InvalidConfigException when INTL is not installed or does not contain required information
1619
     */
1620 46
    private function getUnitMessage($unitType, $unitFormat, $system, $position)
1621
    {
1622 46
        if (isset($this->_unitMessages[$unitType][$system][$position])) {
1623
            return $this->_unitMessages[$unitType][$system][$position];
1624
        }
1625 46
        if (!$this->_intlLoaded) {
1626 2
            throw new InvalidConfigException('Format of ' . $unitType . ' is only supported when PHP intl extension is installed.');
1627
        }
1628
1629 44
        if ($this->_resourceBundle === null) {
1630
            try {
1631 44
                $this->_resourceBundle = new \ResourceBundle($this->locale, 'ICUDATA-unit');
1632
            } catch (\IntlException $e) {
1633
                throw new InvalidConfigException('Current ICU data does not contain information about measure units. Check system requirements.');
1634
            }
1635
        }
1636 44
        $unitNames = array_keys($this->measureUnits[$unitType][$system]);
1637 44
        $bundleKey = 'units' . ($unitFormat === self::FORMAT_WIDTH_SHORT ? 'Short' : '');
1638
1639 44
        $unitBundle = $this->_resourceBundle[$bundleKey][$unitType][$unitNames[$position]];
1640 44
        if ($unitBundle === null) {
1641
            throw new InvalidConfigException('Current ICU data version does not contain information about unit type "' . $unitType . '" and unit measure "' . $unitNames[$position] . '". Check system requirements.');
1642
        }
1643
1644 44
        $message = [];
1645 44
        foreach ($unitBundle as $key => $value) {
1646 44
            if ($key === 'dnam') {
1647 44
                continue;
1648
            }
1649 44
            $message[] = "$key{{$value}}";
1650
        }
1651
1652 44
        return $this->_unitMessages[$unitType][$system][$position] = '{n, plural, ' . implode(' ', $message) . '}';
1653
    }
1654
1655
    /**
1656
     * Given the value in bytes formats number part of the human readable form.
1657
     *
1658
     * @param string|int|float $value value in bytes to be formatted.
1659
     * @param int $decimals the number of digits after the decimal point
1660
     * @param int $maxPosition maximum internal position of size unit, ignored if $formatBase is an array
1661
     * @param array|int $formatBase the base at which each next unit is calculated, either 1000 or 1024, or an array
1662
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1663
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1664
     * @return array [parameters for Yii::t containing formatted number, internal position of size unit]
1665
     * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
1666
     * @since 2.0.32
1667
     */
1668 55
    protected function formatNumber($value, $decimals, $maxPosition, $formatBase, $options, $textOptions)
1669
    {
1670 55
        $value = $this->normalizeNumericValue($value);
1671
1672 55
        $position = 0;
1673 55
        if (is_array($formatBase)) {
1674 46
            $maxPosition = count($formatBase) - 1;
1675
        }
1676
        do {
1677 55
            if (is_array($formatBase)) {
1678 46
                if (!isset($formatBase[$position + 1])) {
1679 12
                    break;
1680
                }
1681
1682 46
                if (abs($value) < $formatBase[$position + 1]) {
1683 46
                    break;
1684
                }
1685
            } else {
1686 9
                if (abs($value) < $formatBase) {
1687 9
                    break;
1688
                }
1689 7
                $value /= $formatBase;
1690
            }
1691 37
            $position++;
1692 37
        } while ($position < $maxPosition + 1);
1693
1694 55
        if (is_array($formatBase) && $position !== 0) {
1695 30
            $value /= $formatBase[$position];
1696
        }
1697
1698
        // no decimals for smallest unit
1699 55
        if ($position === 0) {
1700 25
            $decimals = 0;
1701 37
        } elseif ($decimals !== null) {
1702 10
            $value = round($value, $decimals);
1703
        }
1704
        // disable grouping for edge cases like 1023 to get 1023 B instead of 1,023 B
1705 55
        $oldThousandSeparator = $this->thousandSeparator;
1706 55
        $this->thousandSeparator = '';
1707 55
        if ($this->_intlLoaded && !isset($options[NumberFormatter::GROUPING_USED])) {
1708 49
            $options[NumberFormatter::GROUPING_USED] = false;
1709
        }
1710
        // format the size value
1711
        $params = [
1712
            // this is the unformatted number used for the plural rule
1713
            // abs() to make sure the plural rules work correctly on negative numbers, intl does not cover this
1714
            // http://english.stackexchange.com/questions/9735/is-1-singular-or-plural
1715 55
            'n' => abs($value),
1716
            // this is the formatted number used for display
1717 55
            'nFormatted' => $this->asDecimal($value, $decimals, $options, $textOptions),
1718
        ];
1719 55
        $this->thousandSeparator = $oldThousandSeparator;
1720
1721 55
        return [$params, $position];
1722
    }
1723
1724
    /**
1725
     * Normalizes a numeric input value.
1726
     *
1727
     * - everything [empty](https://secure.php.net/manual/en/function.empty.php) will result in `0`
1728
     * - a [numeric](https://secure.php.net/manual/en/function.is-numeric.php) string will be casted to float
1729
     * - everything else will be returned if it is [numeric](https://secure.php.net/manual/en/function.is-numeric.php),
1730
     *   otherwise an exception is thrown.
1731
     *
1732
     * @param mixed $value the input value
1733
     * @return float|int the normalized number value
1734
     * @throws InvalidArgumentException if the input value is not numeric.
1735
     */
1736 80
    protected function normalizeNumericValue($value)
1737
    {
1738 80
        if (empty($value)) {
1739 20
            return 0;
1740
        }
1741 76
        if (is_string($value) && is_numeric($value)) {
1742 21
            $value = (float) $value;
1743
        }
1744 76
        if (!is_numeric($value)) {
1745 6
            throw new InvalidArgumentException("'$value' is not a numeric value.");
1746
        }
1747
1748 70
        return $value;
1749
    }
1750
1751
    /**
1752
     * Creates a number formatter based on the given type and format.
1753
     *
1754
     * You may override this method to create a number formatter based on patterns.
1755
     *
1756
     * @param int $style the type of the number formatter.
1757
     * Values: NumberFormatter::DECIMAL, ::CURRENCY, ::PERCENT, ::SCIENTIFIC, ::SPELLOUT, ::ORDINAL
1758
     * ::DURATION, ::PATTERN_RULEBASED, ::DEFAULT_STYLE, ::IGNORE
1759
     * @param int|null $decimals the number of digits after the decimal point.
1760
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1761
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1762
     * @return NumberFormatter the created formatter instance
1763
     */
1764 62
    protected function createNumberFormatter($style, $decimals = null, $options = [], $textOptions = [])
1765
    {
1766 62
        $formatter = new NumberFormatter($this->locale, $style);
1767
1768
        // set text attributes
1769 62
        foreach ($this->numberFormatterTextOptions as $name => $attribute) {
1770 2
            $formatter->setTextAttribute($name, $attribute);
1771
        }
1772 62
        foreach ($textOptions as $name => $attribute) {
1773 7
            $formatter->setTextAttribute($name, $attribute);
1774
        }
1775
1776
        // set attributes
1777 62
        foreach ($this->numberFormatterOptions as $name => $value) {
1778 9
            $formatter->setAttribute($name, $value);
1779
        }
1780 62
        foreach ($options as $name => $value) {
1781 51
            $formatter->setAttribute($name, $value);
1782
        }
1783 62
        if ($decimals !== null) {
1784 26
            $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $decimals);
1785 26
            $formatter->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $decimals);
1786
        }
1787
1788
        // set symbols
1789 62
        if ($this->decimalSeparator !== null) {
1790 5
            $formatter->setSymbol(NumberFormatter::DECIMAL_SEPARATOR_SYMBOL, $this->decimalSeparator);
1791
        }
1792 62
        if ($this->currencyDecimalSeparator !== null) {
1793 1
            $formatter->setSymbol(NumberFormatter::MONETARY_SEPARATOR_SYMBOL, $this->currencyDecimalSeparator);
1794
        }
1795 62
        if ($this->thousandSeparator !== null) {
1796 51
            $formatter->setSymbol(NumberFormatter::GROUPING_SEPARATOR_SYMBOL, $this->thousandSeparator);
1797 51
            $formatter->setSymbol(NumberFormatter::MONETARY_GROUPING_SEPARATOR_SYMBOL, $this->thousandSeparator);
1798
        }
1799 62
        foreach ($this->numberFormatterSymbols as $name => $symbol) {
1800 2
            $formatter->setSymbol($name, $symbol);
1801
        }
1802
1803 62
        return $formatter;
1804
    }
1805
1806
    /**
1807
     * Checks if string representations of given value and its normalized version are different.
1808
     * @param string|float|int $value
1809
     * @param float|int $normalizedValue
1810
     * @return bool
1811
     * @since 2.0.16
1812
     */
1813 70
    protected function isNormalizedValueMispresented($value, $normalizedValue)
1814
    {
1815 70
        if (empty($value)) {
1816 18
            $value = 0;
1817
        }
1818
1819 70
        return (string) $normalizedValue !== $this->normalizeNumericStringValue((string) $value);
1820
    }
1821
1822
    /**
1823
     * Normalizes a numeric string value.
1824
     * @param string $value
1825
     * @return string the normalized number value as a string
1826
     * @since 2.0.16
1827
     */
1828 70
    protected function normalizeNumericStringValue($value)
1829
    {
1830 70
        $powerPosition = strrpos($value, 'E');
1831 70
        if ($powerPosition !== false) {
1832 4
            $valuePart = substr($value, 0, $powerPosition);
1833 4
            $powerPart = substr($value, $powerPosition + 1);
1834
        } else {
1835 70
            $powerPart = null;
1836 70
            $valuePart = $value;
1837
        }
1838
1839 70
        $separatorPosition = strrpos($valuePart, '.');
1840
1841 70
        if ($separatorPosition !== false) {
1842 39
            $integerPart = substr($valuePart, 0, $separatorPosition);
1843 39
            $fractionalPart = substr($valuePart, $separatorPosition + 1);
1844
        } else {
1845 50
            $integerPart = $valuePart;
1846 50
            $fractionalPart = null;
1847
        }
1848
1849
        // truncate insignificant zeros, keep minus
1850 70
        $integerPart = preg_replace('/^\+?(-?)0*(\d+)$/', '$1$2', $integerPart);
1851
        // for zeros only leave one zero, keep minus
1852 70
        $integerPart = preg_replace('/^\+?(-?)0*$/', '${1}0', $integerPart);
1853
1854 70
        if ($fractionalPart !== null) {
1855
            // truncate insignificant zeros
1856 39
            $fractionalPart = rtrim($fractionalPart, '0');
1857
1858 39
            if (empty($fractionalPart)) {
1859 7
                $fractionalPart = $powerPart !== null ? '0' : null;
1860
            }
1861
        }
1862
1863 70
        $normalizedValue = $integerPart;
1864 70
        if ($fractionalPart !== null) {
1865 38
            $normalizedValue .= '.' . $fractionalPart;
1866 50
        } elseif ($normalizedValue === '-0') {
1867
            $normalizedValue = '0';
1868
        }
1869
1870 70
        if ($powerPart !== null) {
1871 4
            $normalizedValue .= 'E' . $powerPart;
1872
        }
1873
1874 70
        return $normalizedValue;
1875
    }
1876
1877
    /**
1878
     * Fallback for formatting value as a decimal number.
1879
     *
1880
     * Property [[decimalSeparator]] will be used to represent the decimal point. The value is rounded automatically
1881
     * to the defined decimal digits.
1882
     *
1883
     * @param string|int|float $value the value to be formatted.
1884
     * @param int $decimals the number of digits after the decimal point. The default value is `2`.
1885
     * @return string the formatted result.
1886
     * @see decimalSeparator
1887
     * @see thousandSeparator
1888
     * @since 2.0.16
1889
     */
1890 11
    protected function asDecimalStringFallback($value, $decimals = 2)
1891
    {
1892 11
        if (empty($value)) {
1893
            $value = 0;
1894
        }
1895
1896 11
        $value = $this->normalizeNumericStringValue((string) $value);
1897
1898 11
        $separatorPosition = strrpos($value, '.');
1899
1900 11
        if ($separatorPosition !== false) {
1901 6
            $integerPart = substr($value, 0, $separatorPosition);
1902 6
            $fractionalPart = substr($value, $separatorPosition + 1);
1903
        } else {
1904 11
            $integerPart = $value;
1905 11
            $fractionalPart = null;
1906
        }
1907
1908 11
        $decimalOutput = '';
1909
1910 11
        if ($decimals === null) {
0 ignored issues
show
The condition $decimals === null is always false.
Loading history...
1911 2
            $decimals = 2;
1912
        }
1913
1914 11
        $carry = 0;
1915
1916 11
        if ($decimals > 0) {
1917 6
            $decimalSeparator = $this->decimalSeparator;
1918 6
            if ($this->decimalSeparator === null) {
1919 3
                $decimalSeparator = '.';
1920
            }
1921
1922 6
            if ($fractionalPart === null) {
1923 4
                $fractionalPart = str_repeat('0', $decimals);
1924 6
            } elseif (strlen($fractionalPart) > $decimals) {
1925 4
                $cursor = $decimals;
1926
1927
                // checking if fractional part must be rounded
1928 4
                if ((int) substr($fractionalPart, $cursor, 1) >= 5) {
1929
                    while (--$cursor >= 0) {
1930
                        $carry = 0;
1931
1932
                        $oneUp = (int) substr($fractionalPart, $cursor, 1) + 1;
1933
                        if ($oneUp === 10) {
1934
                            $oneUp = 0;
1935
                            $carry = 1;
1936
                        }
1937
1938
                        $fractionalPart = substr($fractionalPart, 0, $cursor) . $oneUp . substr($fractionalPart, $cursor + 1);
1939
1940
                        if ($carry === 0) {
1941
                            break;
1942
                        }
1943
                    }
1944
                }
1945
1946 4
                $fractionalPart = substr($fractionalPart, 0, $decimals);
1947 2
            } elseif (strlen($fractionalPart) < $decimals) {
1948 2
                $fractionalPart = str_pad($fractionalPart, $decimals, '0');
1949
            }
1950
1951 6
            $decimalOutput .= $decimalSeparator . $fractionalPart;
1952
        }
1953
1954
        // checking if integer part must be rounded
1955 11
        if ($carry || ($decimals === 0 && $fractionalPart !== null && (int) substr($fractionalPart, 0, 1) >= 5)) {
1956 4
            $integerPartLength = strlen($integerPart);
1957 4
            $cursor = 0;
1958
1959 4
            while (++$cursor <= $integerPartLength) {
1960 4
                $carry = 0;
1961
1962 4
                $oneUp = (int) substr($integerPart, -$cursor, 1) + 1;
1963 4
                if ($oneUp === 10) {
1964
                    $oneUp = 0;
1965
                    $carry = 1;
1966
                }
1967
1968 4
                $integerPart = substr($integerPart, 0, -$cursor) . $oneUp . substr($integerPart, $integerPartLength - $cursor + 1);
1969
1970 4
                if ($carry === 0) {
1971 4
                    break;
1972
                }
1973
            }
1974 4
            if ($carry === 1) {
1975
                $integerPart = '1' . $integerPart;
1976
            }
1977
        }
1978
1979 11
        if (strlen($integerPart) > 3) {
1980 11
            $thousandSeparator = $this->thousandSeparator;
1981 11
            if ($thousandSeparator === null) {
0 ignored issues
show
The condition $thousandSeparator === null is always false.
Loading history...
1982 8
                $thousandSeparator = ',';
1983
            }
1984
1985 11
            $integerPart = strrev(implode(',', str_split(strrev($integerPart), 3)));
0 ignored issues
show
It seems like str_split(strrev($integerPart), 3) can also be of type true; however, parameter $pieces of implode() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1985
            $integerPart = strrev(implode(',', /** @scrutinizer ignore-type */ str_split(strrev($integerPart), 3)));
Loading history...
1986 11
            if ($thousandSeparator !== ',') {
1987 11
                $integerPart = str_replace(',', $thousandSeparator, $integerPart);
1988
            }
1989
        }
1990
1991 11
        return $integerPart . $decimalOutput;
1992
    }
1993
1994
    /**
1995
     * Fallback for formatting value as an integer number by removing any decimal digits without rounding.
1996
     *
1997
     * @param string|int|float $value the value to be formatted.
1998
     * @return string the formatted result.
1999
     * @since 2.0.16
2000
     */
2001 5
    protected function asIntegerStringFallback($value)
2002
    {
2003 5
        if (empty($value)) {
2004
            $value = 0;
2005
        }
2006
2007 5
        $value = $this->normalizeNumericStringValue((string) $value);
2008 5
        $separatorPosition = strrpos($value, '.');
2009
2010 5
        if ($separatorPosition !== false) {
2011 5
            $integerPart = substr($value, 0, $separatorPosition);
2012
        } else {
2013 5
            $integerPart = $value;
2014
        }
2015
2016 5
        return $this->asDecimalStringFallback($integerPart, 0);
2017
    }
2018
2019
    /**
2020
     * Fallback for formatting value as a percent number with "%" sign.
2021
     *
2022
     * Property [[decimalSeparator]] will be used to represent the decimal point. The value is rounded automatically
2023
     * to the defined decimal digits.
2024
     *
2025
     * @param string|int|float $value the value to be formatted.
2026
     * @param int|null $decimals the number of digits after the decimal point. The default value is `0`.
2027
     * @return string the formatted result.
2028
     * @since 2.0.16
2029
     */
2030 2
    protected function asPercentStringFallback($value, $decimals = null)
2031
    {
2032 2
        if (empty($value)) {
2033
            $value = 0;
2034
        }
2035
2036 2
        if ($decimals === null) {
2037 2
            $decimals = 0;
2038
        }
2039
2040 2
        $value = $this->normalizeNumericStringValue((string) $value);
2041 2
        $separatorPosition = strrpos($value, '.');
2042
2043 2
        if ($separatorPosition !== false) {
2044 2
            $integerPart = substr($value, 0, $separatorPosition);
2045 2
            $fractionalPart = str_pad(substr($value, $separatorPosition + 1), 2, '0');
2046
2047 2
            $integerPart .= substr($fractionalPart, 0, 2);
2048 2
            $fractionalPart = substr($fractionalPart, 2);
2049
2050 2
            if ($fractionalPart === '') {
2051
                $multipliedValue = $integerPart;
2052
            } else {
2053 2
                $multipliedValue = $integerPart . '.' . $fractionalPart;
2054
            }
2055
        } else {
2056 2
            $multipliedValue = $value . '00';
2057
        }
2058
2059 2
        return $this->asDecimalStringFallback($multipliedValue, $decimals) . '%';
2060
    }
2061
2062
    /**
2063
     * Fallback for formatting value as a currency number.
2064
     *
2065
     * @param string|int|float $value the value to be formatted.
2066
     * @param string|null $currency the 3-letter ISO 4217 currency code indicating the currency to use.
2067
     * If null, [[currencyCode]] will be used.
2068
     * @return string the formatted result.
2069
     * @throws InvalidConfigException if no currency is given and [[currencyCode]] is not defined.
2070
     * @since 2.0.16
2071
     */
2072 3
    protected function asCurrencyStringFallback($value, $currency = null)
2073
    {
2074 3
        if ($currency === null) {
2075 2
            if ($this->currencyCode === null) {
2076 1
                throw new InvalidConfigException('The default currency code for the formatter is not defined.');
2077
            }
2078 1
            $currency = $this->currencyCode;
2079
        }
2080
2081 2
        return $currency . ' ' . $this->asDecimalStringFallback($value, 2);
2082
    }
2083
}
2084