Passed
Push — skip-scientific-format-test ( 645bf8 )
by Alexander
46:20 queued 12:27
created

Formatter::formatUnit()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 28
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 18
nc 5
nop 8
dl 0
loc 28
ccs 19
cts 19
cp 1
crap 4
rs 9.6666
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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

883
        return number_format(/** @scrutinizer ignore-type */ $timestamp->format('U'), 0, '.', '');
Loading history...
884
    }
885
886
    /**
887
     * Formats the value as the time interval between a date and now in human readable form.
888
     *
889
     * This method can be used in three different ways:
890
     *
891
     * 1. Using a timestamp that is relative to `now`.
892
     * 2. Using a timestamp that is relative to the `$referenceTime`.
893
     * 3. Using a `DateInterval` object.
894
     *
895
     * @param int|string|DateTime|DateInterval $value the value to be formatted. The following
896
     * types of value are supported:
897
     *
898
     * - an integer representing a UNIX timestamp
899
     * - a string that can be [parsed to create a DateTime object](https://secure.php.net/manual/en/datetime.formats.php).
900
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
901
     * - a PHP [DateTime](https://secure.php.net/manual/en/class.datetime.php) object
902
     * - a PHP DateInterval object (a positive time interval will refer to the past, a negative one to the future)
903
     *
904
     * @param int|string|DateTime $referenceTime if specified the value is used as a reference time instead of `now`
905
     * when `$value` is not a `DateInterval` object.
906
     * @return string the formatted result.
907
     * @throws InvalidArgumentException if the input value can not be evaluated as a date value.
908
     */
909 92
    public function asRelativeTime($value, $referenceTime = null)
910
    {
911 92
        if ($value === null) {
912 2
            return $this->nullDisplay;
913
        }
914
915 92
        if ($value instanceof DateInterval) {
916 2
            $interval = $value;
917
        } else {
918 92
            $timestamp = $this->normalizeDatetimeValue($value);
919
920 92
            if ($timestamp === false) {
0 ignored issues
show
introduced by
The condition $timestamp === false is always false.
Loading history...
921
                // $value is not a valid date/time value, so we try
922
                // to create a DateInterval with it
923
                try {
924
                    $interval = new DateInterval($value);
925
                } catch (\Exception $e) {
926
                    // invalid date/time and invalid interval
927
                    return $this->nullDisplay;
928
                }
929
            } else {
930 92
                $timeZone = new DateTimeZone($this->timeZone);
931
932 92
                if ($referenceTime === null) {
933
                    $dateNow = new DateTime('now', $timeZone);
934
                } else {
935 92
                    $dateNow = $this->normalizeDatetimeValue($referenceTime);
936 92
                    $dateNow->setTimezone($timeZone);
937
                }
938
939 92
                $dateThen = $timestamp->setTimezone($timeZone);
940
941 92
                $interval = $dateThen->diff($dateNow);
942
            }
943
        }
944
945 92
        if ($interval->invert) {
946 92
            if ($interval->y >= 1) {
947 2
                return Yii::t('yii', 'in {delta, plural, =1{a year} other{# years}}', ['delta' => $interval->y], $this->language);
948
            }
949 92
            if ($interval->m >= 1) {
950 2
                return Yii::t('yii', 'in {delta, plural, =1{a month} other{# months}}', ['delta' => $interval->m], $this->language);
951
            }
952 92
            if ($interval->d >= 1) {
953 2
                return Yii::t('yii', 'in {delta, plural, =1{a day} other{# days}}', ['delta' => $interval->d], $this->language);
954
            }
955 92
            if ($interval->h >= 1) {
956 92
                return Yii::t('yii', 'in {delta, plural, =1{an hour} other{# hours}}', ['delta' => $interval->h], $this->language);
957
            }
958 2
            if ($interval->i >= 1) {
959 2
                return Yii::t('yii', 'in {delta, plural, =1{a minute} other{# minutes}}', ['delta' => $interval->i], $this->language);
960
            }
961 2
            if ($interval->s == 0) {
962
                return Yii::t('yii', 'just now', [], $this->language);
963
            }
964
965 2
            return Yii::t('yii', 'in {delta, plural, =1{a second} other{# seconds}}', ['delta' => $interval->s], $this->language);
966
        }
967
968 92
        if ($interval->y >= 1) {
969 2
            return Yii::t('yii', '{delta, plural, =1{a year} other{# years}} ago', ['delta' => $interval->y], $this->language);
970
        }
971 92
        if ($interval->m >= 1) {
972 2
            return Yii::t('yii', '{delta, plural, =1{a month} other{# months}} ago', ['delta' => $interval->m], $this->language);
973
        }
974 92
        if ($interval->d >= 1) {
975 2
            return Yii::t('yii', '{delta, plural, =1{a day} other{# days}} ago', ['delta' => $interval->d], $this->language);
976
        }
977 92
        if ($interval->h >= 1) {
978 92
            return Yii::t('yii', '{delta, plural, =1{an hour} other{# hours}} ago', ['delta' => $interval->h], $this->language);
979
        }
980 2
        if ($interval->i >= 1) {
981 2
            return Yii::t('yii', '{delta, plural, =1{a minute} other{# minutes}} ago', ['delta' => $interval->i], $this->language);
982
        }
983 2
        if ($interval->s == 0) {
984 2
            return Yii::t('yii', 'just now', [], $this->language);
985
        }
986
987 2
        return Yii::t('yii', '{delta, plural, =1{a second} other{# seconds}} ago', ['delta' => $interval->s], $this->language);
988
    }
989
990
    /**
991
     * Represents the value as duration in human readable format.
992
     *
993
     * @param DateInterval|string|int $value the value to be formatted. Acceptable formats:
994
     *  - [DateInterval object](https://secure.php.net/manual/ru/class.dateinterval.php)
995
     *  - integer - number of seconds. For example: value `131` represents `2 minutes, 11 seconds`
996
     *  - ISO8601 duration format. For example, all of these values represent `1 day, 2 hours, 30 minutes` duration:
997
     *    `2015-01-01T13:00:00Z/2015-01-02T13:30:00Z` - between two datetime values
998
     *    `2015-01-01T13:00:00Z/P1D2H30M` - time interval after datetime value
999
     *    `P1D2H30M/2015-01-02T13:30:00Z` - time interval before datetime value
1000
     *    `P1D2H30M` - simply a date interval
1001
     *    `P-1D2H30M` - a negative date interval (`-1 day, 2 hours, 30 minutes`)
1002
     *
1003
     * @param string $implodeString will be used to concatenate duration parts. Defaults to `, `.
1004
     * @param string $negativeSign will be prefixed to the formatted duration, when it is negative. Defaults to `-`.
1005
     * @return string the formatted duration.
1006
     * @since 2.0.7
1007
     */
1008 2
    public function asDuration($value, $implodeString = ', ', $negativeSign = '-')
1009
    {
1010 2
        if ($value === null) {
1011 2
            return $this->nullDisplay;
1012
        }
1013
1014 2
        if ($value instanceof DateInterval) {
1015 2
            $isNegative = $value->invert;
1016 2
            $interval = $value;
1017 2
        } elseif (is_numeric($value)) {
1018 2
            $isNegative = $value < 0;
1019 2
            $zeroDateTime = (new DateTime())->setTimestamp(0);
1020 2
            $valueDateTime = (new DateTime())->setTimestamp(abs($value));
0 ignored issues
show
Bug introduced by
It seems like abs($value) can also be of type double; however, parameter $unixtimestamp 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

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