GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

Formatter::asDecimalStringFallback()   F
last analyzed

Complexity

Conditions 24
Paths 8360

Size

Total Lines 102
Code Lines 58

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 52
CRAP Score 24.6367

Importance

Changes 0
Metric Value
cc 24
eloc 58
nc 8360
nop 2
dl 0
loc 102
ccs 52
cts 58
cp 0.8966
crap 24.6367
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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

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

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

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