Completed
Push — master ( 55b06d...9f2a87 )
by Alexander
35:57
created

framework/i18n/Formatter.php (3 issues)

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