Formatter   F
last analyzed

Complexity

Total Complexity 281

Size/Duplication

Total Lines 2135
Duplicated Lines 0 %

Test Coverage

Coverage 95.7%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 713
dl 0
loc 2135
ccs 645
cts 674
cp 0.957
rs 1.887
c 2
b 0
f 0
wmc 281

46 Methods

Rating   Name   Duplication   Size   Complexity  
A asTime() 0 7 2
A asRaw() 0 7 2
A asInteger() 0 23 5
A asShortWeight() 0 3 1
A asOrdinal() 0 16 4
A format() 0 22 5
A asIntegerStringFallback() 0 16 3
A asNtext() 0 7 2
A asBoolean() 0 7 3
D createNumberFormatter() 0 40 10
C asRelativeTime() 0 66 17
B asCurrency() 0 40 11
A asDate() 0 7 2
A asLength() 0 3 1
A asDecimal() 0 26 6
F asDuration() 0 47 15
A asImage() 0 7 2
B normalizeNumericStringValue() 0 47 9
A asText() 0 7 2
B init() 0 24 9
A asShortLength() 0 3 1
A asEmail() 0 7 2
A asPercent() 0 27 6
A asTimestamp() 0 7 2
A isNormalizedValueMispresented() 0 7 2
A asCurrencyStringFallback() 0 10 3
A asParagraphs() 0 7 2
A asSpellout() 0 16 4
A normalizeNumericValue() 0 13 5
F asDecimalStringFallback() 0 102 24
A asScientific() 0 21 5
A asPercentStringFallback() 0 30 5
A asHtml() 0 7 2
A asDatetime() 0 7 2
A asUrl() 0 16 4
A asWeight() 0 3 1
B getUnitMessage() 0 36 9
A setFormatterIntAttribute() 0 19 4
C formatNumber() 0 54 13
C asShortSize() 0 37 13
D formatDateTimeValue() 0 81 20
A formatUnit() 0 22 2
A setFormatterTextAttribute() 0 19 4
C asSize() 0 37 13
D normalizeDatetimeValue() 0 50 19
A setFormatterSymbol() 0 14 3

How to fix   Complexity   

Complex Class

Complex classes like Formatter often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

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

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

1
<?php
2
3
/**
4
 * @link https://www.yiiframework.com/
5
 * @copyright Copyright (c) 2008 Yii Software LLC
6
 * @license https://www.yiiframework.com/license/
7
 */
8
9
namespace yii\i18n;
10
11
use Closure;
12
use DateInterval;
13
use DateTime;
14
use DateTimeInterface;
15
use DateTimeZone;
16
use IntlDateFormatter;
17
use NumberFormatter;
18
use Yii;
19
use yii\base\Component;
20
use yii\base\InvalidArgumentException;
21
use yii\base\InvalidConfigException;
22
use yii\helpers\ArrayHelper;
23
use yii\helpers\FormatConverter;
24
use yii\helpers\Html;
25
use yii\helpers\HtmlPurifier;
26
use yii\helpers\Url;
27
28
/**
29
 * Formatter provides a set of commonly used data formatting methods.
30
 *
31
 * The formatting methods provided by Formatter are all named in the form of `asXyz()`.
32
 * The behavior of some of them may be configured via the properties of Formatter. For example,
33
 * by configuring [[dateFormat]], one may control how [[asDate()]] formats the value into a date string.
34
 *
35
 * Formatter is configured as an application component in [[\yii\base\Application]] by default.
36
 * You can access that instance via `Yii::$app->formatter`.
37
 *
38
 * The Formatter class is designed to format values according to a [[locale]]. For this feature to work
39
 * the [PHP intl extension](https://www.php.net/manual/en/book.intl.php) has to be installed.
40
 * Most of the methods however work also if the PHP intl extension is not installed by providing
41
 * a fallback implementation. Without intl month and day names are in English only.
42
 * Note that even if the intl extension is installed, formatting date and time values for years >=2038 or <=1901
43
 * on 32bit systems will fall back to the PHP implementation because intl uses a 32bit UNIX timestamp internally.
44
 * On a 64bit system the intl formatter is used in all cases if installed.
45
 *
46
 * > Note: The Formatter class is meant to be used for formatting values for display to users in different
47
 * > languages and time zones. If you need to format a date or time in machine readable format, use the
48
 * > PHP [date()](https://www.php.net/manual/en/function.date.php) function instead.
49
 *
50
 * @author Qiang Xue <[email protected]>
51
 * @author Enrica Ruedin <[email protected]>
52
 * @author Carsten Brandt <[email protected]>
53
 * @since 2.0
54
 */
55
class Formatter extends Component
56
{
57
    /**
58
     * @since 2.0.13
59
     */
60
    const UNIT_SYSTEM_METRIC = 'metric';
61
    /**
62
     * @since 2.0.13
63
     */
64
    const UNIT_SYSTEM_IMPERIAL = 'imperial';
65
    /**
66
     * @since 2.0.13
67
     */
68
    const FORMAT_WIDTH_LONG = 'long';
69
    /**
70
     * @since 2.0.13
71
     */
72
    const FORMAT_WIDTH_SHORT = 'short';
73
    /**
74
     * @since 2.0.13
75
     */
76
    const UNIT_LENGTH = 'length';
77
    /**
78
     * @since 2.0.13
79
     */
80
    const UNIT_WEIGHT = 'mass';
81
82
    /**
83
     * @var string|null the text to be displayed when formatting a `null` value.
84
     * Defaults to `'<span class="not-set">(not set)</span>'`, where `(not set)`
85
     * will be translated according to [[locale]].
86
     */
87
    public $nullDisplay;
88
    /**
89
     * @var array the text to be displayed when formatting a boolean value. The first element corresponds
90
     * to the text displayed for `false`, the second element for `true`.
91
     * Defaults to `['No', 'Yes']`, where `Yes` and `No`
92
     * will be translated according to [[locale]].
93
     */
94
    public $booleanFormat;
95
    /**
96
     * @var string|null the locale ID that is used to localize the date and number formatting.
97
     * For number and date formatting this is only effective when the
98
     * [PHP intl extension](https://www.php.net/manual/en/book.intl.php) is installed.
99
     * If not set, [[\yii\base\Application::language]] will be used.
100
     */
101
    public $locale;
102
    /**
103
     * @var string|null the language code (e.g. `en-US`, `en`) that is used to translate internal messages.
104
     * If not set, [[locale]] will be used (without the `@calendar` param, if included).
105
     *
106
     * @since 2.0.28
107
     */
108
    public $language;
109
    /**
110
     * @var string|null the time zone to use for formatting time and date values.
111
     *
112
     * 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)
113
     * e.g. `UTC`, `Europe/Berlin` or `America/Chicago`.
114
     * Refer to the [php manual](https://www.php.net/manual/en/timezones.php) for available time zones.
115
     * If this property is not set, [[\yii\base\Application::timeZone]] will be used.
116
     *
117
     * 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.
118
     * If you store your data in a different time zone in the database, you have to adjust [[defaultTimeZone]] accordingly.
119
     */
120
    public $timeZone;
121
    /**
122
     * @var string the time zone that is assumed for input values if they do not include a time zone explicitly.
123
     *
124
     * The value must be a valid time zone identifier, e.g. `UTC`, `Europe/Berlin` or `America/Chicago`.
125
     * Please refer to the [php manual](https://www.php.net/manual/en/timezones.php) for available time zones.
126
     *
127
     * It defaults to `UTC` so you only have to adjust this value if you store datetime values in another time zone in your database.
128
     *
129
     * Note that a UNIX timestamp is always in UTC by its definition. That means that specifying a default time zone different from
130
     * UTC has no effect on date values given as UNIX timestamp.
131
     *
132
     * @since 2.0.1
133
     */
134
    public $defaultTimeZone = 'UTC';
135
    /**
136
     * @var string the default format string to be used to format a [[asDate()|date]].
137
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
138
     *
139
     * It can also be a custom format as specified in the [ICU manual](https://unicode-org.github.io/icu/userguide/format_parse/datetime/).
140
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
141
     * PHP [date()](https://www.php.net/manual/en/function.date.php)-function.
142
     *
143
     * For example:
144
     *
145
     * ```php
146
     * 'MM/dd/yyyy' // date in ICU format
147
     * 'php:m/d/Y' // the same date in PHP format
148
     * ```
149
     */
150
    public $dateFormat = 'medium';
151
    /**
152
     * @var string the default format string to be used to format a [[asTime()|time]].
153
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
154
     *
155
     * It can also be a custom format as specified in the [ICU manual](https://unicode-org.github.io/icu/userguide/format_parse/datetime/).
156
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
157
     * PHP [date()](https://www.php.net/manual/en/function.date.php)-function.
158
     *
159
     * For example:
160
     *
161
     * ```php
162
     * 'HH:mm:ss' // time in ICU format
163
     * 'php:H:i:s' // the same time in PHP format
164
     * ```
165
     */
166
    public $timeFormat = 'medium';
167
    /**
168
     * @var string the default format string to be used to format a [[asDatetime()|date and time]].
169
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
170
     *
171
     * It can also be a custom format as specified in the [ICU manual](https://unicode-org.github.io/icu/userguide/format_parse/datetime/).
172
     *
173
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
174
     * PHP [date()](https://www.php.net/manual/en/function.date.php) function.
175
     *
176
     * For example:
177
     *
178
     * ```php
179
     * 'MM/dd/yyyy HH:mm:ss' // date and time in ICU format
180
     * 'php:m/d/Y H:i:s' // the same date and time in PHP format
181
     * ```
182
     */
183
    public $datetimeFormat = 'medium';
184
    /**
185
     * @var \IntlCalendar|int|null the calendar to be used for date formatting. The value of this property will be directly
186
     * passed to the [constructor of the `IntlDateFormatter` class](https://www.php.net/manual/en/intldateformatter.create.php).
187
     *
188
     * Defaults to `null`, which means the Gregorian calendar will be used. You may also explicitly pass the constant
189
     * `\IntlDateFormatter::GREGORIAN` for Gregorian calendar.
190
     *
191
     * To use an alternative calendar like for example the [Jalali calendar](https://en.wikipedia.org/wiki/Jalali_calendar),
192
     * set this property to `\IntlDateFormatter::TRADITIONAL`.
193
     * The calendar must then be specified in the [[locale]], for example for the persian calendar the configuration for the formatter would be:
194
     *
195
     * ```php
196
     * 'formatter' => [
197
     *     'locale' => 'fa_IR@calendar=persian',
198
     *     'calendar' => \IntlDateFormatter::TRADITIONAL,
199
     * ],
200
     * ```
201
     *
202
     * Available calendar names can be found in the [ICU manual](https://unicode-org.github.io/icu/userguide/datetime/calendar/).
203
     *
204
     * Since PHP 5.5 you may also use an instance of the [[\IntlCalendar]] class.
205
     * Check the [PHP manual](https://www.php.net/manual/en/intldateformatter.create.php) for more details.
206
     *
207
     * If the [PHP intl extension](https://www.php.net/manual/en/book.intl.php) is not available, setting this property will have no effect.
208
     *
209
     * @see https://www.php.net/manual/en/intldateformatter.create.php
210
     * @see https://www.php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants.calendartypes
211
     * @see https://www.php.net/manual/en/class.intlcalendar.php
212
     * @since 2.0.7
213
     */
214
    public $calendar;
215
    /**
216
     * @var string|null the character displayed as the decimal point when formatting a number.
217
     * If not set, the decimal separator corresponding to [[locale]] will be used.
218
     * If [PHP intl extension](https://www.php.net/manual/en/book.intl.php) is not available, the default value is '.'.
219
     */
220
    public $decimalSeparator;
221
    /**
222
     * @var string|null the character displayed as the decimal point when formatting a currency.
223
     * If not set, the currency decimal separator corresponding to [[locale]] will be used.
224
     * If [PHP intl extension](https://www.php.net/manual/en/book.intl.php) is not available, setting this property will have no effect.
225
     * @since 2.0.35
226
     */
227
    public $currencyDecimalSeparator;
228
    /**
229
     * @var string|null the character displayed as the thousands separator (also called grouping separator) character when formatting a number.
230
     * If not set, the thousand separator corresponding to [[locale]] will be used.
231
     * If [PHP intl extension](https://www.php.net/manual/en/book.intl.php) is not available, the default value is ','.
232
     */
233
    public $thousandSeparator;
234
    /**
235
     * @var array a list of name value pairs that are passed to the
236
     * intl [NumberFormatter::setAttribute()](https://www.php.net/manual/en/numberformatter.setattribute.php) method of all
237
     * the number formatter objects created by [[createNumberFormatter()]].
238
     * This property takes only effect if the [PHP intl extension](https://www.php.net/manual/en/book.intl.php) is installed.
239
     *
240
     * Please refer to the [PHP manual](https://www.php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants.unumberformatattribute)
241
     * for the possible options.
242
     *
243
     * For example to adjust the maximum and minimum value of fraction digits you can configure this property like the following:
244
     *
245
     * ```php
246
     * [
247
     *     NumberFormatter::MIN_FRACTION_DIGITS => 0,
248
     *     NumberFormatter::MAX_FRACTION_DIGITS => 2,
249
     * ]
250
     * ```
251
     */
252
    public $numberFormatterOptions = [];
253
    /**
254
     * @var array a list of name value pairs that are passed to the
255
     * intl [NumberFormatter::setTextAttribute()](https://www.php.net/manual/en/numberformatter.settextattribute.php) method of all
256
     * the number formatter objects created by [[createNumberFormatter()]].
257
     * This property takes only effect if the [PHP intl extension](https://www.php.net/manual/en/book.intl.php) is installed.
258
     *
259
     * Please refer to the [PHP manual](https://www.php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants.unumberformattextattribute)
260
     * for the possible options.
261
     *
262
     * For example to change the minus sign for negative numbers you can configure this property like the following:
263
     *
264
     * ```php
265
     * [
266
     *     NumberFormatter::NEGATIVE_PREFIX => 'MINUS',
267
     * ]
268
     * ```
269
     */
270
    public $numberFormatterTextOptions = [];
271
    /**
272
     * @var array a list of name value pairs that are passed to the
273
     * intl [NumberFormatter::setSymbol()](https://www.php.net/manual/en/numberformatter.setsymbol.php) method of all
274
     * the number formatter objects created by [[createNumberFormatter()]].
275
     * This property takes only effect if the [PHP intl extension](https://www.php.net/manual/en/book.intl.php) is installed.
276
     *
277
     * Please refer to the [PHP manual](https://www.php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants.unumberformatsymbol)
278
     * for the possible options.
279
     *
280
     * For example to choose a custom currency symbol, e.g. [U+20BD](https://unicode-table.com/en/20BD/) instead of `руб.` for Russian Ruble:
281
     *
282
     * ```php
283
     * [
284
     *     NumberFormatter::CURRENCY_SYMBOL => '₽',
285
     * ]
286
     * ```
287
     *
288
     * @since 2.0.4
289
     */
290
    public $numberFormatterSymbols = [];
291
    /**
292
     * @var string|null the 3-letter ISO 4217 currency code indicating the default currency to use for [[asCurrency]].
293
     * If not set, the currency code corresponding to [[locale]] will be used.
294
     * Note that in this case the [[locale]] has to be specified with a country code, e.g. `en-US` otherwise it
295
     * is not possible to determine the default currency.
296
     */
297
    public $currencyCode;
298
    /**
299
     * @var int the base at which a kilobyte is calculated (1000 or 1024 bytes per kilobyte), used by [[asSize]] and [[asShortSize]].
300
     * Defaults to 1024.
301
     */
302
    public $sizeFormatBase = 1024;
303
    /**
304
     * @var string default system of measure units. Defaults to [[UNIT_SYSTEM_METRIC]].
305
     * Possible values:
306
     *  - [[UNIT_SYSTEM_METRIC]]
307
     *  - [[UNIT_SYSTEM_IMPERIAL]]
308
     *
309
     * @see asLength
310
     * @see asWeight
311
     * @since 2.0.13
312
     */
313
    public $systemOfUnits = self::UNIT_SYSTEM_METRIC;
314
    /**
315
     * @var array configuration of weight and length measurement units.
316
     * This array contains the most usable measurement units, but you can change it
317
     * in case you have some special requirements.
318
     *
319
     * For example, you can add smaller measure unit:
320
     *
321
     * ```php
322
     * $this->measureUnits[self::UNIT_LENGTH][self::UNIT_SYSTEM_METRIC] = [
323
     *     'nanometer' => 0.000001
324
     * ]
325
     * ```
326
     * @see asLength
327
     * @see asWeight
328
     * @since 2.0.13
329
     */
330
    public $measureUnits = [
331
        self::UNIT_LENGTH => [
332
            self::UNIT_SYSTEM_IMPERIAL => [
333
                'inch' => 1,
334
                'foot' => 12,
335
                'yard' => 36,
336
                'chain' => 792,
337
                'furlong' => 7920,
338
                'mile' => 63360,
339
            ],
340
            self::UNIT_SYSTEM_METRIC => [
341
                'millimeter' => 1,
342
                'centimeter' => 10,
343
                'meter' => 1000,
344
                'kilometer' => 1000000,
345
            ],
346
        ],
347
        self::UNIT_WEIGHT => [
348
            self::UNIT_SYSTEM_IMPERIAL => [
349
                'grain' => 1,
350
                'drachm' => 27.34375,
351
                'ounce' => 437.5,
352
                'pound' => 7000,
353
                'stone' => 98000,
354
                'quarter' => 196000,
355
                'hundredweight' => 784000,
356
                'ton' => 15680000,
357
            ],
358
            self::UNIT_SYSTEM_METRIC => [
359
                'gram' => 1,
360
                'kilogram' => 1000,
361
                'ton' => 1000000,
362
            ],
363
        ],
364
    ];
365
    /**
366
     * @var array The base units that are used as multipliers for smallest possible unit from [[measureUnits]].
367
     * @since 2.0.13
368
     */
369
    public $baseUnits = [
370
        self::UNIT_LENGTH => [
371
            self::UNIT_SYSTEM_IMPERIAL => 12, // 1 feet = 12 inches
372
            self::UNIT_SYSTEM_METRIC => 1000, // 1 meter = 1000 millimeters
373
        ],
374
        self::UNIT_WEIGHT => [
375
            self::UNIT_SYSTEM_IMPERIAL => 7000, // 1 pound = 7000 grains
376
            self::UNIT_SYSTEM_METRIC => 1000, // 1 kilogram = 1000 grams
377
        ],
378
    ];
379
380
    /**
381
     * @var bool whether the [PHP intl extension](https://www.php.net/manual/en/book.intl.php) is loaded.
382
     */
383
    private $_intlLoaded = false;
384
    /**
385
     * @var \ResourceBundle cached ResourceBundle object used to read unit translations
386
     */
387
    private $_resourceBundle;
388
    /**
389
     * @var array cached unit translation patterns
390
     */
391
    private $_unitMessages = [];
392
393
394
    /**
395
     * {@inheritdoc}
396
     */
397 332
    public function init()
398
    {
399 332
        if ($this->timeZone === null) {
400 332
            $this->timeZone = Yii::$app->timeZone;
401
        }
402 332
        if ($this->locale === null) {
403 37
            $this->locale = Yii::$app->language;
404
        }
405 332
        if ($this->language === null) {
406 332
            $this->language = strtok($this->locale, '@');
407
        }
408 332
        if ($this->booleanFormat === null) {
409 332
            $this->booleanFormat = [Yii::t('yii', 'No', [], $this->language), Yii::t('yii', 'Yes', [], $this->language)];
410
        }
411 332
        if ($this->nullDisplay === null) {
412 332
            $this->nullDisplay = '<span class="not-set">' . Yii::t('yii', '(not set)', [], $this->language) . '</span>';
413
        }
414 332
        $this->_intlLoaded = extension_loaded('intl');
415 332
        if (!$this->_intlLoaded) {
416 127
            if ($this->decimalSeparator === null) {
417 127
                $this->decimalSeparator = '.';
418
            }
419 127
            if ($this->thousandSeparator === null) {
420 127
                $this->thousandSeparator = ',';
421
            }
422
        }
423
    }
424
425
    /**
426
     * Formats the value based on the given format type.
427
     * This method will call one of the "as" methods available in this class to do the formatting.
428
     * For type "xyz", the method "asXyz" will be used. For example, if the format is "html",
429
     * then [[asHtml()]] will be used. Format names are case insensitive.
430
     * @param mixed $value the value to be formatted.
431
     * @param string|array|Closure $format the format of the value, e.g., "html", "text" or an anonymous function
432
     * returning the formatted value.
433
     *
434
     * To specify additional parameters of the formatting method, you may use an array.
435
     * The first element of the array specifies the format name, while the rest of the elements will be used as the
436
     * parameters to the formatting method. For example, a format of `['date', 'Y-m-d']` will cause the invocation
437
     * of `asDate($value, 'Y-m-d')`.
438
     *
439
     * The anonymous function signature should be: `function($value, $formatter)`,
440
     * where `$value` is the value that should be formatted and `$formatter` is an instance of the Formatter class,
441
     * which can be used to call other formatting functions.
442
     * The possibility to use an anonymous function is available since version 2.0.13.
443
     * @return string the formatting result.
444
     * @throws InvalidArgumentException if the format type is not supported by this class.
445
     */
446 15
    public function format($value, $format)
447
    {
448 15
        if ($format instanceof Closure) {
449 1
            return $format($value, $this);
450
        }
451 14
        if (is_array($format)) {
452 9
            if (!isset($format[0])) {
453 1
                throw new InvalidArgumentException('The $format array must contain at least one element.');
454
            }
455 8
            $f = $format[0];
456 8
            $format[0] = $value;
457 8
            $params = $format;
458 8
            $format = $f;
459
        } else {
460 7
            $params = [$value];
461
        }
462 13
        $method = 'as' . $format;
463 13
        if ($this->hasMethod($method)) {
464 11
            return call_user_func_array([$this, $method], array_values($params));
465
        }
466
467 3
        throw new InvalidArgumentException("Unknown format type: $format");
468
    }
469
470
    // simple formats
471
472
    /**
473
     * Formats the value as is without any formatting.
474
     * This method simply returns back the parameter without any format.
475
     * The only exception is a `null` value which will be formatted using [[nullDisplay]].
476
     * @param mixed $value the value to be formatted.
477
     * @return string the formatted result.
478
     */
479 1
    public function asRaw($value)
480
    {
481 1
        if ($value === null) {
482 1
            return $this->nullDisplay;
483
        }
484
485 1
        return $value;
486
    }
487
488
    /**
489
     * Formats the value as an HTML-encoded plain text.
490
     * @param string|null $value the value to be formatted.
491
     * @return string the formatted result.
492
     */
493 5
    public function asText($value)
494
    {
495 5
        if ($value === null) {
496 2
            return $this->nullDisplay;
497
        }
498
499 5
        return Html::encode($value);
500
    }
501
502
    /**
503
     * Formats the value as an HTML-encoded plain text with newlines converted into breaks.
504
     * @param string|null $value the value to be formatted.
505
     * @return string the formatted result.
506
     */
507 1
    public function asNtext($value)
508
    {
509 1
        if ($value === null) {
510 1
            return $this->nullDisplay;
511
        }
512
513 1
        return nl2br(Html::encode($value));
514
    }
515
516
    /**
517
     * Formats the value as HTML-encoded text paragraphs.
518
     * Each text paragraph is enclosed within a `<p>` tag.
519
     * One or multiple consecutive empty lines divide two paragraphs.
520
     * @param string|null $value the value to be formatted.
521
     * @return string the formatted result.
522
     */
523 1
    public function asParagraphs($value)
524
    {
525 1
        if ($value === null) {
526 1
            return $this->nullDisplay;
527
        }
528
529 1
        return str_replace('<p></p>', '', '<p>' . preg_replace('/\R{2,}/u', "</p>\n<p>", Html::encode($value)) . '</p>');
530
    }
531
532
    /**
533
     * Formats the value as HTML text.
534
     * The value will be purified using [[HtmlPurifier]] to avoid XSS attacks.
535
     * Use [[asRaw()]] if you do not want any purification of the value.
536
     * @param string|null $value the value to be formatted.
537
     * @param array|null $config the configuration for the HTMLPurifier class.
538
     * @return string the formatted result.
539
     */
540 1
    public function asHtml($value, $config = null)
541
    {
542 1
        if ($value === null) {
543 1
            return $this->nullDisplay;
544
        }
545
546 1
        return HtmlPurifier::process($value, $config);
547
    }
548
549
    /**
550
     * Formats the value as a mailto link.
551
     * @param string|null $value the value to be formatted.
552
     * @param array $options the tag options in terms of name-value pairs. See [[Html::mailto()]].
553
     * @return string the formatted result.
554
     */
555 1
    public function asEmail($value, $options = [])
556
    {
557 1
        if ($value === null) {
558 1
            return $this->nullDisplay;
559
        }
560
561 1
        return Html::mailto(Html::encode($value), $value, $options);
562
    }
563
564
    /**
565
     * Formats the value as an image tag.
566
     * @param mixed $value the value to be formatted.
567
     * @param array $options the tag options in terms of name-value pairs. See [[Html::img()]].
568
     * @return string the formatted result.
569
     */
570 1
    public function asImage($value, $options = [])
571
    {
572 1
        if ($value === null) {
573 1
            return $this->nullDisplay;
574
        }
575
576 1
        return Html::img($value, $options);
577
    }
578
579
    /**
580
     * Formats the value as a hyperlink.
581
     * @param mixed $value the value to be formatted.
582
     * @param array $options the tag options in terms of name-value pairs. See [[Html::a()]]. Since 2.0.43 there is
583
     * a special option available `scheme` - if set it won't be passed to [[Html::a()]] but it will control the URL
584
     * protocol part of the link by normalizing URL and ensuring that it uses specified scheme. See [[Url::ensureScheme()]].
585
     * If `scheme` is not set the original behavior is preserved which is to add "http://" prefix when "://" string is
586
     * not found in the $value.
587
     * @return string the formatted result.
588
     */
589 1
    public function asUrl($value, $options = [])
590
    {
591 1
        if ($value === null) {
592 1
            return $this->nullDisplay;
593
        }
594 1
        $url = $value;
595 1
        $scheme = ArrayHelper::remove($options, 'scheme');
596 1
        if ($scheme === null) {
597 1
            if (strpos($url, '://') === false) {
598 1
                $url = 'http://' . $url;
599
            }
600
        } else {
601 1
            $url = Url::ensureScheme($url, $scheme);
602
        }
603
604 1
        return Html::a(Html::encode($value), $url, $options);
605
    }
606
607
    /**
608
     * Formats the value as a boolean.
609
     * @param mixed $value the value to be formatted.
610
     * @return string the formatted result.
611
     * @see booleanFormat
612
     */
613 1
    public function asBoolean($value)
614
    {
615 1
        if ($value === null) {
616 1
            return $this->nullDisplay;
617
        }
618
619 1
        return $value ? $this->booleanFormat[1] : $this->booleanFormat[0];
620
    }
621
622
    // date and time formats
623
624
    /**
625
     * Formats the value as a date.
626
     * @param int|string|DateTime|DateTimeInterface|null $value the value to be formatted. The following
627
     * types of value are supported:
628
     *
629
     * - an integer representing a UNIX timestamp. A UNIX timestamp is always in UTC by its definition.
630
     * - a string that can be [parsed to create a DateTime object](https://www.php.net/manual/en/datetime.formats.php).
631
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
632
     * - a PHP [DateTime](https://www.php.net/manual/en/class.datetime.php) object. You may set the time zone
633
     *   for the DateTime object to specify the source time zone.
634
     *
635
     * The formatter will convert date values according to [[timeZone]] before formatting it.
636
     * If no timezone conversion should be performed, you need to set [[defaultTimeZone]] and [[timeZone]] to the same value.
637
     * Also no conversion will be performed on values that have no time information, e.g. `"2017-06-05"`.
638
     *
639
     * @param string|null $format the format used to convert the value into a date string.
640
     * If null, [[dateFormat]] will be used.
641
     *
642
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
643
     * It can also be a custom format as specified in the [ICU manual](https://unicode-org.github.io/icu/userguide/format_parse/datetime/).
644
     *
645
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
646
     * PHP [date()](https://www.php.net/manual/en/function.date.php)-function.
647
     *
648
     * @return string the formatted result.
649
     * @throws InvalidArgumentException if the input value can not be evaluated as a date value.
650
     * @throws InvalidConfigException if the date format is invalid.
651
     * @see dateFormat
652
     */
653 169
    public function asDate($value, $format = null)
654
    {
655 169
        if ($format === null) {
656 146
            $format = $this->dateFormat;
657
        }
658
659 169
        return $this->formatDateTimeValue($value, $format, 'date');
660
    }
661
662
    /**
663
     * Formats the value as a time.
664
     * @param int|string|DateTime|DateTimeInterface|null $value the value to be formatted. The following
665
     * types of value are supported:
666
     *
667
     * - an integer representing a UNIX timestamp. A UNIX timestamp is always in UTC by its definition.
668
     * - a string that can be [parsed to create a DateTime object](https://www.php.net/manual/en/datetime.formats.php).
669
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
670
     * - a PHP [DateTime](https://www.php.net/manual/en/class.datetime.php) object. You may set the time zone
671
     *   for the DateTime object to specify the source time zone.
672
     *
673
     * The formatter will convert date values according to [[timeZone]] before formatting it.
674
     * If no timezone conversion should be performed, you need to set [[defaultTimeZone]] and [[timeZone]] to the same value.
675
     *
676
     * @param string|null $format the format used to convert the value into a date string.
677
     * If null, [[timeFormat]] will be used.
678
     *
679
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
680
     * It can also be a custom format as specified in the [ICU manual](https://unicode-org.github.io/icu/userguide/format_parse/datetime/).
681
     *
682
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
683
     * PHP [date()](https://www.php.net/manual/en/function.date.php)-function.
684
     *
685
     * @return string the formatted result.
686
     * @throws InvalidArgumentException if the input value can not be evaluated as a date value.
687
     * @throws InvalidConfigException if the date format is invalid.
688
     * @see timeFormat
689
     */
690 150
    public function asTime($value, $format = null)
691
    {
692 150
        if ($format === null) {
693 146
            $format = $this->timeFormat;
694
        }
695
696 150
        return $this->formatDateTimeValue($value, $format, 'time');
697
    }
698
699
    /**
700
     * Formats the value as a datetime.
701
     * @param int|string|DateTime|DateTimeInterface|null $value the value to be formatted. The following
702
     * types of value are supported:
703
     *
704
     * - an integer representing a UNIX timestamp. A UNIX timestamp is always in UTC by its definition.
705
     * - a string that can be [parsed to create a DateTime object](https://www.php.net/manual/en/datetime.formats.php).
706
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
707
     * - a PHP [DateTime](https://www.php.net/manual/en/class.datetime.php) object. You may set the time zone
708
     *   for the DateTime object to specify the source time zone.
709
     *
710
     * The formatter will convert date values according to [[timeZone]] before formatting it.
711
     * If no timezone conversion should be performed, you need to set [[defaultTimeZone]] and [[timeZone]] to the same value.
712
     *
713
     * @param string|null $format the format used to convert the value into a date string.
714
     * If null, [[datetimeFormat]] will be used.
715
     *
716
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
717
     * It can also be a custom format as specified in the [ICU manual](https://unicode-org.github.io/icu/userguide/format_parse/datetime/).
718
     *
719
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
720
     * PHP [date()](https://www.php.net/manual/en/function.date.php)-function.
721
     *
722
     * @return string the formatted result.
723
     * @throws InvalidArgumentException if the input value can not be evaluated as a date value.
724
     * @throws InvalidConfigException if the date format is invalid.
725
     * @see datetimeFormat
726
     */
727 153
    public function asDatetime($value, $format = null)
728
    {
729 153
        if ($format === null) {
730 144
            $format = $this->datetimeFormat;
731
        }
732
733 153
        return $this->formatDateTimeValue($value, $format, 'datetime');
734
    }
735
736
    /**
737
     * @var array map of short format names to IntlDateFormatter constant values.
738
     */
739
    private $_dateFormats = [
740
        'short' => 3, // IntlDateFormatter::SHORT,
741
        'medium' => 2, // IntlDateFormatter::MEDIUM,
742
        'long' => 1, // IntlDateFormatter::LONG,
743
        'full' => 0, // IntlDateFormatter::FULL,
744
    ];
745
746
    /**
747
     * @param int|string|DateTime|DateTimeInterface|null $value the value to be formatted. The following
748
     * types of value are supported:
749
     *
750
     * - an integer representing a UNIX timestamp
751
     * - a string that can be [parsed to create a DateTime object](https://www.php.net/manual/en/datetime.formats.php).
752
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
753
     * - a PHP [DateTime](https://www.php.net/manual/en/class.datetime.php) object
754
     *
755
     * @param string $format the format used to convert the value into a date string.
756
     * @param string $type 'date', 'time', or 'datetime'.
757
     * @throws InvalidConfigException if the date format is invalid.
758
     * @return string the formatted result.
759
     */
760 180
    private function formatDateTimeValue($value, $format, $type)
761
    {
762 180
        $timeZone = $this->timeZone;
763
        // avoid time zone conversion for date-only and time-only values
764 180
        if ($type === 'date' || $type === 'time') {
765 173
            list($timestamp, $hasTimeInfo, $hasDateInfo) = $this->normalizeDatetimeValue($value, true);
766 171
            if (($type === 'date' && !$hasTimeInfo) || ($type === 'time' && !$hasDateInfo)) {
767 171
                $timeZone = $this->defaultTimeZone;
768
            }
769
        } else {
770 153
            $timestamp = $this->normalizeDatetimeValue($value);
771
        }
772 178
        if ($timestamp === null) {
773 6
            return $this->nullDisplay;
774
        }
775
776
        // intl does not work with dates >=2038 or <=1901 on 32bit machines, fall back to PHP
777 178
        $year = $timestamp->format('Y');
778 178
        if ($this->_intlLoaded && !(PHP_INT_SIZE === 4 && ($year <= 1901 || $year >= 2038))) {
779 86
            if (strncmp($format, 'php:', 4) === 0) {
780 7
                $format = FormatConverter::convertDatePhpToIcu(substr($format, 4));
781
            }
782 86
            if (isset($this->_dateFormats[$format])) {
783 3
                if ($type === 'date') {
784 1
                    $formatter = new IntlDateFormatter(
785 1
                        $this->locale,
786 1
                        $this->_dateFormats[$format],
787 1
                        IntlDateFormatter::NONE,
788 1
                        $timeZone,
789 1
                        $this->calendar
790 1
                    );
791 2
                } elseif ($type === 'time') {
792 1
                    $formatter = new IntlDateFormatter(
793 1
                        $this->locale,
794 1
                        IntlDateFormatter::NONE,
795 1
                        $this->_dateFormats[$format],
796 1
                        $timeZone,
797 1
                        $this->calendar
798 1
                    );
799
                } else {
800 3
                    $formatter = new IntlDateFormatter(
801 3
                        $this->locale,
802 3
                        $this->_dateFormats[$format],
803 3
                        $this->_dateFormats[$format],
804 3
                        $timeZone,
805 3
                        $this->calendar
806 3
                    );
807
                }
808
            } else {
809 86
                $formatter = new IntlDateFormatter(
810 86
                    $this->locale,
811 86
                    IntlDateFormatter::NONE,
812 86
                    IntlDateFormatter::NONE,
813 86
                    $timeZone,
814 86
                    $this->calendar,
815 86
                    $format
816 86
                );
817
            }
818
819
            // make IntlDateFormatter work with DateTimeImmutable
820 86
            if ($timestamp instanceof \DateTimeImmutable) {
821 14
                $timestamp = new DateTime($timestamp->format(DateTime::ISO8601), $timestamp->getTimezone());
822
            }
823
824 86
            return $formatter->format($timestamp);
825
        }
826
827 92
        if (strncmp($format, 'php:', 4) === 0) {
828 14
            $format = substr($format, 4);
829
        } else {
830 85
            $format = FormatConverter::convertDateIcuToPhp($format, $type, $this->locale);
831
        }
832 92
        if ($timeZone != null) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $timeZone of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison !== instead.
Loading history...
833 92
            if ($timestamp instanceof \DateTimeImmutable) {
834 13
                $timestamp = $timestamp->setTimezone(new DateTimeZone($timeZone));
835
            } else {
836 82
                $timestamp->setTimezone(new DateTimeZone($timeZone));
837
            }
838
        }
839
840 92
        return $timestamp->format($format);
841
    }
842
843
    /**
844
     * Normalizes the given datetime value as a DateTime object that can be taken by various date/time formatting methods.
845
     *
846
     * @param int|string|DateTime|DateTimeInterface|null $value the datetime value to be normalized. The following
847
     * types of value are supported:
848
     *
849
     * - an integer representing a UNIX timestamp
850
     * - a string that can be [parsed to create a DateTime object](https://www.php.net/manual/en/datetime.formats.php).
851
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
852
     * - a PHP [DateTime](https://www.php.net/manual/en/class.datetime.php) object
853
     *
854
     * @param bool $checkDateTimeInfo whether to also check if the date/time value has some time and date information attached.
855
     * Defaults to `false`. If `true`, the method will then return an array with the first element being the normalized
856
     * timestamp, the second a boolean indicating whether the timestamp has time information and third a boolean indicating
857
     * whether the timestamp has date information.
858
     * This parameter is available since version 2.0.1.
859
     * @return DateTime|array the normalized datetime value
860
     * Since version 2.0.1 this may also return an array if `$checkDateTimeInfo` is true.
861
     * The first element of the array is the normalized timestamp and the second is a boolean indicating whether
862
     * the timestamp has time information or it is just a date value.
863
     * Since version 2.0.12 the array has third boolean element indicating whether the timestamp has date information
864
     * or it is just a time value.
865
     * @throws InvalidArgumentException if the input value can not be evaluated as a date value.
866
     */
867 185
    protected function normalizeDatetimeValue($value, $checkDateTimeInfo = false)
868
    {
869
        // checking for DateTime and DateTimeInterface is not redundant, DateTimeInterface is only in PHP>5.5
870 185
        if ($value === null || $value instanceof DateTime || $value instanceof DateTimeInterface) {
871
            // skip any processing
872 50
            return $checkDateTimeInfo ? [$value, true, true] : $value;
873
        }
874 145
        if (empty($value)) {
875 10
            $value = 0;
876
        }
877
        try {
878 145
            if (is_numeric($value)) { // process as unix timestamp, which is always in UTC
879 33
                $timestamp = new DateTime('@' . (int) $value, new DateTimeZone('UTC'));
880 33
                return $checkDateTimeInfo ? [$timestamp, true, true] : $timestamp;
881
            }
882
            if (
883 117
                ($timestamp = DateTime::createFromFormat(
884 117
                    'Y-m-d|',
885 117
                    $value,
886 117
                    new DateTimeZone($this->defaultTimeZone)
887 117
                )
888
                ) !== false
889
            ) { // try Y-m-d format (support invalid dates like 2012-13-01)
890 12
                return $checkDateTimeInfo ? [$timestamp, false, true] : $timestamp;
891
            }
892
            if (
893 105
                ($timestamp = DateTime::createFromFormat(
894 105
                    'Y-m-d H:i:s',
895 105
                    $value,
896 105
                    new DateTimeZone($this->defaultTimeZone)
897 105
                )
898
                ) !== false
899
            ) { // try Y-m-d H:i:s format (support invalid dates like 2012-13-01 12:63:12)
900 19
                return $checkDateTimeInfo ? [$timestamp, true, true] : $timestamp;
901
            }
902
            // finally try to create a DateTime object with the value
903 90
            if ($checkDateTimeInfo) {
904 88
                $timestamp = new DateTime($value, new DateTimeZone($this->defaultTimeZone));
905 86
                $info = date_parse($value);
906 86
                return [
907 86
                    $timestamp,
908 86
                    !($info['hour'] === false && $info['minute'] === false && $info['second'] === false),
909 86
                    !($info['year'] === false && $info['month'] === false && $info['day'] === false && empty($info['zone'])),
910 86
                ];
911
            }
912
913 86
            return new DateTime($value, new DateTimeZone($this->defaultTimeZone));
914 2
        } catch (\Exception $e) {
915 2
            throw new InvalidArgumentException("'$value' is not a valid date time value: " . $e->getMessage()
916 2
                . "\n" . print_r(DateTime::getLastErrors(), true), $e->getCode(), $e);
0 ignored issues
show
Bug introduced by
Are you sure print_r(DateTime::getLastErrors(), true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

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

916
                . "\n" . /** @scrutinizer ignore-type */ print_r(DateTime::getLastErrors(), true), $e->getCode(), $e);
Loading history...
917
        }
918
    }
919
920
    /**
921
     * Formats a date, time or datetime in a float number as UNIX timestamp (seconds since 01-01-1970).
922
     * @param int|string|DateTime|DateTimeInterface|null $value the value to be formatted. The following
923
     * types of value are supported:
924
     *
925
     * - an integer representing a UNIX timestamp
926
     * - a string that can be [parsed to create a DateTime object](https://www.php.net/manual/en/datetime.formats.php).
927
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
928
     * - a PHP [DateTime](https://www.php.net/manual/en/class.datetime.php) object
929
     *
930
     * @return string the formatted result.
931
     */
932 145
    public function asTimestamp($value)
933
    {
934 145
        if ($value === null) {
935 2
            return $this->nullDisplay;
936
        }
937 145
        $timestamp = $this->normalizeDatetimeValue($value);
938 145
        return number_format($timestamp->format('U'), 0, '.', '');
0 ignored issues
show
Bug introduced by
$timestamp->format('U') of type string is incompatible with the type double expected by parameter $num of number_format(). ( Ignorable by Annotation )

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

938
        return number_format(/** @scrutinizer ignore-type */ $timestamp->format('U'), 0, '.', '');
Loading history...
939
    }
940
941
    /**
942
     * Formats the value as the time interval between a date and now in human readable form.
943
     *
944
     * This method can be used in three different ways:
945
     *
946
     * 1. Using a timestamp that is relative to `now`.
947
     * 2. Using a timestamp that is relative to the `$referenceTime`.
948
     * 3. Using a `DateInterval` object.
949
     *
950
     * @param int|string|DateTime|DateTimeInterface|DateInterval|null $value the value to be formatted. The following
951
     * types of value are supported:
952
     *
953
     * - an integer representing a UNIX timestamp
954
     * - a string that can be [parsed to create a DateTime object](https://www.php.net/manual/en/datetime.formats.php).
955
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
956
     * - a PHP [DateTime](https://www.php.net/manual/en/class.datetime.php) object
957
     * - a PHP DateInterval object (a positive time interval will refer to the past, a negative one to the future)
958
     *
959
     * @param int|string|DateTime|DateTimeInterface|null $referenceTime if specified the value is used as a reference time instead of `now`
960
     * when `$value` is not a `DateInterval` object.
961
     * @return string the formatted result.
962
     * @throws InvalidArgumentException if the input value can not be evaluated as a date value.
963
     */
964 92
    public function asRelativeTime($value, $referenceTime = null)
965
    {
966 92
        if ($value === null) {
967 2
            return $this->nullDisplay;
968
        }
969
970 92
        if ($value instanceof DateInterval) {
971 2
            $interval = $value;
972
        } else {
973 92
            $timestamp = $this->normalizeDatetimeValue($value);
974 92
            $timeZone = new DateTimeZone($this->timeZone);
975
976 92
            if ($referenceTime === null) {
977 2
                $dateNow = new DateTime('now', $timeZone);
978
            } else {
979 92
                $dateNow = $this->normalizeDatetimeValue($referenceTime);
980 92
                $dateNow->setTimezone($timeZone);
981
            }
982
983 92
            $dateThen = $timestamp->setTimezone($timeZone);
984 92
            $interval = $dateThen->diff($dateNow);
985
        }
986
987 92
        if ($interval->invert) {
988 92
            if ($interval->y >= 1) {
989 2
                return Yii::t('yii', 'in {delta, plural, =1{a year} other{# years}}', ['delta' => $interval->y], $this->language);
990
            }
991 92
            if ($interval->m >= 1) {
992 2
                return Yii::t('yii', 'in {delta, plural, =1{a month} other{# months}}', ['delta' => $interval->m], $this->language);
993
            }
994 92
            if ($interval->d >= 1) {
995 2
                return Yii::t('yii', 'in {delta, plural, =1{a day} other{# days}}', ['delta' => $interval->d], $this->language);
996
            }
997 92
            if ($interval->h >= 1) {
998 92
                return Yii::t('yii', 'in {delta, plural, =1{an hour} other{# hours}}', ['delta' => $interval->h], $this->language);
999
            }
1000 2
            if ($interval->i >= 1) {
1001 2
                return Yii::t('yii', 'in {delta, plural, =1{a minute} other{# minutes}}', ['delta' => $interval->i], $this->language);
1002
            }
1003 2
            if ($interval->s == 0) {
1004 2
                return Yii::t('yii', 'just now', [], $this->language);
1005
            }
1006
1007 2
            return Yii::t('yii', 'in {delta, plural, =1{a second} other{# seconds}}', ['delta' => $interval->s], $this->language);
1008
        }
1009
1010 92
        if ($interval->y >= 1) {
1011 2
            return Yii::t('yii', '{delta, plural, =1{a year} other{# years}} ago', ['delta' => $interval->y], $this->language);
1012
        }
1013 92
        if ($interval->m >= 1) {
1014 2
            return Yii::t('yii', '{delta, plural, =1{a month} other{# months}} ago', ['delta' => $interval->m], $this->language);
1015
        }
1016 92
        if ($interval->d >= 1) {
1017 2
            return Yii::t('yii', '{delta, plural, =1{a day} other{# days}} ago', ['delta' => $interval->d], $this->language);
1018
        }
1019 92
        if ($interval->h >= 1) {
1020 92
            return Yii::t('yii', '{delta, plural, =1{an hour} other{# hours}} ago', ['delta' => $interval->h], $this->language);
1021
        }
1022 2
        if ($interval->i >= 1) {
1023 2
            return Yii::t('yii', '{delta, plural, =1{a minute} other{# minutes}} ago', ['delta' => $interval->i], $this->language);
1024
        }
1025 2
        if ($interval->s == 0) {
1026 2
            return Yii::t('yii', 'just now', [], $this->language);
1027
        }
1028
1029 2
        return Yii::t('yii', '{delta, plural, =1{a second} other{# seconds}} ago', ['delta' => $interval->s], $this->language);
1030
    }
1031
1032
    /**
1033
     * Represents the value as duration in human readable format.
1034
     *
1035
     * @param DateInterval|string|int|null $value the value to be formatted. Acceptable formats:
1036
     *  - [DateInterval object](https://www.php.net/manual/ru/class.dateinterval.php)
1037
     *  - integer - number of seconds. For example: value `131` represents `2 minutes, 11 seconds`
1038
     *  - ISO8601 duration format. For example, all of these values represent `1 day, 2 hours, 30 minutes` duration:
1039
     *    `2015-01-01T13:00:00Z/2015-01-02T13:30:00Z` - between two datetime values
1040
     *    `2015-01-01T13:00:00Z/P1D2H30M` - time interval after datetime value
1041
     *    `P1D2H30M/2015-01-02T13:30:00Z` - time interval before datetime value
1042
     *    `P1D2H30M` - simply a date interval
1043
     *    `P-1D2H30M` - a negative date interval (`-1 day, 2 hours, 30 minutes`)
1044
     *
1045
     * @param string $implodeString will be used to concatenate duration parts. Defaults to `, `.
1046
     * @param string $negativeSign will be prefixed to the formatted duration, when it is negative. Defaults to `-`.
1047
     * @return string the formatted duration.
1048
     * @since 2.0.7
1049
     */
1050 2
    public function asDuration($value, $implodeString = ', ', $negativeSign = '-')
1051
    {
1052 2
        if ($value === null) {
1053 2
            return $this->nullDisplay;
1054
        }
1055
1056 2
        if ($value instanceof DateInterval) {
1057 2
            $isNegative = $value->invert;
1058 2
            $interval = $value;
1059 2
        } elseif (is_numeric($value)) {
1060 2
            $isNegative = $value < 0;
1061 2
            $zeroDateTime = (new DateTime())->setTimestamp(0);
1062 2
            $valueDateTime = (new DateTime())->setTimestamp(abs((int) $value));
0 ignored issues
show
Bug introduced by
It seems like abs((int)$value) can also be of type double; however, parameter $timestamp of DateTime::setTimestamp() does only seem to accept integer, 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

1062
            $valueDateTime = (new DateTime())->setTimestamp(/** @scrutinizer ignore-type */ abs((int) $value));
Loading history...
1063 2
            $interval = $valueDateTime->diff($zeroDateTime);
1064 2
        } elseif (strncmp($value, 'P-', 2) === 0) {
1065 2
            $interval = new DateInterval('P' . substr($value, 2));
1066 2
            $isNegative = true;
1067
        } else {
1068 2
            $interval = new DateInterval($value);
1069 2
            $isNegative = $interval->invert;
1070
        }
1071
1072 2
        $parts = [];
1073 2
        if ($interval->y > 0) {
1074 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 year} other{# years}}', ['delta' => $interval->y], $this->language);
1075
        }
1076 2
        if ($interval->m > 0) {
1077 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 month} other{# months}}', ['delta' => $interval->m], $this->language);
1078
        }
1079 2
        if ($interval->d > 0) {
1080 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 day} other{# days}}', ['delta' => $interval->d], $this->language);
1081
        }
1082 2
        if ($interval->h > 0) {
1083 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 hour} other{# hours}}', ['delta' => $interval->h], $this->language);
1084
        }
1085 2
        if ($interval->i > 0) {
1086 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 minute} other{# minutes}}', ['delta' => $interval->i], $this->language);
1087
        }
1088 2
        if ($interval->s > 0) {
1089 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 second} other{# seconds}}', ['delta' => $interval->s], $this->language);
1090
        }
1091 2
        if ($interval->s === 0 && empty($parts)) {
1092 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 second} other{# seconds}}', ['delta' => $interval->s], $this->language);
1093 2
            $isNegative = false;
1094
        }
1095
1096 2
        return empty($parts) ? $this->nullDisplay : (($isNegative ? $negativeSign : '') . implode($implodeString, $parts));
1097
    }
1098
1099
1100
    // number formats
1101
1102
1103
    /**
1104
     * Formats the value as an integer number by removing any decimal digits without rounding.
1105
     *
1106
     * Since 2.0.16 numbers that are mispresented after normalization are formatted as strings using fallback function
1107
     * without [PHP intl extension](https://www.php.net/manual/en/book.intl.php) support. For very big numbers it's
1108
     * recommended to pass them as strings and not use scientific notation otherwise the output might be wrong.
1109
     *
1110
     * @param mixed $value the value to be formatted.
1111
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1112
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1113
     * @return string the formatted result.
1114
     * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
1115
     */
1116 22
    public function asInteger($value, $options = [], $textOptions = [])
1117
    {
1118 22
        if ($value === null) {
1119 5
            return $this->nullDisplay;
1120
        }
1121
1122 22
        $normalizedValue = $this->normalizeNumericValue($value);
1123
1124 20
        if ($this->isNormalizedValueMispresented($value, $normalizedValue)) {
1125 5
            return $this->asIntegerStringFallback((string) $value);
1126
        }
1127
1128 20
        if ($this->_intlLoaded) {
1129 19
            $f = $this->createNumberFormatter(NumberFormatter::DECIMAL, null, $options, $textOptions);
1130 5
            $f->setAttribute(NumberFormatter::FRACTION_DIGITS, 0);
1131 5
            if (($result = $f->format($normalizedValue, NumberFormatter::TYPE_INT64)) === false) {
1132
                throw new InvalidArgumentException('Formatting integer value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
1133
            }
1134
1135 5
            return $result;
1136
        }
1137
1138 1
        return number_format((int) $normalizedValue, 0, $this->decimalSeparator, $this->thousandSeparator);
1139
    }
1140
1141
    /**
1142
     * Formats the value as a decimal number.
1143
     *
1144
     * Property [[decimalSeparator]] will be used to represent the decimal point. The
1145
     * value is rounded automatically to the defined decimal digits.
1146
     *
1147
     * Since 2.0.16 numbers that are mispresented after normalization are formatted as strings using fallback function
1148
     * without [PHP intl extension](https://www.php.net/manual/en/book.intl.php) support. For very big numbers it's
1149
     * recommended to pass them as strings and not use scientific notation otherwise the output might be wrong.
1150
     *
1151
     * @param mixed $value the value to be formatted.
1152
     * @param int|null $decimals the number of digits after the decimal point.
1153
     * If not given, the number of digits depends in the input value and is determined based on
1154
     * `NumberFormatter::MIN_FRACTION_DIGITS` and `NumberFormatter::MAX_FRACTION_DIGITS`, which can be configured
1155
     * using [[$numberFormatterOptions]].
1156
     * If the PHP intl extension is not available, the default value is `2`.
1157
     * If you want consistent behavior between environments where intl is available and not, you should explicitly
1158
     * specify a value here.
1159
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1160
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1161
     * @return string the formatted result.
1162
     * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
1163
     * @see decimalSeparator
1164
     * @see thousandSeparator
1165
     */
1166 58
    public function asDecimal($value, $decimals = null, $options = [], $textOptions = [])
1167
    {
1168 58
        if ($value === null) {
1169 2
            return $this->nullDisplay;
1170
        }
1171
1172 58
        $normalizedValue = $this->normalizeNumericValue($value);
1173
1174 58
        if ($this->isNormalizedValueMispresented($value, $normalizedValue)) {
1175 2
            return $this->asDecimalStringFallback((string) $value, $decimals);
1176
        }
1177
1178 58
        if ($this->_intlLoaded) {
1179 50
            $f = $this->createNumberFormatter(NumberFormatter::DECIMAL, $decimals, $options, $textOptions);
1180 50
            if (($result = $f->format($normalizedValue)) === false) {
1181
                throw new InvalidArgumentException('Formatting decimal value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
1182
            }
1183
1184 50
            return $result;
1185
        }
1186
1187 8
        if ($decimals === null) {
1188 6
            $decimals = 2;
1189
        }
1190
1191 8
        return number_format($normalizedValue, $decimals, $this->decimalSeparator, $this->thousandSeparator);
1192
    }
1193
1194
    /**
1195
     * Formats the value as a percent number with "%" sign.
1196
     *
1197
     * Since 2.0.16 numbers that are mispresented after normalization are formatted as strings using fallback function
1198
     * without [PHP intl extension](https://www.php.net/manual/en/book.intl.php) support. For very big numbers it's
1199
     * recommended to pass them as strings and not use scientific notation otherwise the output might be wrong.
1200
     *
1201
     * @param mixed $value the value to be formatted. It must be a factor e.g. `0.75` will result in `75%`.
1202
     * @param int|null $decimals the number of digits after the decimal point.
1203
     * If not given, the number of digits depends in the input value and is determined based on
1204
     * `NumberFormatter::MIN_FRACTION_DIGITS` and `NumberFormatter::MAX_FRACTION_DIGITS`, which can be configured
1205
     * using [[$numberFormatterOptions]].
1206
     * If the PHP intl extension is not available, the default value is `0`.
1207
     * If you want consistent behavior between environments where intl is available and not, you should explicitly
1208
     * specify a value here.
1209
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1210
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1211
     * @return string the formatted result.
1212
     * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
1213
     */
1214 2
    public function asPercent($value, $decimals = null, $options = [], $textOptions = [])
1215
    {
1216 2
        if ($value === null) {
1217 2
            return $this->nullDisplay;
1218
        }
1219
1220 2
        $normalizedValue = $this->normalizeNumericValue($value);
1221
1222 2
        if ($this->isNormalizedValueMispresented($value, $normalizedValue)) {
1223 2
            return $this->asPercentStringFallback((string) $value, $decimals);
1224
        }
1225
1226 2
        if ($this->_intlLoaded) {
1227 1
            $f = $this->createNumberFormatter(NumberFormatter::PERCENT, $decimals, $options, $textOptions);
1228 1
            if (($result = $f->format($normalizedValue)) === false) {
1229
                throw new InvalidArgumentException('Formatting percent value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
1230
            }
1231
1232 1
            return $result;
1233
        }
1234
1235 1
        if ($decimals === null) {
1236 1
            $decimals = 0;
1237
        }
1238
1239 1
        $normalizedValue *= 100;
1240 1
        return number_format($normalizedValue, $decimals, $this->decimalSeparator, $this->thousandSeparator) . '%';
1241
    }
1242
1243
    /**
1244
     * Formats the value as a scientific number.
1245
     *
1246
     * @param mixed $value the value to be formatted.
1247
     * @param int|null $decimals the number of digits after the decimal point.
1248
     * If not given, the number of digits depends in the input value and is determined based on
1249
     * `NumberFormatter::MIN_FRACTION_DIGITS` and `NumberFormatter::MAX_FRACTION_DIGITS`, which can be configured
1250
     * using [[$numberFormatterOptions]].
1251
     * If the [PHP intl extension](https://www.php.net/manual/en/book.intl.php) is not available, the default value
1252
     * depends on your PHP configuration.
1253
     * If you want consistent behavior between environments where intl is available and not, you should explicitly
1254
     * specify a value here.
1255
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1256
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1257
     * @return string the formatted result.
1258
     * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
1259
     */
1260 1
    public function asScientific($value, $decimals = null, $options = [], $textOptions = [])
1261
    {
1262 1
        if ($value === null) {
1263 1
            return $this->nullDisplay;
1264
        }
1265 1
        $value = $this->normalizeNumericValue($value);
1266
1267 1
        if ($this->_intlLoaded) {
1268
            $f = $this->createNumberFormatter(NumberFormatter::SCIENTIFIC, $decimals, $options, $textOptions);
1269
            if (($result = $f->format($value)) === false) {
1270
                throw new InvalidArgumentException('Formatting scientific number value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
1271
            }
1272
1273
            return $result;
1274
        }
1275
1276 1
        if ($decimals !== null) {
1277 1
            return sprintf("%.{$decimals}E", $value);
1278
        }
1279
1280 1
        return sprintf('%.E', $value);
1281
    }
1282
1283
    /**
1284
     * Formats the value as a currency number.
1285
     *
1286
     * This function does not require the [PHP intl extension](https://www.php.net/manual/en/book.intl.php) to be installed
1287
     * to work, but it is highly recommended to install it to get good formatting results.
1288
     *
1289
     * Since 2.0.16 numbers that are mispresented after normalization are formatted as strings using fallback function
1290
     * without PHP intl extension support. For very big numbers it's recommended to pass them as strings and not use
1291
     * scientific notation otherwise the output might be wrong.
1292
     *
1293
     * @param mixed $value the value to be formatted.
1294
     * @param string|null $currency the 3-letter ISO 4217 currency code indicating the currency to use.
1295
     * If null, [[currencyCode]] will be used.
1296
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1297
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1298
     * @return string the formatted result.
1299
     * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
1300
     * @throws InvalidConfigException if no currency is given and [[currencyCode]] is not defined.
1301
     */
1302 5
    public function asCurrency($value, $currency = null, $options = [], $textOptions = [])
1303
    {
1304 5
        if ($value === null) {
1305 2
            return $this->nullDisplay;
1306
        }
1307
1308 5
        $normalizedValue = $this->normalizeNumericValue($value);
1309
1310 5
        if ($this->isNormalizedValueMispresented($value, $normalizedValue)) {
1311 3
            return $this->asCurrencyStringFallback((string) $value, $currency);
1312
        }
1313
1314 4
        if ($this->_intlLoaded) {
1315 3
            $currency = $currency ?: $this->currencyCode;
1316
            // currency code must be set before fraction digits
1317
            // https://www.php.net/manual/en/numberformatter.formatcurrency.php#114376
1318 3
            if ($currency && !isset($textOptions[NumberFormatter::CURRENCY_CODE])) {
1319 3
                $textOptions[NumberFormatter::CURRENCY_CODE] = $currency;
1320
            }
1321 3
            $formatter = $this->createNumberFormatter(NumberFormatter::CURRENCY, null, $options, $textOptions);
1322 3
            if ($currency === null) {
1323 2
                $result = $formatter->format($normalizedValue);
1324
            } else {
1325 3
                $result = $formatter->formatCurrency($normalizedValue, $currency);
1326
            }
1327 3
            if ($result === false) {
1328
                throw new InvalidArgumentException('Formatting currency value failed: ' . $formatter->getErrorCode() . ' ' . $formatter->getErrorMessage());
1329
            }
1330
1331 3
            return $result;
1332
        }
1333
1334 1
        if ($currency === null) {
1335 1
            if ($this->currencyCode === null) {
1336
                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.');
1337
            }
1338 1
            $currency = $this->currencyCode;
1339
        }
1340
1341 1
        return $currency . ' ' . $this->asDecimal($normalizedValue, 2, $options, $textOptions);
1342
    }
1343
1344
    /**
1345
     * Formats the value as a number spellout.
1346
     *
1347
     * This function requires the [PHP intl extension](https://www.php.net/manual/en/book.intl.php) to be installed.
1348
     *
1349
     * This formatter does not work well with very big numbers.
1350
     *
1351
     * @param mixed $value the value to be formatted
1352
     * @return string the formatted result.
1353
     * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
1354
     * @throws InvalidConfigException when the [PHP intl extension](https://www.php.net/manual/en/book.intl.php) is not available.
1355
     */
1356 2
    public function asSpellout($value)
1357
    {
1358 2
        if ($value === null) {
1359 1
            return $this->nullDisplay;
1360
        }
1361 2
        $value = $this->normalizeNumericValue($value);
1362 2
        if ($this->_intlLoaded) {
1363 1
            $f = $this->createNumberFormatter(NumberFormatter::SPELLOUT);
1364 1
            if (($result = $f->format($value)) === false) {
1365
                throw new InvalidArgumentException('Formatting number as spellout failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
1366
            }
1367
1368 1
            return $result;
1369
        }
1370
1371 1
        throw new InvalidConfigException('Format as Spellout is only supported when PHP intl extension is installed.');
1372
    }
1373
1374
    /**
1375
     * Formats the value as a ordinal value of a number.
1376
     *
1377
     * This function requires the [PHP intl extension](https://www.php.net/manual/en/book.intl.php) to be installed.
1378
     *
1379
     * This formatter does not work well with very big numbers.
1380
     *
1381
     * @param mixed $value the value to be formatted
1382
     * @return string the formatted result.
1383
     * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
1384
     * @throws InvalidConfigException when the [PHP intl extension](https://www.php.net/manual/en/book.intl.php) is not available.
1385
     */
1386 2
    public function asOrdinal($value)
1387
    {
1388 2
        if ($value === null) {
1389 1
            return $this->nullDisplay;
1390
        }
1391 2
        $value = $this->normalizeNumericValue($value);
1392 2
        if ($this->_intlLoaded) {
1393 2
            $f = $this->createNumberFormatter(NumberFormatter::ORDINAL);
1394 2
            if (($result = $f->format($value)) === false) {
1395
                throw new InvalidArgumentException('Formatting number as ordinal failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
1396
            }
1397
1398 2
            return $result;
1399
        }
1400
1401
        throw new InvalidConfigException('Format as Ordinal is only supported when PHP intl extension is installed.');
1402
    }
1403
1404
    /**
1405
     * Formats the value in bytes as a size in human readable form for example `12 kB`.
1406
     *
1407
     * This is the short form of [[asSize]].
1408
     *
1409
     * If [[sizeFormatBase]] is 1024, [binary prefixes](https://en.wikipedia.org/wiki/Binary_prefix) (e.g. kibibyte/KiB, mebibyte/MiB, ...)
1410
     * are used in the formatting result.
1411
     *
1412
     * @param string|int|float|null $value value in bytes to be formatted.
1413
     * @param int|null $decimals the number of digits after the decimal point.
1414
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1415
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1416
     * @return string the formatted result.
1417
     * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
1418
     * @see sizeFormatBase
1419
     * @see asSize
1420
     */
1421 5
    public function asShortSize($value, $decimals = null, $options = [], $textOptions = [])
1422
    {
1423 5
        if ($value === null) {
1424 2
            return $this->nullDisplay;
1425
        }
1426
1427 5
        list($params, $position) = $this->formatNumber($value, $decimals, 4, $this->sizeFormatBase, $options, $textOptions);
1428
1429 5
        if ($this->sizeFormatBase == 1024) {
1430
            switch ($position) {
1431 5
                case 0:
1432 5
                    return Yii::t('yii', '{nFormatted} B', $params, $this->language);
1433 3
                case 1:
1434 3
                    return Yii::t('yii', '{nFormatted} KiB', $params, $this->language);
1435 3
                case 2:
1436 3
                    return Yii::t('yii', '{nFormatted} MiB', $params, $this->language);
1437 2
                case 3:
1438 2
                    return Yii::t('yii', '{nFormatted} GiB', $params, $this->language);
1439 2
                case 4:
1440 2
                    return Yii::t('yii', '{nFormatted} TiB', $params, $this->language);
1441
                default:
1442 2
                    return Yii::t('yii', '{nFormatted} PiB', $params, $this->language);
1443
            }
1444
        } else {
1445
            switch ($position) {
1446 2
                case 0:
1447 2
                    return Yii::t('yii', '{nFormatted} B', $params, $this->language);
1448 2
                case 1:
1449 2
                    return Yii::t('yii', '{nFormatted} kB', $params, $this->language);
1450 2
                case 2:
1451 2
                    return Yii::t('yii', '{nFormatted} MB', $params, $this->language);
1452 2
                case 3:
1453 2
                    return Yii::t('yii', '{nFormatted} GB', $params, $this->language);
1454 2
                case 4:
1455 2
                    return Yii::t('yii', '{nFormatted} TB', $params, $this->language);
1456
                default:
1457 2
                    return Yii::t('yii', '{nFormatted} PB', $params, $this->language);
1458
            }
1459
        }
1460
    }
1461
1462
    /**
1463
     * Formats the value in bytes as a size in human readable form, for example `12 kilobytes`.
1464
     *
1465
     * If [[sizeFormatBase]] is 1024, [binary prefixes](https://en.wikipedia.org/wiki/Binary_prefix) (e.g. kibibyte/KiB, mebibyte/MiB, ...)
1466
     * are used in the formatting result.
1467
     *
1468
     * @param string|int|float|null $value value in bytes to be formatted.
1469
     * @param int|null $decimals the number of digits after the decimal point.
1470
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1471
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1472
     * @return string the formatted result.
1473
     * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
1474
     * @see sizeFormatBase
1475
     * @see asShortSize
1476
     */
1477 6
    public function asSize($value, $decimals = null, $options = [], $textOptions = [])
1478
    {
1479 6
        if ($value === null) {
1480 2
            return $this->nullDisplay;
1481
        }
1482
1483 6
        list($params, $position) = $this->formatNumber($value, $decimals, 4, $this->sizeFormatBase, $options, $textOptions);
1484
1485 6
        if ($this->sizeFormatBase == 1024) {
1486
            switch ($position) {
1487 6
                case 0:
1488 6
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{byte} other{bytes}}', $params, $this->language);
1489 4
                case 1:
1490 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{kibibyte} other{kibibytes}}', $params, $this->language);
1491 4
                case 2:
1492 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{mebibyte} other{mebibytes}}', $params, $this->language);
1493 4
                case 3:
1494 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{gibibyte} other{gibibytes}}', $params, $this->language);
1495 4
                case 4:
1496 2
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{tebibyte} other{tebibytes}}', $params, $this->language);
1497
                default:
1498 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{pebibyte} other{pebibytes}}', $params, $this->language);
1499
            }
1500
        } else {
1501
            switch ($position) {
1502 4
                case 0:
1503 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{byte} other{bytes}}', $params, $this->language);
1504 4
                case 1:
1505 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{kilobyte} other{kilobytes}}', $params, $this->language);
1506 4
                case 2:
1507 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{megabyte} other{megabytes}}', $params, $this->language);
1508 4
                case 3:
1509 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{gigabyte} other{gigabytes}}', $params, $this->language);
1510 4
                case 4:
1511 2
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{terabyte} other{terabytes}}', $params, $this->language);
1512
                default:
1513 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{petabyte} other{petabytes}}', $params, $this->language);
1514
            }
1515
        }
1516
    }
1517
1518
    /**
1519
     * Formats the value as a length in human readable form for example `12 meters`.
1520
     * Check properties [[baseUnits]] if you need to change unit of value as the multiplier
1521
     * of the smallest unit and [[systemOfUnits]] to switch between [[UNIT_SYSTEM_METRIC]] or [[UNIT_SYSTEM_IMPERIAL]].
1522
     *
1523
     * @param float|int $value value to be formatted.
1524
     * @param int|null $decimals the number of digits after the decimal point.
1525
     * @param array $numberOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1526
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1527
     * @return string the formatted result.
1528
     * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
1529
     * @throws InvalidConfigException when INTL is not installed or does not contain required information.
1530
     * @see asLength
1531
     * @since 2.0.13
1532
     * @author John Was <[email protected]>
1533
     */
1534 13
    public function asLength($value, $decimals = null, $numberOptions = [], $textOptions = [])
1535
    {
1536 13
        return $this->formatUnit(self::UNIT_LENGTH, self::FORMAT_WIDTH_LONG, $value, $decimals, $numberOptions, $textOptions);
1537
    }
1538
1539
    /**
1540
     * Formats the value as a length in human readable form for example `12 m`.
1541
     * This is the short form of [[asLength]].
1542
     *
1543
     * Check properties [[baseUnits]] if you need to change unit of value as the multiplier
1544
     * of the smallest unit and [[systemOfUnits]] to switch between [[UNIT_SYSTEM_METRIC]] or [[UNIT_SYSTEM_IMPERIAL]].
1545
     *
1546
     * @param float|int $value value to be formatted.
1547
     * @param int|null $decimals the number of digits after the decimal point.
1548
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1549
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1550
     * @return string the formatted result.
1551
     * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
1552
     * @throws InvalidConfigException when INTL is not installed or does not contain required information.
1553
     * @see asLength
1554
     * @since 2.0.13
1555
     * @author John Was <[email protected]>
1556
     */
1557 14
    public function asShortLength($value, $decimals = null, $options = [], $textOptions = [])
1558
    {
1559 14
        return $this->formatUnit(self::UNIT_LENGTH, self::FORMAT_WIDTH_SHORT, $value, $decimals, $options, $textOptions);
1560
    }
1561
1562
    /**
1563
     * Formats the value as a weight in human readable form for example `12 kilograms`.
1564
     * Check properties [[baseUnits]] if you need to change unit of value as the multiplier
1565
     * of the smallest unit and [[systemOfUnits]] to switch between [[UNIT_SYSTEM_METRIC]] or [[UNIT_SYSTEM_IMPERIAL]].
1566
     *
1567
     * @param float|int $value value to be formatted.
1568
     * @param int|null $decimals the number of digits after the decimal point.
1569
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1570
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1571
     * @return string the formatted result.
1572
     * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
1573
     * @throws InvalidConfigException when INTL is not installed or does not contain required information.
1574
     * @since 2.0.13
1575
     * @author John Was <[email protected]>
1576
     */
1577 14
    public function asWeight($value, $decimals = null, $options = [], $textOptions = [])
1578
    {
1579 14
        return $this->formatUnit(self::UNIT_WEIGHT, self::FORMAT_WIDTH_LONG, $value, $decimals, $options, $textOptions);
1580
    }
1581
1582
    /**
1583
     * Formats the value as a weight in human readable form for example `12 kg`.
1584
     * This is the short form of [[asWeight]].
1585
     *
1586
     * Check properties [[baseUnits]] if you need to change unit of value as the multiplier
1587
     * of the smallest unit and [[systemOfUnits]] to switch between [[UNIT_SYSTEM_METRIC]] or [[UNIT_SYSTEM_IMPERIAL]].
1588
     *
1589
     * @param float|int $value value to be formatted.
1590
     * @param int|null $decimals the number of digits after the decimal point.
1591
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1592
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1593
     * @return string the formatted result.
1594
     * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
1595
     * @throws InvalidConfigException when INTL is not installed or does not contain required information.
1596
     * @since 2.0.13
1597
     * @author John Was <[email protected]>
1598
     */
1599 13
    public function asShortWeight($value, $decimals = null, $options = [], $textOptions = [])
1600
    {
1601 13
        return $this->formatUnit(self::UNIT_WEIGHT, self::FORMAT_WIDTH_SHORT, $value, $decimals, $options, $textOptions);
1602
    }
1603
1604
    /**
1605
     * @param string $unitType one of [[UNIT_WEIGHT]], [[UNIT_LENGTH]]
1606
     * @param string $unitFormat one of [[FORMAT_WIDTH_SHORT]], [[FORMAT_WIDTH_LONG]]
1607
     * @param float|int|null $value to be formatted
1608
     * @param int|null $decimals the number of digits after the decimal point.
1609
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1610
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1611
     * @return string
1612
     * @throws InvalidConfigException when INTL is not installed or does not contain required information
1613
     */
1614 54
    private function formatUnit($unitType, $unitFormat, $value, $decimals, $options, $textOptions)
1615
    {
1616 54
        if ($value === null) {
1617 4
            return $this->nullDisplay;
1618
        }
1619
1620 50
        $multipliers = array_values($this->measureUnits[$unitType][$this->systemOfUnits]);
1621
1622 50
        list($params, $position) = $this->formatNumber(
1623 50
            $this->normalizeNumericValue($value) * $this->baseUnits[$unitType][$this->systemOfUnits],
1624 50
            $decimals,
1625 50
            null,
1626 50
            $multipliers,
1627 50
            $options,
1628 50
            $textOptions
1629 50
        );
1630
1631 46
        $message = $this->getUnitMessage($unitType, $unitFormat, $this->systemOfUnits, $position);
1632
1633 44
        return (new \MessageFormatter($this->locale, $message))->format([
1634 44
            '0' => $params['nFormatted'],
1635 44
            'n' => $params['n'],
1636 44
        ]);
1637
    }
1638
1639
    /**
1640
     * @param string $unitType one of [[UNIT_WEIGHT]], [[UNIT_LENGTH]]
1641
     * @param string $unitFormat one of [[FORMAT_WIDTH_SHORT]], [[FORMAT_WIDTH_LONG]]
1642
     * @param string|null $system either [[UNIT_SYSTEM_METRIC]] or [[UNIT_SYSTEM_IMPERIAL]]. When `null`, property [[systemOfUnits]] will be used.
1643
     * @param int $position internal position of size unit
1644
     * @return string
1645
     * @throws InvalidConfigException when INTL is not installed or does not contain required information
1646
     */
1647 46
    private function getUnitMessage($unitType, $unitFormat, $system, $position)
1648
    {
1649 46
        if (isset($this->_unitMessages[$unitType][$unitFormat][$system][$position])) {
1650
            return $this->_unitMessages[$unitType][$unitFormat][$system][$position];
1651
        }
1652 46
        if (!$this->_intlLoaded) {
1653 2
            throw new InvalidConfigException('Format of ' . $unitType . ' is only supported when PHP intl extension is installed.');
1654
        }
1655
1656 44
        if ($this->_resourceBundle === null) {
1657
            try {
1658 44
                $this->_resourceBundle = new \ResourceBundle($this->locale, 'ICUDATA-unit');
1659
            } catch (\IntlException $e) {
1660
                throw new InvalidConfigException('Current ICU data does not contain information about measure units. Check system requirements.');
1661
            }
1662
        }
1663 44
        $unitNames = array_keys($this->measureUnits[$unitType][$system]);
1664 44
        $bundleKey = 'units' . ($unitFormat === self::FORMAT_WIDTH_SHORT ? 'Short' : '');
1665
1666 44
        $unitBundle = $this->_resourceBundle[$bundleKey][$unitType][$unitNames[$position]];
1667 44
        if ($unitBundle === null) {
1668
            throw new InvalidConfigException(
1669
                'Current ICU data version does not contain information about unit type "' . $unitType
1670
                . '" and unit measure "' . $unitNames[$position] . '". Check system requirements.'
1671
            );
1672
        }
1673
1674 44
        $message = [];
1675 44
        foreach ($unitBundle as $key => $value) {
1676 44
            if ($key === 'dnam') {
1677 44
                continue;
1678
            }
1679 44
            $message[] = "$key{{$value}}";
1680
        }
1681
1682 44
        return $this->_unitMessages[$unitType][$unitFormat][$system][$position] = '{n, plural, ' . implode(' ', $message) . '}';
1683
    }
1684
1685
    /**
1686
     * Given the value in bytes formats number part of the human readable form.
1687
     *
1688
     * @param string|int|float $value value in bytes to be formatted.
1689
     * @param int|null $decimals the number of digits after the decimal point
1690
     * @param int $maxPosition maximum internal position of size unit, ignored if $formatBase is an array
1691
     * @param array|int $formatBase the base at which each next unit is calculated, either 1000 or 1024, or an array
1692
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1693
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1694
     * @return array [parameters for Yii::t containing formatted number, internal position of size unit]
1695
     * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
1696
     * @since 2.0.32
1697
     */
1698 55
    protected function formatNumber($value, $decimals, $maxPosition, $formatBase, $options, $textOptions)
1699
    {
1700 55
        $value = $this->normalizeNumericValue($value);
1701
1702 55
        $position = 0;
1703 55
        if (is_array($formatBase)) {
1704 46
            $maxPosition = count($formatBase) - 1;
1705
        }
1706
        do {
1707 55
            if (is_array($formatBase)) {
1708 46
                if (!isset($formatBase[$position + 1])) {
1709 12
                    break;
1710
                }
1711
1712 46
                if (abs($value) < $formatBase[$position + 1]) {
1713 46
                    break;
1714
                }
1715
            } else {
1716 9
                if (abs($value) < $formatBase) {
1717 9
                    break;
1718
                }
1719 7
                $value /= $formatBase;
1720
            }
1721 37
            $position++;
1722 37
        } while ($position < $maxPosition + 1);
1723
1724 55
        if (is_array($formatBase) && $position !== 0) {
1725 30
            $value /= $formatBase[$position];
1726
        }
1727
1728
        // no decimals for smallest unit
1729 55
        if ($position === 0) {
1730 25
            $decimals = 0;
1731 37
        } elseif ($decimals !== null) {
1732 10
            $value = round($value, $decimals);
1733
        }
1734
        // disable grouping for edge cases like 1023 to get 1023 B instead of 1,023 B
1735 55
        $oldThousandSeparator = $this->thousandSeparator;
1736 55
        $this->thousandSeparator = '';
1737 55
        if ($this->_intlLoaded && !isset($options[NumberFormatter::GROUPING_USED])) {
1738 49
            $options[NumberFormatter::GROUPING_USED] = 0;
1739
        }
1740
        // format the size value
1741 55
        $params = [
1742
            // this is the unformatted number used for the plural rule
1743
            // abs() to make sure the plural rules work correctly on negative numbers, intl does not cover this
1744
            // https://english.stackexchange.com/questions/9735/is-1-followed-by-a-singular-or-plural-noun
1745 55
            'n' => abs($value),
1746
            // this is the formatted number used for display
1747 55
            'nFormatted' => $this->asDecimal($value, $decimals, $options, $textOptions),
1748 55
        ];
1749 55
        $this->thousandSeparator = $oldThousandSeparator;
1750
1751 55
        return [$params, $position];
1752
    }
1753
1754
    /**
1755
     * Normalizes a numeric input value.
1756
     *
1757
     * - everything [empty](https://www.php.net/manual/en/function.empty.php) will result in `0`
1758
     * - a [numeric](https://www.php.net/manual/en/function.is-numeric.php) string will be casted to float
1759
     * - everything else will be returned if it is [numeric](https://www.php.net/manual/en/function.is-numeric.php),
1760
     *   otherwise an exception is thrown.
1761
     *
1762
     * @param mixed $value the input value
1763
     * @return float|int the normalized number value
1764
     * @throws InvalidArgumentException if the input value is not numeric.
1765
     */
1766 95
    protected function normalizeNumericValue($value)
1767
    {
1768 95
        if (empty($value)) {
1769 20
            return 0;
1770
        }
1771 91
        if (is_string($value) && is_numeric($value)) {
1772 21
            $value = (float) $value;
1773
        }
1774 91
        if (!is_numeric($value)) {
1775 6
            throw new InvalidArgumentException("'$value' is not a numeric value.");
1776
        }
1777
1778 85
        return $value;
1779
    }
1780
1781
    /**
1782
     * Creates a number formatter based on the given type and format.
1783
     *
1784
     * You may override this method to create a number formatter based on patterns.
1785
     *
1786
     * @param int $style the type of the number formatter.
1787
     * Values: NumberFormatter::DECIMAL, ::CURRENCY, ::PERCENT, ::SCIENTIFIC, ::SPELLOUT, ::ORDINAL
1788
     * ::DURATION, ::PATTERN_RULEBASED, ::DEFAULT_STYLE, ::IGNORE
1789
     * @param int|null $decimals the number of digits after the decimal point.
1790
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1791
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1792
     * @return NumberFormatter the created formatter instance
1793
     */
1794 76
    protected function createNumberFormatter($style, $decimals = null, $options = [], $textOptions = [])
1795
    {
1796 76
        $formatter = new NumberFormatter($this->locale, $style);
1797
1798
        // set text attributes
1799 76
        foreach ($this->numberFormatterTextOptions as $attribute => $value) {
1800 5
            $this->setFormatterTextAttribute($formatter, $attribute, $value, 'numberFormatterTextOptions', 'numberFormatterOptions');
1801
        }
1802 73
        foreach ($textOptions as $attribute => $value) {
1803 10
            $this->setFormatterTextAttribute($formatter, $attribute, $value, '$textOptions', '$options');
1804
        }
1805
1806
        // set attributes
1807 70
        foreach ($this->numberFormatterOptions as $attribute => $value) {
1808 12
            $this->setFormatterIntAttribute($formatter, $attribute, $value, 'numberFormatterOptions', 'numberFormatterTextOptions');
1809
        }
1810 67
        foreach ($options as $attribute => $value) {
1811 54
            $this->setFormatterIntAttribute($formatter, $attribute, $value, '$options', '$textOptions');
1812
        }
1813 64
        if ($decimals !== null) {
1814 26
            $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $decimals);
1815 26
            $formatter->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $decimals);
1816
        }
1817
1818
        // set symbols
1819 64
        if ($this->decimalSeparator !== null) {
1820 5
            $formatter->setSymbol(NumberFormatter::DECIMAL_SEPARATOR_SYMBOL, $this->decimalSeparator);
1821
        }
1822 64
        if ($this->currencyDecimalSeparator !== null) {
1823 1
            $formatter->setSymbol(NumberFormatter::MONETARY_SEPARATOR_SYMBOL, $this->currencyDecimalSeparator);
1824
        }
1825 64
        if ($this->thousandSeparator !== null) {
1826 51
            $formatter->setSymbol(NumberFormatter::GROUPING_SEPARATOR_SYMBOL, $this->thousandSeparator);
1827 51
            $formatter->setSymbol(NumberFormatter::MONETARY_GROUPING_SEPARATOR_SYMBOL, $this->thousandSeparator);
1828
        }
1829 64
        foreach ($this->numberFormatterSymbols as $symbol => $value) {
1830 4
            $this->setFormatterSymbol($formatter, $symbol, $value, 'numberFormatterSymbols');
1831
        }
1832
1833 62
        return $formatter;
1834
    }
1835
1836
    /**
1837
     * @param NumberFormatter $formatter
1838
     * @param mixed $attribute
1839
     * @param mixed $value
1840
     * @param string $source
1841
     * @param string $alternative
1842
     */
1843 14
    private function setFormatterTextAttribute($formatter, $attribute, $value, $source, $alternative)
1844
    {
1845 14
        if (!is_int($attribute)) {
1846 2
            throw new InvalidArgumentException(
1847 2
                "The $source array keys must be integers recognizable by NumberFormatter::setTextAttribute(). \""
1848 2
                . gettype($attribute) . '" provided instead.'
1849 2
            );
1850
        }
1851 12
        if (!is_string($value)) {
1852 4
            if (is_int($value)) {
1853 2
                throw new InvalidArgumentException(
1854 2
                    "The $source array values must be strings. Did you mean to use $alternative?"
1855 2
                );
1856
            }
1857 2
            throw new InvalidArgumentException(
1858 2
                "The $source array values must be strings. \"" . gettype($value) . '" provided instead.'
1859 2
            );
1860
        }
1861 8
        $formatter->setTextAttribute($attribute, $value);
1862
    }
1863
1864
    /**
1865
     * @param NumberFormatter $formatter
1866
     * @param mixed $symbol
1867
     * @param mixed $value
1868
     * @param string $source
1869
     */
1870 4
    private function setFormatterSymbol($formatter, $symbol, $value, $source)
1871
    {
1872 4
        if (!is_int($symbol)) {
1873 1
            throw new InvalidArgumentException(
1874 1
                "The $source array keys must be integers recognizable by NumberFormatter::setSymbol(). \""
1875 1
                . gettype($symbol) . '" provided instead.'
1876 1
            );
1877
        }
1878 3
        if (!is_string($value)) {
1879 1
            throw new InvalidArgumentException(
1880 1
                "The $source array values must be strings. \"" . gettype($value) . '" provided instead.'
1881 1
            );
1882
        }
1883 2
        $formatter->setSymbol($symbol, $value);
1884
    }
1885
1886
    /**
1887
     * @param NumberFormatter $formatter
1888
     * @param mixed $attribute
1889
     * @param mixed $value
1890
     * @param string $source
1891
     * @param string $alternative
1892
     */
1893 63
    private function setFormatterIntAttribute($formatter, $attribute, $value, $source, $alternative)
1894
    {
1895 63
        if (!is_int($attribute)) {
1896 2
            throw new InvalidArgumentException(
1897 2
                "The $source array keys must be integers recognizable by NumberFormatter::setAttribute(). \""
1898 2
                . gettype($attribute) . '" provided instead.'
1899 2
            );
1900
        }
1901 61
        if (!is_int($value)) {
1902 4
            if (is_string($value)) {
1903 2
                throw new InvalidArgumentException(
1904 2
                    "The $source array values must be integers. Did you mean to use $alternative?"
1905 2
                );
1906
            }
1907 2
            throw new InvalidArgumentException(
1908 2
                "The $source array values must be integers. \"" . gettype($value) . '" provided instead.'
1909 2
            );
1910
        }
1911 57
        $formatter->setAttribute($attribute, $value);
1912
    }
1913
1914
    /**
1915
     * Checks if string representations of given value and its normalized version are different.
1916
     * @param string|float|int $value
1917
     * @param float|int $normalizedValue
1918
     * @return bool
1919
     * @since 2.0.16
1920
     */
1921 84
    protected function isNormalizedValueMispresented($value, $normalizedValue)
1922
    {
1923 84
        if (empty($value)) {
1924 18
            $value = 0;
1925
        }
1926
1927 84
        return (string) $normalizedValue !== $this->normalizeNumericStringValue((string) $value);
1928
    }
1929
1930
    /**
1931
     * Normalizes a numeric string value.
1932
     * @param string $value
1933
     * @return string the normalized number value as a string
1934
     * @since 2.0.16
1935
     */
1936 84
    protected function normalizeNumericStringValue($value)
1937
    {
1938 84
        $powerPosition = strrpos($value, 'E');
1939 84
        if ($powerPosition !== false) {
1940 4
            $valuePart = substr($value, 0, $powerPosition);
1941 4
            $powerPart = substr($value, $powerPosition + 1);
1942
        } else {
1943 84
            $powerPart = null;
1944 84
            $valuePart = $value;
1945
        }
1946
1947 84
        $separatorPosition = strrpos($valuePart, '.');
1948
1949 84
        if ($separatorPosition !== false) {
1950 39
            $integerPart = substr($valuePart, 0, $separatorPosition);
1951 39
            $fractionalPart = substr($valuePart, $separatorPosition + 1);
1952
        } else {
1953 64
            $integerPart = $valuePart;
1954 64
            $fractionalPart = null;
1955
        }
1956
1957
        // truncate insignificant zeros, keep minus
1958 84
        $integerPart = preg_replace('/^\+?(-?)0*(\d+)$/', '$1$2', $integerPart);
1959
        // for zeros only leave one zero, keep minus
1960 84
        $integerPart = preg_replace('/^\+?(-?)0*$/', '${1}0', $integerPart);
1961
1962 84
        if ($fractionalPart !== null) {
1963
            // truncate insignificant zeros
1964 39
            $fractionalPart = rtrim($fractionalPart, '0');
1965
1966 39
            if (empty($fractionalPart)) {
1967 7
                $fractionalPart = $powerPart !== null ? '0' : null;
1968
            }
1969
        }
1970
1971 84
        $normalizedValue = $integerPart;
1972 84
        if ($fractionalPart !== null) {
1973 38
            $normalizedValue .= '.' . $fractionalPart;
1974 64
        } elseif ($normalizedValue === '-0') {
1975
            $normalizedValue = '0';
1976
        }
1977
1978 84
        if ($powerPart !== null) {
1979 4
            $normalizedValue .= 'E' . $powerPart;
1980
        }
1981
1982 84
        return $normalizedValue;
1983
    }
1984
1985
    /**
1986
     * Fallback for formatting value as a decimal number.
1987
     *
1988
     * Property [[decimalSeparator]] will be used to represent the decimal point. The value is rounded automatically
1989
     * to the defined decimal digits.
1990
     *
1991
     * @param string|int|float $value the value to be formatted.
1992
     * @param int|null $decimals the number of digits after the decimal point. The default value is `2`.
1993
     * @return string the formatted result.
1994
     * @see decimalSeparator
1995
     * @see thousandSeparator
1996
     * @since 2.0.16
1997
     */
1998 11
    protected function asDecimalStringFallback($value, $decimals = 2)
1999
    {
2000 11
        if (empty($value)) {
2001
            $value = 0;
2002
        }
2003
2004 11
        $value = $this->normalizeNumericStringValue((string) $value);
2005
2006 11
        $separatorPosition = strrpos($value, '.');
2007
2008 11
        if ($separatorPosition !== false) {
2009 6
            $integerPart = substr($value, 0, $separatorPosition);
2010 6
            $fractionalPart = substr($value, $separatorPosition + 1);
2011
        } else {
2012 11
            $integerPart = $value;
2013 11
            $fractionalPart = null;
2014
        }
2015
2016 11
        $decimalOutput = '';
2017
2018 11
        if ($decimals === null) {
2019 2
            $decimals = 2;
2020
        }
2021
2022 11
        $carry = 0;
2023
2024 11
        if ($decimals > 0) {
2025 6
            $decimalSeparator = $this->decimalSeparator;
2026 6
            if ($this->decimalSeparator === null) {
2027 3
                $decimalSeparator = '.';
2028
            }
2029
2030 6
            if ($fractionalPart === null) {
2031 4
                $fractionalPart = str_repeat('0', $decimals);
2032 6
            } elseif (strlen($fractionalPart) > $decimals) {
2033 4
                $cursor = $decimals;
2034
2035
                // checking if fractional part must be rounded
2036 4
                if ((int) substr($fractionalPart, $cursor, 1) >= 5) {
2037 1
                    while (--$cursor >= 0) {
2038 1
                        $carry = 0;
2039
2040 1
                        $oneUp = (int) substr($fractionalPart, $cursor, 1) + 1;
2041 1
                        if ($oneUp === 10) {
2042
                            $oneUp = 0;
2043
                            $carry = 1;
2044
                        }
2045
2046 1
                        $fractionalPart = substr($fractionalPart, 0, $cursor) . $oneUp . substr($fractionalPart, $cursor + 1);
2047
2048 1
                        if ($carry === 0) {
2049 1
                            break;
2050
                        }
2051
                    }
2052
                }
2053
2054 4
                $fractionalPart = substr($fractionalPart, 0, $decimals);
2055 2
            } elseif (strlen($fractionalPart) < $decimals) {
2056 2
                $fractionalPart = str_pad($fractionalPart, $decimals, '0');
2057
            }
2058
2059 6
            $decimalOutput .= $decimalSeparator . $fractionalPart;
2060
        }
2061
2062
        // checking if integer part must be rounded
2063 11
        if ($carry || ($decimals === 0 && $fractionalPart !== null && (int) substr($fractionalPart, 0, 1) >= 5)) {
2064 4
            $integerPartLength = strlen($integerPart);
2065 4
            $cursor = 0;
2066
2067 4
            while (++$cursor <= $integerPartLength) {
2068 4
                $carry = 0;
2069
2070 4
                $oneUp = (int) substr($integerPart, -$cursor, 1) + 1;
2071 4
                if ($oneUp === 10) {
2072
                    $oneUp = 0;
2073
                    $carry = 1;
2074
                }
2075
2076 4
                $integerPart = substr($integerPart, 0, -$cursor) . $oneUp . substr($integerPart, $integerPartLength - $cursor + 1);
2077
2078 4
                if ($carry === 0) {
2079 4
                    break;
2080
                }
2081
            }
2082 4
            if ($carry === 1) {
2083
                $integerPart = '1' . $integerPart;
2084
            }
2085
        }
2086
2087 11
        if (strlen($integerPart) > 3) {
2088 11
            $thousandSeparator = $this->thousandSeparator;
2089 11
            if ($thousandSeparator === null) {
2090 8
                $thousandSeparator = ',';
2091
            }
2092
2093 11
            $integerPart = strrev(implode(',', str_split(strrev($integerPart), 3)));
0 ignored issues
show
Bug introduced by
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

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