Issues (910)

framework/i18n/Formatter.php (1 issue)

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

2092
            $integerPart = strrev(implode(',', /** @scrutinizer ignore-type */ str_split(strrev($integerPart), 3)));
Loading history...
2093 11
            if ($thousandSeparator !== ',') {
2094 11
                $integerPart = str_replace(',', $thousandSeparator, $integerPart);
2095
            }
2096
        }
2097
2098 11
        return $integerPart . $decimalOutput;
2099
    }
2100
2101
    /**
2102
     * Fallback for formatting value as an integer number by removing any decimal digits without rounding.
2103
     *
2104
     * @param string|int|float $value the value to be formatted.
2105
     * @return string the formatted result.
2106
     * @since 2.0.16
2107
     */
2108 5
    protected function asIntegerStringFallback($value)
2109
    {
2110 5
        if (empty($value)) {
2111
            $value = 0;
2112
        }
2113
2114 5
        $value = $this->normalizeNumericStringValue((string) $value);
2115 5
        $separatorPosition = strrpos($value, '.');
2116
2117 5
        if ($separatorPosition !== false) {
2118 5
            $integerPart = substr($value, 0, $separatorPosition);
2119
        } else {
2120 5
            $integerPart = $value;
2121
        }
2122
2123 5
        return $this->asDecimalStringFallback($integerPart, 0);
2124
    }
2125
2126
    /**
2127
     * Fallback for formatting value as a percent number with "%" sign.
2128
     *
2129
     * Property [[decimalSeparator]] will be used to represent the decimal point. The value is rounded automatically
2130
     * to the defined decimal digits.
2131
     *
2132
     * @param string|int|float $value the value to be formatted.
2133
     * @param int|null $decimals the number of digits after the decimal point. The default value is `0`.
2134
     * @return string the formatted result.
2135
     * @since 2.0.16
2136
     */
2137 2
    protected function asPercentStringFallback($value, $decimals = null)
2138
    {
2139 2
        if (empty($value)) {
2140
            $value = 0;
2141
        }
2142
2143 2
        if ($decimals === null) {
2144 2
            $decimals = 0;
2145
        }
2146
2147 2
        $value = $this->normalizeNumericStringValue((string) $value);
2148 2
        $separatorPosition = strrpos($value, '.');
2149
2150 2
        if ($separatorPosition !== false) {
2151 2
            $integerPart = substr($value, 0, $separatorPosition);
2152 2
            $fractionalPart = str_pad(substr($value, $separatorPosition + 1), 2, '0');
2153
2154 2
            $integerPart .= substr($fractionalPart, 0, 2);
2155 2
            $fractionalPart = substr($fractionalPart, 2);
2156
2157 2
            if ($fractionalPart === '') {
2158
                $multipliedValue = $integerPart;
2159
            } else {
2160 2
                $multipliedValue = $integerPart . '.' . $fractionalPart;
2161
            }
2162
        } else {
2163 2
            $multipliedValue = $value . '00';
2164
        }
2165
2166 2
        return $this->asDecimalStringFallback($multipliedValue, $decimals) . '%';
2167
    }
2168
2169
    /**
2170
     * Fallback for formatting value as a currency number.
2171
     *
2172
     * @param string|int|float $value the value to be formatted.
2173
     * @param string|null $currency the 3-letter ISO 4217 currency code indicating the currency to use.
2174
     * If null, [[currencyCode]] will be used.
2175
     * @return string the formatted result.
2176
     * @throws InvalidConfigException if no currency is given and [[currencyCode]] is not defined.
2177
     * @since 2.0.16
2178
     */
2179 3
    protected function asCurrencyStringFallback($value, $currency = null)
2180
    {
2181 3
        if ($currency === null) {
2182 2
            if ($this->currencyCode === null) {
2183 1
                throw new InvalidConfigException('The default currency code for the formatter is not defined.');
2184
            }
2185 1
            $currency = $this->currencyCode;
2186
        }
2187
2188 2
        return $currency . ' ' . $this->asDecimalStringFallback($value, 2);
2189
    }
2190
}
2191